sourcecode 1.67.0__tar.gz → 1.69.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.
Files changed (124) hide show
  1. {sourcecode-1.67.0 → sourcecode-1.69.0}/CHANGELOG.md +105 -0
  2. {sourcecode-1.67.0 → sourcecode-1.69.0}/PKG-INFO +1 -1
  3. {sourcecode-1.67.0 → sourcecode-1.69.0}/pyproject.toml +1 -1
  4. sourcecode-1.69.0/scripts/generate_jdk_exports.py +132 -0
  5. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/__init__.py +1 -1
  6. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/classifier.py +121 -1
  7. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/cli.py +21 -0
  8. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/java.py +36 -1
  9. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/explain.py +83 -1
  10. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/integration_detector.py +109 -10
  11. sourcecode-1.69.0/src/sourcecode/jdk_exports.py +26 -0
  12. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/migrate_check.py +119 -6
  13. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/repository_ir.py +138 -9
  14. {sourcecode-1.67.0 → sourcecode-1.69.0}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-1.67.0 → sourcecode-1.69.0}/.gitignore +0 -0
  16. {sourcecode-1.67.0 → sourcecode-1.69.0}/.ruff.toml +0 -0
  17. {sourcecode-1.67.0 → sourcecode-1.69.0}/CONTRIBUTING.md +0 -0
  18. {sourcecode-1.67.0 → sourcecode-1.69.0}/LICENSE +0 -0
  19. {sourcecode-1.67.0 → sourcecode-1.69.0}/README.md +0 -0
  20. {sourcecode-1.67.0 → sourcecode-1.69.0}/SECURITY.md +0 -0
  21. {sourcecode-1.67.0 → sourcecode-1.69.0}/raw +0 -0
  22. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/adaptive_scanner.py +0 -0
  23. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/architecture_analyzer.py +0 -0
  24. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/architecture_summary.py +0 -0
  25. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/ast_extractor.py +0 -0
  26. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/cache.py +0 -0
  27. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/canonical_ir.py +0 -0
  28. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/cir_graphs.py +0 -0
  29. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  30. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/confidence_analyzer.py +0 -0
  31. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/context_scorer.py +0 -0
  32. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/context_summarizer.py +0 -0
  33. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/contract_model.py +0 -0
  34. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/contract_pipeline.py +0 -0
  35. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/coverage_parser.py +0 -0
  36. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/dependency_analyzer.py +0 -0
  37. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/__init__.py +0 -0
  38. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/base.py +0 -0
  39. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  40. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/dart.py +0 -0
  41. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/dotnet.py +0 -0
  42. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/elixir.py +0 -0
  43. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/go.py +0 -0
  44. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/heuristic.py +0 -0
  45. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/hybrid.py +0 -0
  46. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/nodejs.py +0 -0
  48. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/parsers.py +0 -0
  49. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/php.py +0 -0
  50. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/project.py +0 -0
  51. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/python.py +0 -0
  52. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/ruby.py +0 -0
  53. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/rust.py +0 -0
  54. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  59. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/env_analyzer.py +0 -0
  60. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/error_schema.py +0 -0
  61. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/file_chunker.py +0 -0
  62. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/file_classifier.py +0 -0
  63. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/flow_analyzer.py +0 -0
  64. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/format_contract.py +0 -0
  65. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/fqn_utils.py +0 -0
  66. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/git_analyzer.py +0 -0
  67. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/graph_analyzer.py +0 -0
  68. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/hibernate_strat.py +0 -0
  69. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/license.py +0 -0
  70. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/__init__.py +0 -0
  71. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  72. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  73. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  74. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  75. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  76. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  77. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/registry.py +0 -0
  78. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/runner.py +0 -0
  79. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp/server.py +0 -0
  80. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/mcp_nudge.py +0 -0
  81. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/metrics_analyzer.py +0 -0
  82. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/openapi_surface.py +0 -0
  83. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/output_budget.py +0 -0
  84. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/path_filters.py +0 -0
  85. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  86. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/pr_impact.py +0 -0
  87. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/prepare_context.py +0 -0
  88. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/progress.py +0 -0
  89. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/ranking_engine.py +0 -0
  90. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/redactor.py +0 -0
  91. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/relevance_scorer.py +0 -0
  92. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/rename_refactor.py +0 -0
  93. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/repo_classifier.py +0 -0
  94. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/ris.py +0 -0
  95. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/runtime_classifier.py +0 -0
  96. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/scanner.py +0 -0
  97. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/schema.py +0 -0
  98. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/security_config.py +0 -0
  99. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/semantic_analyzer.py +0 -0
  100. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/serializer.py +0 -0
  101. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_event_topology.py +0 -0
  102. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_findings.py +0 -0
  103. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_impact.py +0 -0
  104. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_model.py +0 -0
  105. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_security_audit.py +0 -0
  106. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_semantic.py +0 -0
  107. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  108. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/summarizer.py +0 -0
  109. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/telemetry/__init__.py +0 -0
  110. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/telemetry/config.py +0 -0
  111. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/telemetry/consent.py +0 -0
  112. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/telemetry/events.py +0 -0
  113. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/telemetry/filters.py +0 -0
  114. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/telemetry/transport.py +0 -0
  115. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/tree_utils.py +0 -0
  116. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/validation_surface.py +0 -0
  117. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/version_check.py +0 -0
  118. {sourcecode-1.67.0 → sourcecode-1.69.0}/src/sourcecode/workspace.py +0 -0
  119. {sourcecode-1.67.0 → sourcecode-1.69.0}/supabase/functions/README.md +0 -0
  120. {sourcecode-1.67.0 → sourcecode-1.69.0}/supabase/functions/get-license/index.ts +0 -0
  121. {sourcecode-1.67.0 → sourcecode-1.69.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  122. {sourcecode-1.67.0 → sourcecode-1.69.0}/supabase/functions/telemetry/index.ts +0 -0
  123. {sourcecode-1.67.0 → sourcecode-1.69.0}/supabase/sql/license_event_ordering.sql +0 -0
  124. {sourcecode-1.67.0 → sourcecode-1.69.0}/supabase/sql/telemetry_events.sql +0 -0
@@ -1,5 +1,110 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.69.0] — 2026-06-30
4
+
5
+ ### Fixed — framing & symbol-graph fidelity on a non-framework Java library (JobRunr field test)
6
+
7
+ A full architectural audit of **JobRunr** (Gradle multi-module, 707 classes, a
8
+ background-job-processing **library** that is framework-agnostic at its core and
9
+ integrates with Quarkus/Micronaut/Spring only through small optional adapter
10
+ modules) surfaced four reproducible defects. Each fix targets the **root cause** so
11
+ it generalizes to any equivalent repo, with new regression coverage across five
12
+ suites.
13
+
14
+ - **`migrate-check` MIG-011 — false positive on JDK-exported packages (BUG #1).**
15
+ The rule flagged *any* `sun.*` / `com.sun.*` import as a strongly-encapsulated
16
+ JDK internal on a pure prefix heuristic. On JobRunr this turned 14 benign
17
+ `com.sun.net.httpserver.*` imports into 14 `high`/`manual_migration` findings —
18
+ **100% of an inflated `blocking_count: 14`** on a healthy repo (the GO/NO-GO
19
+ metric). `com.sun.net.httpserver` is exported **unconditionally** by the
20
+ `jdk.httpserver` module (public since Java 6, basis of JEP 408) and needs no
21
+ `--add-exports`/`--add-opens`. MIG-011 now consults an allowlist of
22
+ unconditionally-exported JDK packages **generated from the running JDK** by
23
+ `scripts/generate_jdk_exports.py` (`src/sourcecode/jdk_exports.py`), never
24
+ hand-maintained. Genuinely-internal packages (`sun.misc.Unsafe`,
25
+ `com.sun.tools.*`, `com.sun.jdi.*`, `com.sun.source.*`) stay `high`.
26
+ `blocking_count` and `estimated_effort_days` recompute accordingly. JobRunr
27
+ `blocking_count` 14 → 0.
28
+ - **Phantom symbols extracted from prose (BUG #2).** `modernize`'s
29
+ `statically_unreferenced` / `framework_dispatched` lists (and the shared symbol
30
+ graph behind `repo-ir` / `impact` / `export --c4`) contained FQNs that match no
31
+ real type — e.g. `org.jobrunr.configuration.provides` (from the Javadoc "This
32
+ class **provides** the entry point") and `…details.instead` (from an exception
33
+ string "…**instead** of an actual implementation"). The non-annotated fast-path
34
+ symbol scan ran `(?:class|interface|enum)\s+(\w+)` over **raw source**, tokenizing
35
+ words inside comments and string literals. It now strips comments + string
36
+ literals first and requires a capitalized type name, matching the precision of the
37
+ annotated-file extractor. Fixing the shared parser fixes every downstream command.
38
+ - **`endpoints` — total false negative on imperative router DSLs (BUG #3).** JobRunr
39
+ exposes 12 live dashboard REST routes via `get("/jobs", handler)` /
40
+ `post(...)` / `delete(...)` calls; the annotation-only extractor returned **0
41
+ endpoints** (silently disabling `validation` downstream). A shape-based detector
42
+ now recognizes router-DSL registrations — an HTTP-verb method whose first argument
43
+ is a path literal **and** that has a second argument (the trailing comma
44
+ distinguishes a route from a 1-arg `Map.get`). Reported at `confidence: medium`
45
+ with `source: router_dsl`. Handles bare (Spark/static-import) and `app.get(...)`
46
+ (Javalin) forms. JobRunr `endpoints` 0 → 12.
47
+ - **Stack/architecture classification biased to the most-recognizable framework (BUG #4).**
48
+ JobRunr (19 modules, framework only in 3 small adapters, `core` ≈ 85% of code) was
49
+ reported as `project_type: "api"` / `frameworks: ["Quarkus"]`. Two root causes:
50
+ (a) the Gradle detector substring-matched `"quarkus"` inside an **exclusion filter**
51
+ (`configure(subprojects.findAll { !it.name.contains('quarkus') })`) — framework
52
+ tokens are now matched only inside genuine dependency/plugin lines; and (b)
53
+ presence-based typing — a framework confined to a minority adapter module (its
54
+ dominant source module uses no framework) no longer drives the project type, and a
55
+ multi-module JVM repo with no defining web/API framework classifies as `library`,
56
+ never `unknown`. A monolithic framework app is unaffected. `explain` no longer
57
+ emits the misleading "No stereotype detected — may be a plain class or utility" for
58
+ a structurally central non-annotated class: it infers a low-confidence stereotype
59
+ from in-degree, lifecycle methods, and naming (e.g. `BackgroundJobServer` →
60
+ "Likely server/orchestrator … lifecycle methods detected (start/stop)"). JobRunr
61
+ `project_type` "api" → "library", `frameworks` `["Quarkus"]` → `[]`.
62
+
63
+ ## [1.68.0] — 2026-06-30
64
+
65
+ ### Fixed — narrative/structured-data parity on non-Spring DI repos (Netflix Eureka field test)
66
+
67
+ A real audit of **Netflix Eureka** (Gradle multi-module, JAX-RS/Jersey 1.x + Google
68
+ Guice, **no Spring, no Hibernate, no persistence layer**) surfaced six reliability
69
+ defects that share one root cause: **prose that assumed a Spring/Hibernate/persistence
70
+ context even though the same JSON's own structured fields already knew it did not
71
+ apply.** A consumer trusting the narrative over the structured fields would have read
72
+ false conclusions. Guiding principle applied throughout: **a narrative term (Spring,
73
+ Guice, persistence, …) may only appear when the corresponding structured signal
74
+ confirms it.** New regression coverage across four suites.
75
+
76
+ - **SMTP false positive on a log string (BUG #1).** `export --integrations` reported
77
+ `kind=smtp` for `logger.warn("Transport initialization failure", e)` — the bare word
78
+ "Transport" in a log literal. The detector now (a) skips any token matched inside a
79
+ string literal and (b) gates the JavaMail tokens (`Transport`, `MimeMessage`) on a
80
+ `javax.mail`/`jakarta.mail`/`org.springframework.mail` import. Eureka SMTP hits 3 → 0.
81
+ - **JNDI DNS mislabeled as LDAP (BUG #2).** `javax.naming.directory.{Dir,Initial}Context`
82
+ is protocol-agnostic; it is now classified by its `INITIAL_CONTEXT_FACTORY` —
83
+ `DnsContextFactory` → `kind=dns` (`client=jndi-dns`), `LdapCtxFactory` → `ldap`, and an
84
+ unresolvable factory → `kind=naming-directory-unknown` with `confidence=low`, never an
85
+ assumed LDAP. Eureka `DnsResolver` now reports `dns`.
86
+ - **"Spring Boot 3" narrative on a non-Spring repo (BUG #3).** When `spring_present` is
87
+ `False`, every `migrate-check` finding's `explanation` is rewritten from Spring-Boot
88
+ framing into framework-neutral Jakarta-EE / servlet-container framing (the javax→jakarta
89
+ finding itself is unchanged). Guarantee: no explanation contains "Spring Boot" unless
90
+ the report confirms runtime Spring.
91
+ - **Inconsistent DI-framework label (BUG #4).** `impact` emitted "Spring/CDI DI pattern"
92
+ in the short `explanation` but "Spring/CDI/Guice" in `via_interface_note` of the *same*
93
+ JSON, while `direct_callers` proved the framework was Guice. Both strings now derive
94
+ from one label computed from caller evidence — a `*.guice.*` caller yields "Guice".
95
+ - **"persistence paths" for non-persistence mappers (BUG #5).** Eureka's `AzToRegionMapper`
96
+ classes (AWS topology, no data layer) were counted as "4 persistence paths". Each
97
+ `mappers_affected` entry now carries `mapper_kind` (`persistence` only for a confirmed
98
+ `@Repository` role or MyBatis `mapper_interface`; otherwise `name_heuristic`). The
99
+ explanation says "persistence path" only for confirmed entries and neutral
100
+ "data-mapping class" otherwise — the word "persistence" never appears for a repo with no
101
+ confirmed persistence signal.
102
+ - **Ambiguous `member_count` unit in `modernize` (BUG #6).** `subsystem_summary.member_count`
103
+ counts all graph members (classes + methods + fields), ~5× the class count and not
104
+ comparable to repo-level `total_classes`. Each entry now also carries an unambiguous
105
+ `class_count` (class/interface declarations only, comparable to `total_classes`), with a
106
+ `subsystem_summary_note` documenting both units.
107
+
3
108
  ## [1.67.0] — 2026-06-30
4
109
 
5
110
  ### Fixed — framing accuracy on non-Spring repos (Apache OFBiz field test)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.67.0
3
+ Version: 1.69.0
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  License-File: LICENSE
6
6
  Keywords: agents,ai,codebase,context,developer-tools,llm
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.67.0"
7
+ version = "1.69.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env python3
2
+ """Generate the JDK unconditional-export allowlist used by migrate-check MIG-011.
3
+
4
+ MIG-011 flags `sun.*` / `com.sun.*` imports as strongly-encapsulated JDK-internal
5
+ APIs. That heuristic is *prefix-based* and produces false positives on packages
6
+ that merely share the prefix but are in fact exported **unconditionally** by a
7
+ JDK module (no `to` clause) and therefore require NO `--add-exports` /
8
+ `--add-opens` on any classpath or module-path scenario. The canonical example is
9
+ `com.sun.net.httpserver.*` (module `jdk.httpserver`) — public and documented
10
+ since Java 6, the basis of JEP 408.
11
+
12
+ This script derives the allowlist directly from the running JDK by parsing
13
+ `java --describe-module` for every module, so the data tracks the real platform
14
+ instead of a hand-maintained list that silently rots across releases.
15
+
16
+ Two filters are applied:
17
+
18
+ 1. Only `exports <pkg>` lines WITHOUT a `to <module>` qualifier are kept
19
+ (qualified exports still need `--add-exports` for arbitrary code).
20
+
21
+ 2. Modules whose whole purpose is internal / tooling / deprecated access are
22
+ DENYLISTED — their `sun.*` / `com.sun.*` exports are genuine migration
23
+ concerns and must keep MIG-011's `high` severity:
24
+ - jdk.unsupported → sun.misc.Unsafe, sun.reflect (deprecated for removal)
25
+ - jdk.compiler → com.sun.source.*, com.sun.tools.javac (compiler internals)
26
+ - jdk.attach → com.sun.tools.attach.* (tooling)
27
+ - jdk.jdi → com.sun.jdi.* (debugger internals)
28
+ - jdk.jconsole / jdk.jdeps / jdk.javadoc / jdk.jshell → com.sun.tools.*
29
+
30
+ Run with the OLDEST supported target JDK to stay conservative (a package
31
+ exported in JDK 21 but not JDK 17 must not be allowlisted for a JDK 17 target).
32
+ Re-run after a JDK bump and commit the regenerated module.
33
+
34
+ Usage:
35
+ python scripts/generate_jdk_exports.py > src/sourcecode/jdk_exports.py
36
+ """
37
+ from __future__ import annotations
38
+
39
+ import re
40
+ import subprocess
41
+ import sys
42
+
43
+ # Modules whose sun.*/com.sun.* exports are genuinely internal/tooling/deprecated.
44
+ # Their packages must NOT be allowlisted — MIG-011 keeps firing `high` for them.
45
+ _DENYLIST_MODULES: frozenset[str] = frozenset(
46
+ {
47
+ "jdk.unsupported",
48
+ "jdk.compiler",
49
+ "jdk.attach",
50
+ "jdk.jdi",
51
+ "jdk.jconsole",
52
+ "jdk.jdeps",
53
+ "jdk.javadoc",
54
+ "jdk.jshell",
55
+ "jdk.internal.ed",
56
+ "jdk.internal.le",
57
+ "jdk.internal.opt",
58
+ "jdk.internal.vm.ci",
59
+ "jdk.internal.vm.compiler",
60
+ }
61
+ )
62
+
63
+ _EXPORT_RE = re.compile(r"^exports\s+(\S+?)(?:\s+to\s+.*)?$")
64
+
65
+
66
+ def _list_modules() -> list[str]:
67
+ out = subprocess.run(
68
+ ["java", "--list-modules"], capture_output=True, text=True, check=True
69
+ ).stdout
70
+ return [line.split("@", 1)[0].strip() for line in out.splitlines() if line.strip()]
71
+
72
+
73
+ def _module_exports(module: str) -> list[tuple[str, bool]]:
74
+ """Return (package, is_unconditional) for each `exports` line of a module."""
75
+ out = subprocess.run(
76
+ ["java", "--describe-module", module],
77
+ capture_output=True,
78
+ text=True,
79
+ ).stdout
80
+ results: list[tuple[str, bool]] = []
81
+ for line in out.splitlines():
82
+ line = line.strip()
83
+ m = _EXPORT_RE.match(line)
84
+ if not m:
85
+ continue
86
+ pkg = m.group(1)
87
+ unconditional = " to " not in line
88
+ results.append((pkg, unconditional))
89
+ return results
90
+
91
+
92
+ def main() -> int:
93
+ java_version = subprocess.run(
94
+ ["java", "-version"], capture_output=True, text=True
95
+ ).stderr.splitlines()[0]
96
+
97
+ allowlist: set[str] = set()
98
+ for module in _list_modules():
99
+ if module in _DENYLIST_MODULES:
100
+ continue
101
+ for pkg, unconditional in _module_exports(module):
102
+ if not unconditional:
103
+ continue
104
+ if pkg.startswith("sun.") or pkg.startswith("com.sun."):
105
+ allowlist.add(pkg)
106
+
107
+ packages = sorted(allowlist)
108
+
109
+ print('"""GENERATED FILE — do not edit by hand.')
110
+ print()
111
+ print("Allowlist of sun.* / com.sun.* packages exported UNCONDITIONALLY by the")
112
+ print("JDK (no `--add-exports` / `--add-opens` required on classpath or module")
113
+ print("path). Consumed by migrate-check MIG-011 to suppress false positives.")
114
+ print()
115
+ print(f"Regenerate with: python scripts/generate_jdk_exports.py > "
116
+ "src/sourcecode/jdk_exports.py")
117
+ print(f"Generated from: {java_version}")
118
+ print('"""')
119
+ print("from __future__ import annotations")
120
+ print()
121
+ print()
122
+ print("JDK_UNCONDITIONAL_EXPORTS: frozenset[str] = frozenset(")
123
+ print(" {")
124
+ for pkg in packages:
125
+ print(f' "{pkg}",')
126
+ print(" }")
127
+ print(")")
128
+ return 0
129
+
130
+
131
+ if __name__ == "__main__":
132
+ sys.exit(main())
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.67.0"
3
+ __version__ = "1.69.0"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from collections.abc import Iterable, Sequence
4
5
  from dataclasses import replace
5
6
  from typing import Any, Literal
@@ -104,6 +105,21 @@ class TypeClassifier:
104
105
  stack_names = {stack.stack for stack in stacks}
105
106
  framework_names = {framework.name for stack in stacks for framework in stack.frameworks}
106
107
 
108
+ # BUG #4 (JobRunr field test): a framework present only in a small optional
109
+ # integration/adapter submodule must NOT label the whole repo as that
110
+ # framework's app type. JobRunr is a framework-agnostic background-job
111
+ # LIBRARY whose `core` module holds ~85% of the code; Quarkus/Micronaut/Spring
112
+ # appear only in tiny per-framework adapter modules — yet presence-based
113
+ # classification returned project_type="api"+Quarkus. We weight by code
114
+ # locality: if the DOMINANT source module (the one with the most source files)
115
+ # contains no evidence of the app-defining framework, the framework is an
116
+ # optional adapter and the repo is a library. A monolithic Spring app is
117
+ # unaffected — its dominant module *does* use the framework. Drop such
118
+ # localized frameworks from the set that drives the app-type decision below.
119
+ _app_frameworks = framework_names & (_WEB_FRAMEWORKS | _API_FRAMEWORKS)
120
+ _localized = self._localized_adapter_frameworks(file_tree, stacks, _app_frameworks)
121
+ framework_names = framework_names - _localized
122
+
107
123
  if len(stack_names) >= 2 and self._is_fullstack(stacks):
108
124
  return "fullstack"
109
125
 
@@ -129,9 +145,16 @@ class TypeClassifier:
129
145
  if framework_names & _API_FRAMEWORKS:
130
146
  return "api"
131
147
 
148
+ # All app-defining frameworks were localized to optional adapter submodules
149
+ # (multi-module library with per-framework integrations) — report library,
150
+ # never "unknown", when there is clearly source code present.
151
+ if _localized and not (framework_names & (_WEB_FRAMEWORKS | _API_FRAMEWORKS)):
152
+ return "library"
153
+
154
+ # Strong CLI signals: a CLI framework or an explicit cli entry point.
132
155
  if framework_names & _CLI_FRAMEWORKS or any(
133
156
  entry.kind == "cli" for entry in entry_points
134
- ) or any(path.startswith("bin/") for path in flat_paths):
157
+ ):
135
158
  return "cli"
136
159
 
137
160
  if stack_names:
@@ -141,8 +164,105 @@ class TypeClassifier:
141
164
  if single in {"cpp", "dotnet"} and any(entry.kind == "cli" for entry in entry_points):
142
165
  return "cli"
143
166
 
167
+ # BUG #4 (JobRunr field test): a multi-module JVM repo with no app-defining
168
+ # web/API framework is a library/toolkit, not an "unknown" — never let the
169
+ # first command of an audit emit a vacuous classification for a clearly
170
+ # structured codebase (e.g. JobRunr: core + per-framework adapter modules).
171
+ # This is checked BEFORE the weak `bin/`-directory CLI heuristic so a build
172
+ # output / wrapper `bin/` dir does not mislabel a library as a CLI.
173
+ if stack_names & {"java", "kotlin", "scala"} and self._is_multi_module(file_tree):
174
+ return "library"
175
+
176
+ # Weak CLI heuristic: a top-level bin/ directory (only when nothing stronger).
177
+ if any(path.startswith("bin/") for path in flat_paths):
178
+ return "cli"
179
+
144
180
  return "unknown" if stacks else None
145
181
 
182
+ def _is_multi_module(self, file_tree: dict[str, Any]) -> bool:
183
+ """True when the repo has >1 source module (distinct `*/src/...` roots)."""
184
+ _CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
185
+ modules = {
186
+ self._module_of(p)
187
+ for p in flatten_file_tree(file_tree)
188
+ if p.endswith(_CODE_EXTS)
189
+ }
190
+ modules.discard("")
191
+ return len(modules) >= 2
192
+
193
+ @staticmethod
194
+ def _module_of(path: str) -> str:
195
+ """Group a source path into its module root.
196
+
197
+ For Maven/Gradle layouts the module is everything before `/src/`
198
+ (e.g. `framework-support/jobrunr-quarkus/src/main/java/...` →
199
+ `framework-support/jobrunr-quarkus`). Otherwise the top-level directory.
200
+ """
201
+ norm = path.replace("\\", "/")
202
+ idx = norm.find("/src/")
203
+ if idx > 0:
204
+ return norm[:idx]
205
+ head, _, tail = norm.partition("/")
206
+ return head if tail else ""
207
+
208
+ _EVIDENCE_PATH_RE = re.compile(r"\(([^()]+)\)\s*$")
209
+
210
+ def _localized_adapter_frameworks(
211
+ self,
212
+ file_tree: dict[str, Any],
213
+ stacks: Sequence[StackDetection],
214
+ candidate_frameworks: set[str],
215
+ ) -> set[str]:
216
+ """Frameworks confined to a minority module while a framework-agnostic
217
+ module dominates the codebase (library + per-framework adapters).
218
+
219
+ Returns the subset of ``candidate_frameworks`` that should NOT drive the
220
+ project-type decision. A framework qualifies only when (a) the repo is
221
+ multi-module, (b) the framework's evidence files are all outside the
222
+ dominant source module, and (c) the framework has located evidence files
223
+ (a manifest-only/root detection applies repo-wide and never localizes).
224
+ """
225
+ if not candidate_frameworks:
226
+ return set()
227
+
228
+ _CODE_EXTS = (".java", ".kt", ".kts", ".scala", ".groovy")
229
+ module_file_counts: dict[str, int] = {}
230
+ for p in flatten_file_tree(file_tree):
231
+ if not p.endswith(_CODE_EXTS):
232
+ continue
233
+ mod = self._module_of(p)
234
+ module_file_counts[mod] = module_file_counts.get(mod, 0) + 1
235
+
236
+ # Need a genuine multi-module repo to reason about locality.
237
+ if len(module_file_counts) < 2:
238
+ return set()
239
+ dominant_module = max(module_file_counts, key=lambda m: module_file_counts[m])
240
+
241
+ # Collect evidence file paths per framework from detected_via.
242
+ evidence: dict[str, set[str]] = {}
243
+ for stack in stacks:
244
+ for fw in stack.frameworks:
245
+ if fw.name not in candidate_frameworks:
246
+ continue
247
+ paths = evidence.setdefault(fw.name, set())
248
+ for ev in fw.detected_via:
249
+ if ev.startswith("manifest:"):
250
+ continue
251
+ m = self._EVIDENCE_PATH_RE.search(ev)
252
+ if m:
253
+ paths.add(m.group(1).strip())
254
+
255
+ localized: set[str] = set()
256
+ for fw_name in candidate_frameworks:
257
+ files = evidence.get(fw_name) or set()
258
+ if not files:
259
+ # No locatable evidence (manifest-only) → applies repo-wide.
260
+ continue
261
+ modules = {self._module_of(f) for f in files}
262
+ if dominant_module not in modules:
263
+ localized.add(fw_name)
264
+ return localized
265
+
146
266
  def _is_fullstack(self, stacks: Sequence[StackDetection]) -> bool:
147
267
  has_web = False
148
268
  has_api = False
@@ -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
@@ -90,6 +90,19 @@ _GRADLE_JAVA_VERSION_RE = re.compile(
90
90
  _GRADLE_JAVA_ENUM_RE = re.compile(
91
91
  r"""(?:sourceCompatibility|targetCompatibility)\s*=\s*JavaVersion\.VERSION_(\d+)"""
92
92
  )
93
+ # BUG #4: a gradle line that genuinely declares a dependency or plugin. Matches a
94
+ # quoted Maven coordinate (group:artifact, requires the colon), a plugins-block
95
+ # `id '...'`, an `apply plugin:`, or a `classpath`/`platform(`/`enforcedPlatform(`
96
+ # declaration. Deliberately does NOT match `it.name.contains('quarkus')`-style
97
+ # subproject filters (a bare quoted name with no colon and no plugin/dep keyword).
98
+ _GRADLE_DEP_LINE_RE = re.compile(
99
+ r"""['"][\w.\-]+:[\w.\-]+""" # maven coordinate "group:artifact..."
100
+ r"""|\bclasspath\b"""
101
+ r"""|\b(?:enforced)?platform\s*\("""
102
+ r"""|\bapply\s+plugin\s*:"""
103
+ r"""|\bid\s*[('"]""",
104
+ re.IGNORECASE,
105
+ )
93
106
 
94
107
 
95
108
  class JavaDetector(AbstractDetector):
@@ -279,7 +292,29 @@ class JavaDetector(AbstractDetector):
279
292
  original = "\n".join(read_text_lines(path))
280
293
  content = original.lower()
281
294
  sb_version = self._extract_gradle_sb_version(original)
282
- return self._detect_jvm_frameworks(content, "build.gradle", sb_version=sb_version)
295
+ # BUG #4 (JobRunr field test): framework tokens must be matched only inside
296
+ # genuine dependency / plugin declarations, never anywhere in the file. A
297
+ # multi-module root build.gradle that EXCLUDES a subproject by name —
298
+ # `configure(subprojects.findAll { !it.name.contains('quarkus') })` — must
299
+ # not be read as "this project uses Quarkus". Restrict the scan to lines
300
+ # carrying a Maven coordinate / plugin id; the exclusion filter has neither.
301
+ dep_text = self._gradle_dependency_text(content)
302
+ return self._detect_jvm_frameworks(dep_text, "build.gradle", sb_version=sb_version)
303
+
304
+ @staticmethod
305
+ def _gradle_dependency_text(content: str) -> str:
306
+ """Keep only gradle lines that declare a dependency or plugin.
307
+
308
+ A real dependency carries a Maven coordinate (`group:artifact`...) and a
309
+ plugin carries an `id`/`plugin`/`classpath` token. Subproject-name filters
310
+ and arbitrary prose are dropped, so a framework substring there cannot
311
+ manufacture a phantom framework detection.
312
+ """
313
+ kept: list[str] = []
314
+ for line in content.splitlines():
315
+ if _GRADLE_DEP_LINE_RE.search(line):
316
+ kept.append(line)
317
+ return "\n".join(kept)
283
318
 
284
319
  def _extract_gradle_sb_version(self, content: str) -> str | None:
285
320
  m = _GRADLE_SB_PLUGIN_RE.search(content)
@@ -223,7 +223,89 @@ def _build_purpose(
223
223
  if role_anns:
224
224
  desc = f"{role_anns[0]} bean"
225
225
 
226
- return desc or "No stereotype detected — may be a plain class or utility."
226
+ if desc:
227
+ return desc
228
+
229
+ # BUG #4 (JobRunr field test): with no recognized framework annotation, the old
230
+ # fallback returned "No stereotype detected — may be a plain class or utility",
231
+ # which is actively misleading for a central domain class. Libraries and clean/
232
+ # hexagonal architectures model rich roles WITHOUT DI annotations. Infer a
233
+ # low-confidence structural role from signals the tool already computes:
234
+ # in-degree (coupling), lifecycle methods, and naming convention.
235
+ structural = _structural_purpose(class_fqn, raw_nodes, cir)
236
+ if structural:
237
+ return structural
238
+ return "No stereotype detected — may be a plain class or utility."
239
+
240
+
241
+ # Lifecycle method names that signal an orchestrator/component managing state.
242
+ _LIFECYCLE_METHODS = frozenset({
243
+ "start", "stop", "init", "initialize", "shutdown", "close",
244
+ "pause", "resume", "run", "destroy", "open", "restart",
245
+ })
246
+
247
+ # Class-name suffix → inferred role (no annotation required).
248
+ _NAME_ROLE_SUFFIXES: tuple[tuple[str, str], ...] = (
249
+ ("Server", "server/orchestrator"),
250
+ ("Manager", "manager/coordinator"),
251
+ ("Controller", "controller"),
252
+ ("Service", "service"),
253
+ ("Repository", "repository/data access"),
254
+ ("Factory", "factory"),
255
+ ("Builder", "builder"),
256
+ ("Handler", "handler"),
257
+ ("Listener", "listener"),
258
+ ("Provider", "provider"),
259
+ ("Registry", "registry"),
260
+ ("Scheduler", "scheduler"),
261
+ ("Dispatcher", "dispatcher"),
262
+ ("Processor", "processor"),
263
+ ("Filter", "filter"),
264
+ ("Interceptor", "interceptor"),
265
+ )
266
+
267
+
268
+ def _structural_purpose(
269
+ class_fqn: str,
270
+ raw_nodes: list[dict],
271
+ cir: "CanonicalRepositoryIR",
272
+ ) -> str:
273
+ """Infer a low-confidence stereotype from structural signals (no annotations)."""
274
+ try:
275
+ in_degree = len(_build_callers(class_fqn, cir))
276
+ except Exception:
277
+ in_degree = 0
278
+
279
+ try:
280
+ method_names = {m.split("(")[0].lower() for m in _build_public_methods(class_fqn, raw_nodes)}
281
+ except Exception:
282
+ method_names = set()
283
+ lifecycle = sorted(method_names & _LIFECYCLE_METHODS)
284
+
285
+ simple = _simple(class_fqn)
286
+ role = ""
287
+ for suffix, label in _NAME_ROLE_SUFFIXES:
288
+ if simple.endswith(suffix):
289
+ role = label
290
+ break
291
+ if not role and lifecycle:
292
+ role = "orchestrator/lifecycle component"
293
+
294
+ # Require at least one real signal — otherwise stay honestly silent.
295
+ if not role and in_degree < 3:
296
+ return ""
297
+
298
+ head = f"Likely {role} (no DI annotations found)" if role else \
299
+ "Likely a structurally significant class (no DI annotations found)"
300
+ signals: list[str] = []
301
+ if in_degree >= 3:
302
+ signals.append(f"high in-degree ({in_degree})")
303
+ elif in_degree:
304
+ signals.append(f"in-degree {in_degree}")
305
+ if lifecycle:
306
+ signals.append(f"lifecycle methods detected ({'/'.join(lifecycle)})")
307
+ suffix = f" — {', '.join(signals)}" if signals else ""
308
+ return f"{head}{suffix} — inferred from structure, not annotations (low confidence)."
227
309
 
228
310
 
229
311
  def _build_public_methods(class_fqn: str, raw_nodes: list[dict]) -> list[str]: