sourcecode 1.67.0__tar.gz → 1.68.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-1.67.0 → sourcecode-1.68.0}/CHANGELOG.md +45 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/PKG-INFO +1 -1
- {sourcecode-1.67.0 → sourcecode-1.68.0}/pyproject.toml +1 -1
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/cli.py +21 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/integration_detector.py +109 -10
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/migrate_check.py +56 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/repository_ir.py +57 -7
- {sourcecode-1.67.0 → sourcecode-1.68.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/.gitignore +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/.ruff.toml +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/LICENSE +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/README.md +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/SECURITY.md +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/raw +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/validation_surface.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.67.0 → sourcecode-1.68.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.68.0] — 2026-06-30
|
|
4
|
+
|
|
5
|
+
### Fixed — narrative/structured-data parity on non-Spring DI repos (Netflix Eureka field test)
|
|
6
|
+
|
|
7
|
+
A real audit of **Netflix Eureka** (Gradle multi-module, JAX-RS/Jersey 1.x + Google
|
|
8
|
+
Guice, **no Spring, no Hibernate, no persistence layer**) surfaced six reliability
|
|
9
|
+
defects that share one root cause: **prose that assumed a Spring/Hibernate/persistence
|
|
10
|
+
context even though the same JSON's own structured fields already knew it did not
|
|
11
|
+
apply.** A consumer trusting the narrative over the structured fields would have read
|
|
12
|
+
false conclusions. Guiding principle applied throughout: **a narrative term (Spring,
|
|
13
|
+
Guice, persistence, …) may only appear when the corresponding structured signal
|
|
14
|
+
confirms it.** New regression coverage across four suites.
|
|
15
|
+
|
|
16
|
+
- **SMTP false positive on a log string (BUG #1).** `export --integrations` reported
|
|
17
|
+
`kind=smtp` for `logger.warn("Transport initialization failure", e)` — the bare word
|
|
18
|
+
"Transport" in a log literal. The detector now (a) skips any token matched inside a
|
|
19
|
+
string literal and (b) gates the JavaMail tokens (`Transport`, `MimeMessage`) on a
|
|
20
|
+
`javax.mail`/`jakarta.mail`/`org.springframework.mail` import. Eureka SMTP hits 3 → 0.
|
|
21
|
+
- **JNDI DNS mislabeled as LDAP (BUG #2).** `javax.naming.directory.{Dir,Initial}Context`
|
|
22
|
+
is protocol-agnostic; it is now classified by its `INITIAL_CONTEXT_FACTORY` —
|
|
23
|
+
`DnsContextFactory` → `kind=dns` (`client=jndi-dns`), `LdapCtxFactory` → `ldap`, and an
|
|
24
|
+
unresolvable factory → `kind=naming-directory-unknown` with `confidence=low`, never an
|
|
25
|
+
assumed LDAP. Eureka `DnsResolver` now reports `dns`.
|
|
26
|
+
- **"Spring Boot 3" narrative on a non-Spring repo (BUG #3).** When `spring_present` is
|
|
27
|
+
`False`, every `migrate-check` finding's `explanation` is rewritten from Spring-Boot
|
|
28
|
+
framing into framework-neutral Jakarta-EE / servlet-container framing (the javax→jakarta
|
|
29
|
+
finding itself is unchanged). Guarantee: no explanation contains "Spring Boot" unless
|
|
30
|
+
the report confirms runtime Spring.
|
|
31
|
+
- **Inconsistent DI-framework label (BUG #4).** `impact` emitted "Spring/CDI DI pattern"
|
|
32
|
+
in the short `explanation` but "Spring/CDI/Guice" in `via_interface_note` of the *same*
|
|
33
|
+
JSON, while `direct_callers` proved the framework was Guice. Both strings now derive
|
|
34
|
+
from one label computed from caller evidence — a `*.guice.*` caller yields "Guice".
|
|
35
|
+
- **"persistence paths" for non-persistence mappers (BUG #5).** Eureka's `AzToRegionMapper`
|
|
36
|
+
classes (AWS topology, no data layer) were counted as "4 persistence paths". Each
|
|
37
|
+
`mappers_affected` entry now carries `mapper_kind` (`persistence` only for a confirmed
|
|
38
|
+
`@Repository` role or MyBatis `mapper_interface`; otherwise `name_heuristic`). The
|
|
39
|
+
explanation says "persistence path" only for confirmed entries and neutral
|
|
40
|
+
"data-mapping class" otherwise — the word "persistence" never appears for a repo with no
|
|
41
|
+
confirmed persistence signal.
|
|
42
|
+
- **Ambiguous `member_count` unit in `modernize` (BUG #6).** `subsystem_summary.member_count`
|
|
43
|
+
counts all graph members (classes + methods + fields), ~5× the class count and not
|
|
44
|
+
comparable to repo-level `total_classes`. Each entry now also carries an unambiguous
|
|
45
|
+
`class_count` (class/interface declarations only, comparable to `total_classes`), with a
|
|
46
|
+
`subsystem_summary_note` documenting both units.
|
|
47
|
+
|
|
3
48
|
## [1.67.0] — 2026-06-30
|
|
4
49
|
|
|
5
50
|
### Fixed — framing accuracy on non-Spring repos (Apache OFBiz field test)
|
|
@@ -5777,14 +5777,32 @@ def modernize_cmd(
|
|
|
5777
5777
|
"statically_unreferenced": len(dead_zones),
|
|
5778
5778
|
"framework_dispatched": len(framework_dispatched),
|
|
5779
5779
|
}
|
|
5780
|
+
# BUG #6 (v1.68.0): `member_count` counts ALL graph members in the subsystem —
|
|
5781
|
+
# classes, methods AND fields — so it runs ~5x higher than the class count and
|
|
5782
|
+
# is not comparable to the repo-level `total_classes` (which counts only
|
|
5783
|
+
# class/interface nodes). Surface an unambiguous `class_count` alongside it,
|
|
5784
|
+
# computed against the same class/interface node set as total_classes, so a
|
|
5785
|
+
# reader does not have to reverse-engineer the unit. `member_count` is retained
|
|
5786
|
+
# for backward compatibility but is now documented as the graph-member tally.
|
|
5787
|
+
_class_fqns = {n["fqn"] for n in graph_nodes if n.get("type") in ("class", "interface")}
|
|
5788
|
+
|
|
5789
|
+
def _class_count(members: list) -> int:
|
|
5790
|
+
return sum(1 for m in members if m in _class_fqns)
|
|
5791
|
+
|
|
5780
5792
|
_subsystem_summary = [
|
|
5781
5793
|
{
|
|
5782
5794
|
"label": s.get("label") or s.get("name") or "",
|
|
5783
5795
|
"package_prefix": s.get("package_prefix") or s.get("pkg") or "",
|
|
5796
|
+
"class_count": _class_count(s.get("members") or []),
|
|
5784
5797
|
"member_count": len(s.get("members") or []),
|
|
5785
5798
|
}
|
|
5786
5799
|
for s in subsystems[:15]
|
|
5787
5800
|
]
|
|
5801
|
+
_subsystem_summary_note = (
|
|
5802
|
+
"class_count = class/interface declarations only (comparable to "
|
|
5803
|
+
"summary.total_classes). member_count = all graph members in the subsystem "
|
|
5804
|
+
"including methods and fields, so it is larger and not a class tally."
|
|
5805
|
+
)
|
|
5788
5806
|
|
|
5789
5807
|
if (not _mod_is_pro) and _mod_large(str(root)):
|
|
5790
5808
|
# Large monolith, free tier: structural discovery preview only — no dead
|
|
@@ -5801,6 +5819,7 @@ def modernize_cmd(
|
|
|
5801
5819
|
),
|
|
5802
5820
|
"summary": _summary,
|
|
5803
5821
|
"subsystem_summary": _subsystem_summary,
|
|
5822
|
+
"subsystem_summary_note": _subsystem_summary_note,
|
|
5804
5823
|
"hotspot_candidates": hotspots[:3],
|
|
5805
5824
|
"high_coupling_nodes": [
|
|
5806
5825
|
{"fqn": n["fqn"], "in_degree": n.get("in_degree", 0), "role": n.get("role", "other")}
|
|
@@ -5833,9 +5852,11 @@ def modernize_cmd(
|
|
|
5833
5852
|
for n in framework_dispatched
|
|
5834
5853
|
],
|
|
5835
5854
|
"subsystem_summary": _subsystem_summary,
|
|
5855
|
+
"subsystem_summary_note": _subsystem_summary_note,
|
|
5836
5856
|
"cross_module_tangles": [
|
|
5837
5857
|
{
|
|
5838
5858
|
"label": s.get("label") or s.get("name") or "",
|
|
5859
|
+
"class_count": _class_count(s.get("members") or []),
|
|
5839
5860
|
"member_count": len(s.get("members") or []),
|
|
5840
5861
|
}
|
|
5841
5862
|
for s in tangle_modules
|
|
@@ -7,8 +7,14 @@ deterministic source-text matching (same approach as the JNDI datasource scan in
|
|
|
7
7
|
|
|
8
8
|
Covered clients:
|
|
9
9
|
|
|
10
|
-
* HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative)
|
|
11
|
-
|
|
10
|
+
* HTTP — ``RestTemplate``, ``WebClient``, ``@FeignClient`` (declarative),
|
|
11
|
+
JDK/Apache/OkHttp clients
|
|
12
|
+
* LDAP — ``LdapTemplate``, JNDI ``InitialLdapContext``/``LdapContext``
|
|
13
|
+
* DNS — JNDI ``DirContext`` configured with ``DnsContextFactory`` (BUG #2:
|
|
14
|
+
``DirContext`` is protocol-agnostic and is classified by its
|
|
15
|
+
``INITIAL_CONTEXT_FACTORY``, not assumed to be LDAP)
|
|
16
|
+
* SMTP — JavaMail / Jakarta Mail (BUG #1: gated on a mail import so the bare
|
|
17
|
+
word "Transport" in a log string is not a false positive)
|
|
12
18
|
* JMS — ``JmsTemplate``, ActiveMQ connection factories
|
|
13
19
|
|
|
14
20
|
Each hit is reported with a ``file:line`` evidence anchor and, when a literal URL
|
|
@@ -28,6 +34,24 @@ _URL_RE = re.compile(r'"((?:https?|ldaps?|tcp|amqp|jms|nio)://[^"]*)"')
|
|
|
28
34
|
# First string literal on a line (fallback target, e.g. WebClient.create("x")).
|
|
29
35
|
_STR_RE = re.compile(r'"([^"]+)"')
|
|
30
36
|
|
|
37
|
+
# BUG #1 (v1.68.0): a JavaMail/Jakarta-Mail import is required before an SMTP token
|
|
38
|
+
# (Transport, MimeMessage) is trusted. The bare word "Transport" is also a common
|
|
39
|
+
# English noun that appears in log strings ( logger.warn("Transport initialization
|
|
40
|
+
# failure") in non-mail code), so a mail-import gate plus a string-literal skip
|
|
41
|
+
# stops those false positives.
|
|
42
|
+
_MAIL_IMPORT_RE = re.compile(
|
|
43
|
+
r"^\s*import\s+(?:(?:javax|jakarta)\.mail|org\.springframework\.mail)\b",
|
|
44
|
+
re.MULTILINE,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# BUG #2 (v1.68.0): javax.naming.directory.{DirContext,InitialDirContext} is NOT
|
|
48
|
+
# LDAP-specific — the actual protocol is decided by the value bound to
|
|
49
|
+
# Context.INITIAL_CONTEXT_FACTORY. com.sun.jndi.dns.DnsContextFactory means DNS
|
|
50
|
+
# (SRV/A record lookups), com.sun.jndi.ldap.LdapCtxFactory means LDAP. Classify by
|
|
51
|
+
# the factory class present in the file instead of defaulting to LDAP.
|
|
52
|
+
_DNS_FACTORY_RE = re.compile(r"jndi\.dns|DnsContextFactory", re.IGNORECASE)
|
|
53
|
+
_LDAP_FACTORY_RE = re.compile(r"jndi\.ldap|LdapCtxFactory", re.IGNORECASE)
|
|
54
|
+
|
|
31
55
|
# Declarative HTTP client. Attrs may span multiple lines, so matched on full text.
|
|
32
56
|
_FEIGN_RE = re.compile(r"@FeignClient\s*\(([^)]*)\)", re.DOTALL)
|
|
33
57
|
_ATTR_URL_RE = re.compile(r'url\s*=\s*"([^"]*)"')
|
|
@@ -82,6 +106,39 @@ def _extract_target(line: str) -> Optional[str]:
|
|
|
82
106
|
return None
|
|
83
107
|
|
|
84
108
|
|
|
109
|
+
def _in_string_literal(line: str, idx: int) -> bool:
|
|
110
|
+
"""True if char offset ``idx`` falls inside a double-quoted string on ``line``.
|
|
111
|
+
|
|
112
|
+
BUG #1 (v1.68.0): a Java type token never legitimately appears inside a string
|
|
113
|
+
literal, so a match there (e.g. the word "Transport" in a log message) is noise,
|
|
114
|
+
not a real client construct. Counts unescaped quotes before ``idx``.
|
|
115
|
+
"""
|
|
116
|
+
quote_count = 0
|
|
117
|
+
i = 0
|
|
118
|
+
while i < idx and i < len(line):
|
|
119
|
+
ch = line[i]
|
|
120
|
+
if ch == "\\":
|
|
121
|
+
i += 2
|
|
122
|
+
continue
|
|
123
|
+
if ch == '"':
|
|
124
|
+
quote_count += 1
|
|
125
|
+
i += 1
|
|
126
|
+
return quote_count % 2 == 1
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _classify_naming_factory(text: str) -> Optional[str]:
|
|
130
|
+
"""Classify a javax.naming.directory usage by its INITIAL_CONTEXT_FACTORY.
|
|
131
|
+
|
|
132
|
+
Returns ``"dns"``, ``"ldap"``, or ``None`` (factory not statically resolvable).
|
|
133
|
+
See BUG #2 — DirContext alone does not imply LDAP.
|
|
134
|
+
"""
|
|
135
|
+
if _DNS_FACTORY_RE.search(text):
|
|
136
|
+
return "dns"
|
|
137
|
+
if _LDAP_FACTORY_RE.search(text):
|
|
138
|
+
return "ldap"
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
85
142
|
def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
86
143
|
"""Detect outbound integrations across ``file_paths`` (relative to ``root``).
|
|
87
144
|
|
|
@@ -93,18 +150,31 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
93
150
|
seen: "set[tuple[str, str, Optional[str], str]]" = set()
|
|
94
151
|
records: "list[dict]" = []
|
|
95
152
|
|
|
96
|
-
def _add(
|
|
153
|
+
def _add(
|
|
154
|
+
kind: str,
|
|
155
|
+
client: str,
|
|
156
|
+
target: Optional[str],
|
|
157
|
+
rel: str,
|
|
158
|
+
line: int,
|
|
159
|
+
confidence: Optional[str] = None,
|
|
160
|
+
) -> None:
|
|
97
161
|
evidence = f"{rel}:{line}"
|
|
98
162
|
key = (kind, client, target, evidence)
|
|
99
163
|
if key in seen:
|
|
100
164
|
return
|
|
101
165
|
seen.add(key)
|
|
102
|
-
|
|
166
|
+
rec = {
|
|
103
167
|
"kind": kind,
|
|
104
168
|
"client": client,
|
|
105
169
|
"target": target,
|
|
106
170
|
"evidence": evidence,
|
|
107
|
-
}
|
|
171
|
+
}
|
|
172
|
+
# Per-record confidence is emitted only when the classification is uncertain
|
|
173
|
+
# (e.g. a JNDI DirContext whose factory could not be resolved). Confident hits
|
|
174
|
+
# stay schema-clean without the field.
|
|
175
|
+
if confidence is not None:
|
|
176
|
+
rec["confidence"] = confidence
|
|
177
|
+
records.append(rec)
|
|
108
178
|
|
|
109
179
|
for rel in file_paths:
|
|
110
180
|
try:
|
|
@@ -126,6 +196,10 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
126
196
|
)
|
|
127
197
|
_add("http", "feign", target, rel, _line_of(text, m.start()))
|
|
128
198
|
|
|
199
|
+
# Per-file context used to disambiguate / gate uncertain tokens (BUG #1/#2).
|
|
200
|
+
has_mail_import = bool(_MAIL_IMPORT_RE.search(text))
|
|
201
|
+
naming_factory = _classify_naming_factory(text)
|
|
202
|
+
|
|
129
203
|
# Token clients — per line, skipping imports/package/comment noise.
|
|
130
204
|
# First pass records the declaration site and any variable name bound to
|
|
131
205
|
# the client, so a later call site (where the URL literal usually lives)
|
|
@@ -146,7 +220,28 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
146
220
|
m = token_re.search(line)
|
|
147
221
|
if not m:
|
|
148
222
|
continue
|
|
149
|
-
|
|
223
|
+
# BUG #1: a token matched inside a string literal (log message, doc
|
|
224
|
+
# comment text) is never a real client construct — skip it.
|
|
225
|
+
if _in_string_literal(line, m.start()):
|
|
226
|
+
continue
|
|
227
|
+
confidence: Optional[str] = None
|
|
228
|
+
# BUG #1: SMTP tokens require a JavaMail/Jakarta-Mail import to be
|
|
229
|
+
# trusted — "Transport" / "MimeMessage" are too generic otherwise.
|
|
230
|
+
if kind == "smtp" and not has_mail_import:
|
|
231
|
+
continue
|
|
232
|
+
# BUG #2: javax.naming.directory.{Dir,Initial}Context is protocol-
|
|
233
|
+
# agnostic. Reclassify by the configured context factory; default to
|
|
234
|
+
# an explicit low-confidence "unknown" rather than assuming LDAP.
|
|
235
|
+
if client == "jndi-ldap" and "Dir" in token_re.pattern:
|
|
236
|
+
if naming_factory == "dns":
|
|
237
|
+
kind, client = "dns", "jndi-dns"
|
|
238
|
+
elif naming_factory == "ldap":
|
|
239
|
+
kind, client = "ldap", "jndi-ldap"
|
|
240
|
+
else:
|
|
241
|
+
kind, client, confidence = (
|
|
242
|
+
"naming-directory-unknown", "jndi-dircontext", "low",
|
|
243
|
+
)
|
|
244
|
+
_add(kind, client, _extract_target(line), rel, lineno, confidence)
|
|
150
245
|
tok = m.group(0)
|
|
151
246
|
# `Type name` (field/local decl) and `name = new Type(` forms.
|
|
152
247
|
decl = re.search(re.escape(tok) + r"\s+(\w+)\b", line)
|
|
@@ -186,9 +281,13 @@ def detect_integrations(file_paths: "list[str]", root: Path) -> dict:
|
|
|
186
281
|
"confidence": confidence,
|
|
187
282
|
"coverage_note": (
|
|
188
283
|
"Detects HTTP (RestTemplate/WebClient/JDK/Apache/OkHttp), LDAP (Spring "
|
|
189
|
-
"+ JNDI),
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"statically
|
|
284
|
+
"+ JNDI), DNS (JNDI DirContext w/ DnsContextFactory), SMTP (JavaMail, "
|
|
285
|
+
"import-gated), and JMS client constructs by source-text matching. JNDI "
|
|
286
|
+
"DirContext usage is classified by its INITIAL_CONTEXT_FACTORY (dns vs "
|
|
287
|
+
"ldap); when the factory is not statically resolvable the kind is "
|
|
288
|
+
"'naming-directory-unknown' with confidence='low', never assumed LDAP. A "
|
|
289
|
+
"count of 0 means no such construct was found, not that the system has no "
|
|
290
|
+
"outbound integrations — runtime/DI-wired clients are not statically "
|
|
291
|
+
"visible."
|
|
193
292
|
),
|
|
194
293
|
}
|
|
@@ -1631,6 +1631,53 @@ def _refine_mig006(matched_imports: list[str]) -> tuple[str, str]:
|
|
|
1631
1631
|
)
|
|
1632
1632
|
|
|
1633
1633
|
|
|
1634
|
+
# ---------------------------------------------------------------------------
|
|
1635
|
+
# BUG #3 (v1.68.0): framework-gated narrative.
|
|
1636
|
+
# MIG-* explanations are written assuming a Spring Boot 2→3 upgrade because that is
|
|
1637
|
+
# the overwhelmingly common Jakarta-migration context. On a repo where the tool has
|
|
1638
|
+
# already determined spring_present=False (plain Jakarta EE, JAX-RS/Jersey, Quarkus,
|
|
1639
|
+
# Guice, ...), the underlying javax→jakarta finding is still valid, but prose that
|
|
1640
|
+
# says "Spring Boot 3 requires ..." is factually wrong for that repo and misleads any
|
|
1641
|
+
# reader who trusts the narrative over the structured spring_present field.
|
|
1642
|
+
#
|
|
1643
|
+
# Treat the narrative as a pure function of the structured data: when spring_present
|
|
1644
|
+
# is False, rewrite Spring-specific framing into framework-neutral Jakarta-EE /
|
|
1645
|
+
# servlet-container framing. Ordered phrase map first (keeps nice sentences for the
|
|
1646
|
+
# common phrasings), then a catch-all so no "Spring Boot" string can survive.
|
|
1647
|
+
# ---------------------------------------------------------------------------
|
|
1648
|
+
_NON_SPRING_EXPLANATION_SUBS: tuple[tuple[str, str], ...] = (
|
|
1649
|
+
("Spring Boot 3 bundles Jakarta Servlet 6.0.",
|
|
1650
|
+
"Modern servlet containers (Jetty 12+, Tomcat 11+) require Jakarta Servlet 6.0."),
|
|
1651
|
+
("Spring Boot 3 requires Jakarta Servlet 5.0+",
|
|
1652
|
+
"Jakarta EE 9+ servlet containers require Jakarta Servlet 5.0+"),
|
|
1653
|
+
("Spring Boot 3 uses Jakarta EE 9 which moved",
|
|
1654
|
+
"Jakarta EE 9 moved"),
|
|
1655
|
+
("Spring Boot 3 uses Hibernate Validator 8.x which implements jakarta.validation.",
|
|
1656
|
+
"Jakarta EE 9+ uses Hibernate Validator 8.x which implements jakarta.validation."),
|
|
1657
|
+
("Spring Boot 3 depends on Jakarta Transactions (jakarta.transaction).",
|
|
1658
|
+
"Jakarta EE 9+ uses Jakarta Transactions (jakarta.transaction)."),
|
|
1659
|
+
("Spring Boot 3 requires Hibernate 6",
|
|
1660
|
+
"Jakarta Persistence (JPA 3.x) requires Hibernate 6"),
|
|
1661
|
+
("Spring Boot 3 cache abstraction. Spring Boot 3 requires EhCache 3.x",
|
|
1662
|
+
"Jakarta-EE cache abstraction. The modern JCache provider requires EhCache 3.x"),
|
|
1663
|
+
# Catch-all: any remaining Spring-Boot framing → generic Jakarta EE 9+.
|
|
1664
|
+
("Spring Boot 3", "Jakarta EE 9+"),
|
|
1665
|
+
("Spring Boot 2", "the pre-Jakarta (javax) baseline"),
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def _neutralize_non_spring_explanation(text: str) -> str:
|
|
1670
|
+
"""Rewrite Spring-specific migration prose into framework-neutral Jakarta framing.
|
|
1671
|
+
|
|
1672
|
+
Applied only when the report's spring_present is False. Deterministic phrase
|
|
1673
|
+
substitution — no model, no guess. Guarantees the returned text contains no
|
|
1674
|
+
"Spring Boot" framing that the structured data does not support.
|
|
1675
|
+
"""
|
|
1676
|
+
for needle, repl in _NON_SPRING_EXPLANATION_SUBS:
|
|
1677
|
+
text = text.replace(needle, repl)
|
|
1678
|
+
return text
|
|
1679
|
+
|
|
1680
|
+
|
|
1634
1681
|
def _scan_file(
|
|
1635
1682
|
source: str,
|
|
1636
1683
|
rel_path: str,
|
|
@@ -1822,6 +1869,15 @@ def run_migrate_check(
|
|
|
1822
1869
|
for f in all_findings:
|
|
1823
1870
|
f.code_context = _classify_code_context(f)
|
|
1824
1871
|
|
|
1872
|
+
# BUG #3 (v1.68.0): on a non-Spring repo, rewrite Spring-Boot-specific framing in
|
|
1873
|
+
# every finding's explanation into framework-neutral Jakarta-EE framing. The
|
|
1874
|
+
# finding itself (javax→jakarta) stays; only the prose is corrected so it matches
|
|
1875
|
+
# the structured spring_present=False signal in the same report.
|
|
1876
|
+
if not spring_present:
|
|
1877
|
+
for f in all_findings:
|
|
1878
|
+
if f.explanation:
|
|
1879
|
+
f.explanation = _neutralize_non_spring_explanation(f.explanation)
|
|
1880
|
+
|
|
1825
1881
|
# Hibernate 5→6 stratification (independent of min_severity — it is its own
|
|
1826
1882
|
# risk model, not a severity-filtered finding stream).
|
|
1827
1883
|
from sourcecode.hibernate_strat import analyze_hibernate
|
|
@@ -20,7 +20,7 @@ import subprocess
|
|
|
20
20
|
from collections import deque
|
|
21
21
|
from dataclasses import dataclass, field
|
|
22
22
|
from pathlib import Path
|
|
23
|
-
from typing import Any, Optional
|
|
23
|
+
from typing import Any, Iterable, Optional
|
|
24
24
|
|
|
25
25
|
from sourcecode.fqn_utils import normalize_owner_fqn as _normalize_owner_fqn
|
|
26
26
|
from sourcecode.path_filters import is_test_path as _is_test_path
|
|
@@ -4700,9 +4700,16 @@ def compute_blast_radius(
|
|
|
4700
4700
|
)
|
|
4701
4701
|
if is_mapper and fqn not in _seen_mapper_fqns:
|
|
4702
4702
|
_seen_mapper_fqns.add(fqn)
|
|
4703
|
+
# BUG #5 (v1.68.0): distinguish a CONFIRMED persistence mapper (a
|
|
4704
|
+
# @Repository/DAO role or a MyBatis @Mapper interface) from a class that
|
|
4705
|
+
# only matched the name pattern (e.g. AzToRegionMapper — an AWS topology
|
|
4706
|
+
# mapper with no data layer). Only confirmed entries may be described as
|
|
4707
|
+
# "persistence" in the narrative.
|
|
4708
|
+
_is_persistence = role in _MAPPER_ROLES or symbol_kind == "mapper_interface"
|
|
4703
4709
|
_mapper_entry: dict = {
|
|
4704
4710
|
"fqn": fqn,
|
|
4705
4711
|
"role": role or ("mapper" if symbol_kind == "mapper_interface" else "repository"),
|
|
4712
|
+
"mapper_kind": "persistence" if _is_persistence else "name_heuristic",
|
|
4706
4713
|
"source_file": node_dict.get("source_file") or "",
|
|
4707
4714
|
}
|
|
4708
4715
|
if canonical != fqn:
|
|
@@ -4841,18 +4848,33 @@ def compute_blast_radius(
|
|
|
4841
4848
|
_parts.append(f"{n_ep} endpoint{'s' if n_ep != 1 else ''} exposed")
|
|
4842
4849
|
if n_txn:
|
|
4843
4850
|
_parts.append(f"{n_txn} transactional boundary{'s' if n_txn != 1 else ''} touched")
|
|
4844
|
-
|
|
4845
|
-
|
|
4851
|
+
# BUG #5 (v1.68.0): only confirmed persistence mappers earn the word "persistence".
|
|
4852
|
+
# Name-heuristic matches (e.g. *Mapper utility classes with no data layer) are
|
|
4853
|
+
# surfaced as neutral "data-mapping class(es)" so the prose never invents a
|
|
4854
|
+
# persistence tier the repo does not have.
|
|
4855
|
+
_n_persist = sum(1 for m in mappers_affected if m.get("mapper_kind") == "persistence")
|
|
4856
|
+
_n_heur = n_mappers - _n_persist
|
|
4857
|
+
if _n_persist:
|
|
4858
|
+
_parts.append(f"{_n_persist} persistence path{'s' if _n_persist != 1 else ''} in blast cone")
|
|
4859
|
+
if _n_heur:
|
|
4860
|
+
_parts.append(f"{_n_heur} data-mapping class{'es' if _n_heur != 1 else ''} in blast cone")
|
|
4846
4861
|
if n_sec:
|
|
4847
4862
|
_parts.append(f"{n_sec} security-gated endpoint{'s' if n_sec != 1 else ''} affected")
|
|
4848
4863
|
if n_modules > 1:
|
|
4849
4864
|
_parts.append(f"impact crosses {n_modules} modules")
|
|
4850
4865
|
|
|
4866
|
+
# BUG #4 (v1.68.0): derive the DI-framework label ONCE from caller evidence and
|
|
4867
|
+
# reuse it for both the short explanation and the long via_interface_note, so the
|
|
4868
|
+
# two never disagree (the old code hardcoded "Spring/CDI" in one and "Spring/CDI/
|
|
4869
|
+
# Guice" in the other). When the actual framework is identifiable from the caller
|
|
4870
|
+
# FQNs (e.g. *.guice.* packages on a Guice repo), name it specifically instead of
|
|
4871
|
+
# a generic guess.
|
|
4872
|
+
_di_label = _detect_di_framework_label(direct_callers)
|
|
4851
4873
|
if _iface_bridging:
|
|
4852
4874
|
_iface_names = [b["interface"].split(".")[-1] for b in _iface_bridging]
|
|
4853
4875
|
_parts.append(
|
|
4854
4876
|
f"callers resolved via interface{'s' if len(_iface_names) > 1 else ''} "
|
|
4855
|
-
f"({', '.join(_iface_names)}) —
|
|
4877
|
+
f"({', '.join(_iface_names)}) — {_di_label} DI pattern"
|
|
4856
4878
|
)
|
|
4857
4879
|
|
|
4858
4880
|
# Transparency: hub-class BFS truncation must appear in explanation so the
|
|
@@ -4924,9 +4946,9 @@ def compute_blast_radius(
|
|
|
4924
4946
|
if _iface_bridging:
|
|
4925
4947
|
out["via_interface_resolution"] = _iface_bridging
|
|
4926
4948
|
out["via_interface_note"] = (
|
|
4927
|
-
"Target is a concrete class injected via interface(s) in
|
|
4928
|
-
"
|
|
4929
|
-
"interface(s) — these are the real production dependents."
|
|
4949
|
+
"Target is a concrete class injected via interface(s) in a "
|
|
4950
|
+
f"{_di_label} DI framework. direct_callers includes callers of the "
|
|
4951
|
+
"implemented interface(s) — these are the real production dependents."
|
|
4930
4952
|
)
|
|
4931
4953
|
if _bfs_truncated:
|
|
4932
4954
|
out["bfs_truncation_reason"] = "hub_class_depth_cap"
|
|
@@ -5106,3 +5128,31 @@ def _all_callers_from_rg(fqn: str, reverse_graph: dict[str, dict[str, list[str]]
|
|
|
5106
5128
|
def _simple_name(fqn: str) -> str:
|
|
5107
5129
|
"""Extract the simple class name from a fully-qualified name."""
|
|
5108
5130
|
return fqn.split(".")[-1].split("#")[0]
|
|
5131
|
+
|
|
5132
|
+
|
|
5133
|
+
def _detect_di_framework_label(caller_fqns: "Iterable[str]") -> str:
|
|
5134
|
+
"""Name the DI framework(s) evidenced by the caller FQNs (BUG #4, v1.68.0).
|
|
5135
|
+
|
|
5136
|
+
Returns a "/"-joined label of the frameworks actually identifiable from the
|
|
5137
|
+
callers' package names (e.g. "Guice" when callers live under a ``*.guice.*``
|
|
5138
|
+
package, "Spring" under ``org.springframework``). When nothing is identifiable
|
|
5139
|
+
the label degrades to the generic "Spring/CDI/Guice" rather than asserting a
|
|
5140
|
+
framework the evidence does not support. The same label feeds both the short
|
|
5141
|
+
explanation and the long via_interface_note so they cannot disagree.
|
|
5142
|
+
"""
|
|
5143
|
+
frameworks: set[str] = set()
|
|
5144
|
+
for fqn in caller_fqns:
|
|
5145
|
+
low = fqn.lower()
|
|
5146
|
+
if "guice" in low or "com.google.inject" in low:
|
|
5147
|
+
frameworks.add("Guice")
|
|
5148
|
+
if "springframework" in low or ".spring." in low:
|
|
5149
|
+
frameworks.add("Spring")
|
|
5150
|
+
if "jakarta.enterprise" in low or "javax.enterprise" in low or ".cdi." in low:
|
|
5151
|
+
frameworks.add("CDI")
|
|
5152
|
+
if "micronaut" in low:
|
|
5153
|
+
frameworks.add("Micronaut")
|
|
5154
|
+
if "dagger" in low:
|
|
5155
|
+
frameworks.add("Dagger")
|
|
5156
|
+
if frameworks:
|
|
5157
|
+
return "/".join(sorted(frameworks))
|
|
5158
|
+
return "Spring/CDI/Guice"
|
|
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
|