sourcecode 5.9.18__py3-none-any.whl → 5.9.22__py3-none-any.whl

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.

Potentially problematic release.


This version of sourcecode might be problematic. Click here for more details.

sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "5.9.18"
7
+ __version__ = "5.9.22"
@@ -1,5 +1,5 @@
1
1
  """Generated at build time by hatch_build.py. Do not edit (AUD-592-D04)."""
2
2
 
3
- BUILD_COMMIT = 'e60fde756124d84699d9c2112b653f3e832e3717'
3
+ BUILD_COMMIT = '64c8b3e429eadd7a12d34eb1e5eefa0c08e430e4'
4
4
  CLOSURE_PROVENANCE = {'AUD-611-B05': {'commit': '', 'status': 'uncited'}, 'AUD-611-B06': {'commit': '', 'status': 'uncited'}, 'AUD-611-B07': {'commit': '', 'status': 'uncited'}, 'AUD-611-B08': {'commit': '', 'status': 'uncited'}, 'AUD-611-B09': {'commit': '', 'status': 'uncited'}, 'AUD-611-B10': {'commit': '', 'status': 'uncited'}, 'AUD-611-B11': {'commit': '', 'status': 'uncited'}, 'AUD-612-07': {'commit': '', 'status': 'uncited'}, 'AUD-612-09': {'commit': '', 'status': 'uncited'}, 'AUD-612-14': {'commit': '', 'status': 'uncited'}, 'AUD-612-B03': {'commit': '', 'status': 'uncited'}, 'AUD-612-B07': {'commit': '', 'status': 'uncited'}, 'AUD-612-B13': {'commit': '', 'status': 'uncited'}, 'AUD-612-E': {'commit': '', 'status': 'uncited'}, 'AUD-612-G': {'commit': '', 'status': 'uncited'}, 'AUD-612-I': {'commit': '', 'status': 'uncited'}, 'AUD-612-K': {'commit': '', 'status': 'uncited'}, 'AUD-612-M': {'commit': '', 'status': 'uncited'}, 'AUD-612-R': {'commit': '', 'status': 'uncited'}, 'AUD-613-ROOT': {'commit': '', 'status': 'uncited'}, 'AUD-614-07': {'commit': '', 'status': 'uncited'}, 'AUD-614-E': {'commit': '', 'status': 'uncited'}, 'AUD-614-F': {'commit': '', 'status': 'uncited'}, 'AUD-614-G': {'commit': '', 'status': 'uncited'}, 'AUD-614-I': {'commit': '', 'status': 'uncited'}, 'AUD-614-INTENT-TIE': {'commit': '', 'status': 'uncited'}, 'AUD-614-JUNIT4': {'commit': '', 'status': 'uncited'}, 'AUD-614-RISK-L0': {'commit': '', 'status': 'uncited'}, 'AUD-614-SELFTEST-TIMEOUT': {'commit': '', 'status': 'uncited'}, 'AUD-615-07': {'commit': '', 'status': 'uncited'}, 'AUD-615-COLD-COMPACT': {'commit': '', 'status': 'uncited'}, 'AUD-615-DRIFT': {'commit': '', 'status': 'uncited'}, 'AUD-615-E': {'commit': '', 'status': 'uncited'}, 'AUD-615-F': {'commit': '', 'status': 'uncited'}, 'AUD-615-INTENT-TIE': {'commit': '', 'status': 'uncited'}, 'AUD-615-JUNIT4': {'commit': '', 'status': 'uncited'}, 'AUD-615-RISK-L0': {'commit': '', 'status': 'uncited'}, 'AUD-615-SELFTEST-TIMEOUT': {'commit': '', 'status': 'uncited'}, 'AUD-616-E-PROV': {'commit': '', 'status': 'uncited'}, 'AUD-616-F, -RISK-L0, -SELFTEST-TIMEOUT, -INTENT-TIE, -DRIFT, -COLD-COMPACT, -E, -07': {'commit': '', 'status': 'uncited'}, 'AUD-616-INTENT-TIE-COV': {'commit': '', 'status': 'uncited'}, 'AUD-616-JUNIT4-SELF': {'commit': '', 'status': 'uncited'}, 'AUD-616-KOTLIN': {'commit': '', 'status': 'uncited'}, 'AUD-616-O': {'commit': '', 'status': 'uncited'}, 'AUD-616-P': {'commit': '', 'status': 'uncited'}, 'Window': {'commit': '', 'status': 'uncited'}, '`ASK-FIELD-01`': {'commit': '829f9fa', 'status': 'present'}, '`ASK-FIELD-02`': {'commit': '6a659fa', 'status': 'present'}, '`ASK-FIELD-03`': {'commit': '55b0d51', 'status': 'present'}, '`AUD-588-F05`': {'commit': '470e4a4', 'status': 'present'}, '`AUD-590-B01` / `B-01`, `F-01`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B02` / `B-02`, `F-02`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B03` / `B-03`, `F-03`': {'commit': '', 'status': 'uncited'}, '`AUD-590-B04` / `B-04`, `F-04`': {'commit': '', 'status': 'uncited'}, '`AUD-590-R01` / `R-01`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A01` / `A-1`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A02` / `A-2`, `A-4`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A03` / `A-3`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A05` / `A-5`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A06` / `A-6`, `A-7`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A08` / `A-8`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A09` / `A-9`': {'commit': '', 'status': 'uncited'}, '`AUD-591-A10` / `A-10`': {'commit': '', 'status': 'uncited'}, '`AUD-592-A02` / `A-1` second half, `§12.5`': {'commit': '63f3d61', 'status': 'present'}, '`AUD-592-B01` / `B-8`': {'commit': '8329614', 'status': 'present'}, '`AUD-592-D01` / `D-1`': {'commit': '0f8e03f', 'status': 'present'}, '`AUD-592-D02` / `D-2`': {'commit': '1d3db4d', 'status': 'present'}, '`AUD-592-D03` / `D-3`': {'commit': 'f28fe40', 'status': 'present'}, '`AUD-592-R01` / `A-1`': {'commit': '216025f', 'status': 'present'}, '`AUD-592-R02` / `N-4`, reopens `AUD-513-N04`': {'commit': 'ec3050d', 'status': 'present'}, '`AUD-593-N01` / `N-01`': {'commit': 'dd6a706', 'status': 'present'}, '`AUD-593-N02` / `N-02`': {'commit': 'f539787', 'status': 'present'}, '`AUD-593-N03` / `N-03`': {'commit': '1dd0b1c', 'status': 'present'}, '`AUD-593-N05` / `N-05`': {'commit': 'aa89388', 'status': 'present'}, '`AUD-593-N06` / `N-06`': {'commit': 'aa584a6', 'status': 'present'}, '`AUD-594-N04` / `D-5`, disclosure half of `B-3`': {'commit': 'a0060b7', 'status': 'present'}, '`AUD-594-X01` / `A-1`, residual of `AUD-593-N03`': {'commit': 'dd222b9', 'status': 'present'}, '`AUD-594-X02` / `A-1` second half': {'commit': '1193937', 'status': 'present'}, '`AUD-594-X03` / `X-03`, residual of `AUD-593-N01`': {'commit': 'e89e8f0', 'status': 'present'}, '`AUD-594-X04`': {'commit': '05b8cc9', 'status': 'present'}, '`AUD-595-A02` / `N-5`, advisory half of `AUD-588-B11`': {'commit': '3e15227', 'status': 'present'}, '`AUD-595-A03` / `B-6` exit-code half': {'commit': 'ab7285f', 'status': 'present'}, '`AUD-595-A05`': {'commit': 'e0b3d10', 'status': 'present'}, '`AUD-595-A07`, narrow half of `AUD-592-A02`': {'commit': 'cc15177', 'status': 'present'}, '`AUD-595-B01` / §B': {'commit': 'b77a1ad', 'status': 'present'}, '`AUD-595-Q01`': {'commit': '91a6566', 'status': 'present'}, '`AUD-596-A01` / `ASK-DET-001`': {'commit': 'fa508b4', 'status': 'present'}, '`AUD-596-A02` / `ASK-SELF-001`': {'commit': '4dfadc8', 'status': 'present'}, '`AUD-596-A03` / `ASK-C1-001`': {'commit': 'bffa3a6', 'status': 'present'}, '`AUD-596-A04` / `ASK-UX-003` + `ASK-UX-004`': {'commit': 'f0aac3c', 'status': 'present'}, '`AUD-596-B01` / `B-01`, harder witness for `BUG-6`': {'commit': '567d09d', 'status': 'present'}, '`AUD-596-B02` / `B-02`, class residual of `R2`': {'commit': '', 'status': 'uncited'}, '`AUD-596-B03` / `B-03`': {'commit': 'a8cb814', 'status': 'present'}, '`AUD-596-B06` / `B-06`': {'commit': '8107927', 'status': 'present'}, '`AUD-596-B07` / `B-07`, class residual of `AUD-592-R02`': {'commit': 'e060485', 'status': 'present'}, '`AUD-596-B09` / `B-09`': {'commit': '5b8146c', 'status': 'present'}, '`AUD-596-B10` / `B-10`': {'commit': '5cb994d', 'status': 'present'}, '`AUD-596-B16` / `B-16`': {'commit': '1507f69', 'status': 'present'}, '`AUD-596-D01` / `B-13` + `B-14`': {'commit': '0b1bdf0', 'status': 'present'}, '`AUD-596-D02` / `B-08`, residual of `AUD-594-N04`': {'commit': 'e7f73c3', 'status': 'present'}, '`AUD-596-D03` / `ASK-AGT-001`': {'commit': '8c46967', 'status': 'present'}, '`AUD-596-D04` / `B-18`': {'commit': '040f027', 'status': 'present'}, '`AUD-596-D05` / `B-19`': {'commit': 'fd2bf68', 'status': 'present'}, '`AUD-596-D06` / `B-20`': {'commit': '784cd83', 'status': 'present'}, '`AUD-596-D07` / `B-17`, half refuted': {'commit': 'c3ff822', 'status': 'present'}, '`AUD-596-D08` / `B-22`, class residual of `AUD-591-A06`': {'commit': 'b396c6a', 'status': 'present'}, '`AUD-596-D09` / `B-23`, surviving half of `B26`': {'commit': 'fd3d63f', 'status': 'present'}, '`AUD-596-D10` / `B-29`': {'commit': '', 'status': 'uncited'}, '`AUD-596-D11` / `ASK-UX-005`': {'commit': 'f4b4f77', 'status': 'present'}, '`AUD-596-D12` — six hints that send the reader back to the error': {'commit': '498df44', 'status': 'present'}, '`AUD-596-D13` / `B-28`': {'commit': '1014c72', 'status': 'present'}, '`AUD-596-D14` / `B-15`, narrow half of `B20`': {'commit': '10f2827', 'status': 'present'}, '`AUD-596-R01` / `B-05`': {'commit': '', 'status': 'uncited'}, '`AUD-596-X01` / `ASK-GATE-001`, gating half of `B24`': {'commit': 'e59c655', 'status': 'present'}, '`AUD-596-X02` / `ASK-UX-001` + `ASK-UX-002` + `B-21`': {'commit': 'f6ebf50', 'status': 'present'}, '`AUD-597-A01` / `ASK-UX-001` + `N-01`': {'commit': 'f8cf061', 'status': 'present'}, '`AUD-597-A02` / `ASK-UX-008` + `ASK-DOC-002`': {'commit': '9d47a4c', 'status': 'present'}, '`AUD-597-A03` / `ASK-DET-001`': {'commit': '4c7622a', 'status': 'present'}, '`AUD-597-A04` / `ASK-LEDGER-001`': {'commit': '46fb870', 'status': 'present'}, '`AUD-597-D01` / `B-04`': {'commit': '1094e6a', 'status': 'present'}, '`AUD-597-D02` / `ASK-DOC-001`': {'commit': 'ecb1bb7', 'status': 'present'}, '`AUD-597-D03` / `N-06`': {'commit': '9784333', 'status': 'present'}, '`AUD-597-D04` / `N-05`': {'commit': 'e51c087', 'status': 'present'}, '`AUD-597-D05` / `N-04`, fourth appearance of `C4-27`': {'commit': '9cccc15', 'status': 'present'}, '`AUD-597-D06` / `ASK-CLI-001`': {'commit': '178aa6d', 'status': 'present'}, '`AUD-597-D07` / `N-03`': {'commit': '9245e35', 'status': 'present'}, '`AUD-597-R01` / `ASK-PERF-001`': {'commit': '6910054', 'status': 'present'}, '`AUD-597-R02` / `ASK-ENC-001`': {'commit': 'ce6dac1', 'status': 'present'}, '`AUD-597-R03` / `ASK-UX-002`': {'commit': 'c57e50b', 'status': 'present'}, '`AUD-597-X01` / `ASK-BUILD-001` + `ASK-META-001` / `B-12`': {'commit': '354338b', 'status': 'present'}, '`AUD-598-B01` / `ASK-META-001`': {'commit': 'b2f7c65', 'status': 'present'}, '`AUD-598-B02` / `ASK-UX-002`': {'commit': '56a4bf7', 'status': 'present'}, '`AUD-598-B03` / `B-11`': {'commit': 'c63cb24', 'status': 'present'}, '`AUD-598-B04` / `N-02`': {'commit': '187937f', 'status': 'present'}, '`AUD-598-R01` / `ASK-PERF-002`': {'commit': '', 'status': 'uncited'}, '`AUD-598-X01` / `N-01` + `B-13` + `B-14` + `B-29` + `N-06`': {'commit': '6f2f5a4', 'status': 'present'}, '`AUD-600-B01` / `P-01`': {'commit': '366ffed', 'status': 'present'}, '`AUD-600-B02` / `P-02` + `ASK-PACK-002`': {'commit': '2a814c3', 'status': 'present'}, '`AUD-600-B03` / `ASK-PACK-001`': {'commit': '452f2a0', 'status': 'present'}, '`AUD-600-D01`': {'commit': '1224e1b', 'status': 'present'}, '`AUD-600-D02`': {'commit': 'eab8172', 'status': 'present'}, '`AUD-600-R01` / `R-01`': {'commit': '88add64', 'status': 'present'}, '`AUD-601-B01` / `N-02`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-B02` / `ASK-PACK-004`': {'commit': '', 'status': 'uncited'}, '`AUD-601-D01` / `N-01`, `ASK-UX-001`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-D02` / `B-13`, `B-14`, 5th round': {'commit': '', 'status': 'uncited'}, '`AUD-601-M01` / `B-26`, `B-27` retracted': {'commit': '', 'status': 'uncited'}, '`AUD-601-R01` / `R-02`': {'commit': '', 'status': 'uncited'}, '`AUD-619-WATCH`': {'commit': '', 'status': 'uncited'}, '`AUD-620-COMMAND-SURFACE`': {'commit': '', 'status': 'uncited'}, '`AUD-620-REVIEW-PR`': {'commit': '', 'status': 'uncited'}, '`AUD-621-REVIEW-PR`': {'commit': '', 'status': 'uncited'}, '`B-3`': {'commit': '773268d', 'status': 'present'}, '`B-6`': {'commit': '17c6e06', 'status': 'present'}, '`B-8`': {'commit': '8329614', 'status': 'present'}, '`BUG-3` / `443b345`': {'commit': '', 'status': 'uncited'}, '`BUG-4a` / `AUD-588-B11` residual': {'commit': '24b4185', 'status': 'present'}, '`BUG-4b` / `AUD-588-B11` residual': {'commit': '0376350', 'status': 'present'}, '`E-39`, class residual of `AUD-593-N06`': {'commit': '25fc11f', 'status': 'present'}, '`N-5`': {'commit': 'c68083f', 'status': 'present'}, '`N-7`': {'commit': '082e9de', 'status': 'present'}, '`R-1`': {'commit': '424fb1c', 'status': 'present'}, '`R-2`': {'commit': '5158b92', 'status': 'present'}, '`R-3`': {'commit': '88aec04', 'status': 'present'}, '`R-4`': {'commit': '04fd04f', 'status': 'present'}}
5
5
  CLOSURE_COVERAGE = {'closed_rows': 165, 'present': 91, 'absent': 0, 'unresolved': 0, 'uncited': 74, 'unknown': 0}
@@ -16,7 +16,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
16
16
 
17
17
  ## Current Synchronization — External Audit `AUD-621`
18
18
 
19
- **Release `5.9.18` remediation.** `AUD-621-PACK-STDERR` is corrected in code: the child pipe is read as UTF-8 with replacement and the forwarding loop retains a decode-failure guard. `AUD-621-R` has a second Windows barrier at process-pool construction, complementing the sequential fallback. Both findings remain open until their required native Windows evidence is recorded; `AUD-621-CONTEXT` and `AUD-621-WIN-PROC` are unchanged.
19
+ **Release `5.9.19` remediation.** The stderr-decoding fix for `AUD-621-PACK-STDERR` remains in code, and the distinct `pack gate --since` crash found by the post-remediation re-audit is corrected with a Python 3.9–3.11-compatible tar extraction path. `AUD-621-R` has a second Windows barrier at process-pool construction, complementing the sequential fallback. The broader findings remain open until their required native Windows evidence is recorded; `AUD-621-CONTEXT` and `AUD-621-WIN-PROC` are unchanged.
20
20
 
21
21
  **Re-audit of release `5.9.17`, received 2026-08-31.** Four independent
22
22
  Windows measurements overturn the apparent partial recovery of the shared
@@ -31,7 +31,9 @@ The audit also isolates a new, high-confidence Windows defect in `pack gate`:
31
31
  the `_forward_stderr` thread decodes child output using the active codepage,
32
32
  raises `UnicodeDecodeError` on UTF-8 bytes, then stops draining the pipe. The
33
33
  child can block once that pipe fills. Two independent forks captured the same
34
- traceback. The full evidence is
34
+ traceback. A separate re-audit then found that `pack gate --since` can still
35
+ fail earlier in `git_checkout.py` under Python 3.10, so the stderr fix is
36
+ necessary but not sufficient. The full evidence is
35
37
  [`AUDIT-2026-08-31-EXTERNAL-5.9.17.md`](AUDIT-2026-08-31-EXTERNAL-5.9.17.md).
36
38
 
37
39
  | ID | Severity | Status | Evidence and required closure |
@@ -46,16 +46,18 @@ CLI commands — impact, endpoints, spring-audit, explain, … each a pro
46
46
  The key idea: the extraction is **content-addressed**. Commands reuse the parse cache and,
47
47
  where their analysed scope matches, the shared Canonical IR; `ask cache model` names what a
48
48
  warm buys for each command rather than implying that every projection costs the same. In
49
- 5.9.18, `validation` enters through that shared CIR and `data-exposure` reuses one semantic
49
+ 5.9.19, `validation` enters through that shared CIR and `data-exposure` reuses one semantic
50
50
  model across all declared label seeds. (The extraction and consumption contract is fixed in
51
51
  the architecture ADRs 0001–0004 under `docs/architecture/`.)
52
52
 
53
53
  The current external pack audit intake is tracked in
54
54
  [`AUDIT-2026-08-26-EXTERNAL-5.8.28-PACK.md`](AUDIT-2026-08-26-EXTERNAL-5.8.28-PACK.md);
55
- the two pack fixes are included in release 5.9.18, with release-artifact verification
56
- remaining for detached execution and pack evidence.
55
+ the two pack fixes were tracked in release 5.9.18 as intended changes, and
56
+ release 5.9.19 adds the tarfile compatibility correction found by the later
57
+ re-audit. Release-artifact verification remains for detached execution and pack
58
+ evidence, and native Windows reliability evidence is still required.
57
59
 
58
- ### Reliability contracts in 5.9.18
60
+ ### Reliability contracts in 5.9.19
59
61
 
60
62
  **Failures must be visible.** `risk`, `verify`, `audit-report` and
61
63
  `migrate-check` turn unexpected analysis failures into structured
@@ -67,8 +69,11 @@ closing platform-specific findings.
67
69
  **Current Windows limitation.** The `5.9.17` external audit reproduced the
68
70
  shared worker-start failure in `risk`, `verify`, `audit-report`, `spring-audit`,
69
71
  `migrate-check` and `migrate-apply`. Do not use these commands as a production
70
- CI gate on Windows until the native matrix closes them. `pack gate --since` now
71
- uses safe UTF-8 stderr forwarding, but still requires native repeat evidence. See the
72
+ CI gate on Windows until the native matrix closes them. A 2026-08-31 re-audit
73
+ also found a separate `pack gate --since` crash in the git-ref materialization
74
+ path when the subprocess resolves to Python 3.10 and hits the Python 3.12-only
75
+ `TarFile.extract(..., filter="data")` API. Release 5.9.19 adds a compatible
76
+ fallback; the command still requires native repeat evidence. See the
72
77
  [development roadmap](DEVELOPMENT-ROADMAP.md#external-audit-intake--aud-621-re-audit-release-5917).
73
78
 
74
79
  **A deadline the root analysis honours.** `ASK_MAX_ANALYSIS_SECONDS=<seconds>` now
@@ -274,7 +279,7 @@ pipx install sourcecode # isolated install, no venv needed
274
279
 
275
280
  # Verify
276
281
  ask version
277
- # ask 5.9.17
282
+ # ask 5.9.19
278
283
  ```
279
284
 
280
285
  Requires Python 3.9+.
@@ -327,7 +332,7 @@ value in field use, and they are the ones to learn first:
327
332
  | `ask posture . --diff dev:prod` | The only command that answers *"what changes about effective access between two profile sets, across every endpoint at once"*. Experimental, and the most differentiated capability in the product. |
328
333
  | `ask endpoints .` | Every REST endpoint with its **effective path** (context-path + `spring.mvc.servlet.path` resolved), inferred security policy and per-endpoint confidence. Turns "audit the API" into a list. |
329
334
  | `ask spring-audit .` | Transactional anomalies that a compiler cannot see — `@Transactional` on a private method is a silent CGLIB no-op — plus the security surface. |
330
- | `ask migrate-check . --compact` | Boot 2→3 readiness plus Java LTS/licensing inventory: blockers, effort, target JDK evidence and explicit licensing-review signals. |
335
+ | `ask migrate-check . --compact` | Boot 2→3 readiness plus Java LTS/licensing and local JVM ecosystem inventory: blockers, effort, target JDK evidence, framework/runtime signals and explicit licensing-review signals. |
331
336
 
332
337
  Everything else is either a projection of the same model (`impact`, `export`, `explain`) or an
333
338
  agent-facing packaging of it (`--compact`, `--agent`, `prepare-context`). The full surface is
@@ -528,10 +528,17 @@ def ir_dict_to_canonical(
528
528
  files = sorted(file_set)
529
529
 
530
530
  # Sorted call graph edges — stable ordering: from, type, to
531
- call_graph = sorted(
532
- edges,
533
- key=lambda e: (e.get("from", ""), e.get("type", ""), e.get("to", "")),
534
- )
531
+ # The raw graph is owned by this CIR conversion and its edge order is not
532
+ # observable until after conversion. Sort in place instead of creating a
533
+ # second repository-sized list during the raw/CIR overlap peak.
534
+ if isinstance(edges, list):
535
+ edges.sort(key=lambda e: (e.get("from", ""), e.get("type", ""), e.get("to", "")))
536
+ call_graph = edges
537
+ else:
538
+ call_graph = sorted(
539
+ edges,
540
+ key=lambda e: (e.get("from", ""), e.get("type", ""), e.get("to", "")),
541
+ )
535
542
 
536
543
  # Dependency edges (structural subset of call_graph)
537
544
  _dep_types = frozenset({"imports", "extends", "implements", "injects"})
sourcecode/cli.py CHANGED
@@ -4404,7 +4404,9 @@ app = typer.Typer(
4404
4404
  name="ask",
4405
4405
  help=_HELP,
4406
4406
  add_completion=False,
4407
- rich_markup_mode="rich",
4407
+ # Rich's Windows help renderer can drop options from long command panels;
4408
+ # plain Click help keeps the complete option contract visible there.
4409
+ rich_markup_mode=None if os.name == "nt" else "rich",
4408
4410
  no_args_is_help=False,
4409
4411
  )
4410
4412
 
@@ -7531,6 +7533,7 @@ def prepare_context_cmd(
7531
7533
  Path("."),
7532
7534
  help="Repository path to analyze (default: current directory)",
7533
7535
  ),
7536
+ jobs: Optional[int] = _jobs_option(),
7534
7537
  since: Optional[str] = typer.Option(
7535
7538
  None,
7536
7539
  "--since",
@@ -7605,7 +7608,6 @@ def prepare_context_cmd(
7605
7608
  ),
7606
7609
  ),
7607
7610
  progress_mode: Optional[str] = _progress_option(),
7608
- jobs: Optional[int] = _jobs_option(),
7609
7611
  ) -> None:
7610
7612
  """Task-specific context for AI coding agents.
7611
7613
 
@@ -8426,6 +8428,7 @@ def repo_ir_cmd(
8426
8428
  Path("."),
8427
8429
  help="Repository root to analyze (default: current directory)",
8428
8430
  ),
8431
+ jobs: Optional[int] = _jobs_option(),
8429
8432
  since: Optional[str] = typer.Option(
8430
8433
  None,
8431
8434
  "--since",
@@ -8491,7 +8494,6 @@ def repo_ir_cmd(
8491
8494
  help="Compress output with gzip. Requires --output. Reduces large IR files by ~70-80%.",
8492
8495
  ),
8493
8496
  progress_mode: Optional[str] = _progress_option(),
8494
- jobs: Optional[int] = _jobs_option(),
8495
8497
  ) -> None:
8496
8498
  """Deterministic symbol-level IR for Java repositories.
8497
8499
 
@@ -9185,6 +9187,7 @@ def endpoints_cmd(
9185
9187
  Path("."),
9186
9188
  help="Repository path to scan for REST endpoints (default: current directory)",
9187
9189
  ),
9190
+ jobs: Optional[int] = _jobs_option(),
9188
9191
  output_path: Optional[Path] = typer.Option(
9189
9192
  None, "--output", "-o",
9190
9193
  help="Write output to a file instead of stdout.",
@@ -9251,7 +9254,6 @@ def endpoints_cmd(
9251
9254
  "than protect).",
9252
9255
  ),
9253
9256
  progress_mode: Optional[str] = _progress_option(),
9254
- jobs: Optional[int] = _jobs_option(),
9255
9257
  ) -> None:
9256
9258
  """Extract REST API endpoint surface from Java source files.
9257
9259
 
@@ -9806,6 +9808,7 @@ def export_cmd(
9806
9808
  Path("."),
9807
9809
  help="Repository path to export (default: current directory)",
9808
9810
  ),
9811
+ jobs: Optional[int] = _jobs_option(),
9809
9812
  output_path: Optional[Path] = typer.Option(
9810
9813
  None, "--output", "-o",
9811
9814
  help="Write output to a file instead of stdout.",
@@ -9848,7 +9851,6 @@ def export_cmd(
9848
9851
  "written on the diagram whenever it bites.",
9849
9852
  ),
9850
9853
  progress_mode: Optional[str] = _progress_option(),
9851
- jobs: Optional[int] = _jobs_option(),
9852
9854
  ) -> None:
9853
9855
  """Export structured, tool-agnostic codebase views for downstream tooling.
9854
9856
 
@@ -10015,6 +10017,7 @@ def validation_cmd(
10015
10017
  Path("."),
10016
10018
  help="Repository path to scan for request-body validation (default: current directory)",
10017
10019
  ),
10020
+ jobs: Optional[int] = _jobs_option(),
10018
10021
  output_path: Optional[Path] = typer.Option(
10019
10022
  None, "--output", "-o",
10020
10023
  help="Write output to a file instead of stdout.",
@@ -10045,7 +10048,6 @@ def validation_cmd(
10045
10048
  help=_NO_CACHE_SUBCOMMAND_HELP,
10046
10049
  ),
10047
10050
  progress_mode: Optional[str] = _progress_option(),
10048
- jobs: Optional[int] = _jobs_option(),
10049
10051
  ) -> None:
10050
10052
  """Map request-body validation per endpoint (constraints + custom validators).
10051
10053
 
@@ -12173,6 +12175,7 @@ def spring_audit_cmd(
12173
12175
  Path("."),
12174
12176
  help="Repository path to audit (default: current directory)",
12175
12177
  ),
12178
+ jobs: Optional[int] = _jobs_option(),
12176
12179
  output_path: Optional[Path] = typer.Option(
12177
12180
  None, "--output", "-o",
12178
12181
  help="Write output to a file instead of stdout.",
@@ -12304,7 +12307,6 @@ def spring_audit_cmd(
12304
12307
  ),
12305
12308
  detach: bool = _detach_option(),
12306
12309
  progress_mode: Optional[str] = _progress_option(),
12307
- jobs: Optional[int] = _jobs_option(),
12308
12310
  ) -> None:
12309
12311
  """Spring semantic audit: {{RULE_SUMMARY}}.
12310
12312
 
@@ -13483,6 +13485,7 @@ def risk_cmd(
13483
13485
  Path("."),
13484
13486
  help="Repository path (default: current directory).",
13485
13487
  ),
13488
+ jobs: Optional[int] = _jobs_option(),
13486
13489
  limit: int = typer.Option(
13487
13490
  50, "--limit", help="How many composed risks to publish (highest first).", min=0,
13488
13491
  ),
@@ -13542,7 +13545,6 @@ def risk_cmd(
13542
13545
  allow_partial: bool = _allow_partial_option(),
13543
13546
  detach: bool = _detach_option(),
13544
13547
  progress_mode: Optional[str] = _progress_option(),
13545
- jobs: Optional[int] = _jobs_option(),
13546
13548
  ) -> None:
13547
13549
  """What each defect actually costs, once reach and access are in it.
13548
13550
 
@@ -14796,6 +14798,7 @@ def posture_cmd(
14796
14798
  Path("."),
14797
14799
  help="Repository path (default: current directory).",
14798
14800
  ),
14801
+ jobs: Optional[int] = _jobs_option(),
14799
14802
  profile: Optional[str] = typer.Option(
14800
14803
  None,
14801
14804
  "--profile",
@@ -14877,7 +14880,6 @@ def posture_cmd(
14877
14880
  format: str = _format_option(help="Output format: json or yaml."),
14878
14881
  copy: bool = _copy_option(),
14879
14882
  progress_mode: Optional[str] = _progress_option(),
14880
- jobs: Optional[int] = _jobs_option(),
14881
14883
  ) -> None:
14882
14884
  """Which beans are wired under a profile set — and which cannot be decided.
14883
14885
 
@@ -15300,6 +15302,7 @@ def migrate_check_cmd(
15300
15302
  Path("."),
15301
15303
  help="Repository path to scan (default: current directory)",
15302
15304
  ),
15305
+ jobs: Optional[int] = _jobs_option(),
15303
15306
  output_path: Optional[Path] = typer.Option(
15304
15307
  None, "--output", "-o",
15305
15308
  help="Write output to a file instead of stdout.",
@@ -15441,7 +15444,6 @@ def migrate_check_cmd(
15441
15444
  ),
15442
15445
  detach: bool = _detach_option(),
15443
15446
  progress_mode: Optional[str] = _progress_option(),
15444
- jobs: Optional[int] = _jobs_option(),
15445
15447
  ) -> None:
15446
15448
  """Spring Boot 2→3 migration readiness + Java LTS/licensing inventory.
15447
15449
 
@@ -15847,6 +15849,7 @@ def impact_chain_cmd(
15847
15849
  Path("."),
15848
15850
  help="Repository root (default: current directory)",
15849
15851
  ),
15852
+ jobs: Optional[int] = _jobs_option(),
15850
15853
  depth: int = typer.Option(
15851
15854
  4,
15852
15855
  "--depth",
@@ -15886,7 +15889,6 @@ def impact_chain_cmd(
15886
15889
  ),
15887
15890
  ),
15888
15891
  progress_mode: Optional[str] = _progress_option(),
15889
- jobs: Optional[int] = _jobs_option(),
15890
15892
  ) -> None:
15891
15893
  """Spring impact-chain: systemic blast radius of a symbol; TX/SEC findings are opt-in.
15892
15894
 
@@ -405,7 +405,7 @@ class ContextGraph:
405
405
  """
406
406
 
407
407
  __slots__ = (
408
- "_cir", "_nodes_by_fqn", "_nodes", "_build_ms",
408
+ "_cir", "_raw_nodes", "_nodes_by_fqn", "_nodes", "_build_ms",
409
409
  "_body_index", "_literal_index", "_guard_index", "_span_index",
410
410
  "_class_type_index", "_relations_all", "_symbols_sorted",
411
411
  "_symbols_by_file", "_relations_by_source",
@@ -427,11 +427,9 @@ class ContextGraph:
427
427
  self._class_type_index: Optional[_ClassTypeIndex] = None
428
428
  # Index the rich node dicts once (the IR already produced them). This is
429
429
  # a lookup index over existing data, not a second copy of the model.
430
- raw_nodes = ((cir._raw_ir.get("graph") or {}).get("nodes")) or []
431
- self._nodes: tuple[Symbol, ...] = tuple(
432
- _node_to_symbol(n) for n in raw_nodes
433
- )
434
- self._nodes_by_fqn: dict[str, Symbol] = {s.fqn: s for s in self._nodes}
430
+ self._raw_nodes = ((cir._raw_ir.get("graph") or {}).get("nodes")) or []
431
+ self._nodes: Optional[tuple[Symbol, ...]] = None
432
+ self._nodes_by_fqn: Optional[dict[str, Symbol]] = None
435
433
  # Memoized full projections (the CIR is immutable for this graph's lifetime).
436
434
  # `relations()`/`symbols()` are called once PER FILE by per-file consumers such as
437
435
  # `_java_contract_from_graph`; without these caches each call re-converted every
@@ -447,6 +445,13 @@ class ContextGraph:
447
445
  self._symbols_by_file: Optional[dict[str, list[Symbol]]] = None
448
446
  self._relations_by_source: Optional[dict[str, list[Relation]]] = None
449
447
 
448
+ def _ensure_nodes(self) -> tuple[Symbol, ...]:
449
+ """Materialize the typed node view only for symbol-oriented queries."""
450
+ if self._nodes is None:
451
+ self._nodes = tuple(_node_to_symbol(n) for n in self._raw_nodes)
452
+ self._nodes_by_fqn = {s.fqn: s for s in self._nodes}
453
+ return self._nodes
454
+
450
455
  # -- construction -------------------------------------------------------
451
456
 
452
457
  @classmethod
@@ -501,7 +506,8 @@ class ContextGraph:
501
506
 
502
507
  def symbol(self, fqn: str) -> Optional[Symbol]:
503
508
  """Look up a single symbol by fully-qualified name. O(1)."""
504
- return self._nodes_by_fqn.get(fqn)
509
+ self._ensure_nodes()
510
+ return self._nodes_by_fqn.get(fqn) # type: ignore[union-attr]
505
511
 
506
512
  def symbols(
507
513
  self,
@@ -517,8 +523,9 @@ class ContextGraph:
517
523
  set. Results are sorted by FQN for determinism. This is the primitive
518
524
  the role-convenience methods below are thin sugar over.
519
525
  """
526
+ nodes = self._ensure_nodes()
520
527
  if self._symbols_sorted is None:
521
- self._symbols_sorted = tuple(sorted(self._nodes, key=lambda s: s.fqn))
528
+ self._symbols_sorted = tuple(sorted(nodes, key=lambda s: s.fqn))
522
529
  # Filter over the memoized fqn-sorted set (filtering preserves that order, so the
523
530
  # result is identical to sorting after filtering — but the sort happens once).
524
531
  return [
@@ -537,7 +544,7 @@ class ContextGraph:
537
544
  """
538
545
  if self._symbols_by_file is None:
539
546
  if self._symbols_sorted is None:
540
- self._symbols_sorted = tuple(sorted(self._nodes, key=lambda s: s.fqn))
547
+ self._symbols_sorted = tuple(sorted(self._ensure_nodes(), key=lambda s: s.fqn))
541
548
  idx: dict[str, list[Symbol]] = {}
542
549
  for s in self._symbols_sorted:
543
550
  idx.setdefault(s.source_file, []).append(s)
@@ -560,9 +567,7 @@ class ContextGraph:
560
567
 
561
568
  def types(self) -> list[Symbol]:
562
569
  """All class/interface/enum/annotation symbols, sorted by FQN."""
563
- return sorted(
564
- (s for s in self._nodes if s.is_type), key=lambda s: s.fqn
565
- )
570
+ return sorted((s for s in self._ensure_nodes() if s.is_type), key=lambda s: s.fqn)
566
571
 
567
572
  def fields_of(self, type_fqn: str) -> list[Symbol]:
568
573
  """Annotated field symbols declared directly on a type (the IR emits a
@@ -572,7 +577,7 @@ class ContextGraph:
572
577
  prefix = type_fqn + "."
573
578
  out = [
574
579
  s
575
- for s in self._nodes
580
+ for s in self._ensure_nodes()
576
581
  if s.kind == "field"
577
582
  and s.fqn.startswith(prefix)
578
583
  and "." not in s.fqn[len(prefix):]
@@ -1010,17 +1015,18 @@ class ContextGraph:
1010
1015
  def metrics(self) -> dict:
1011
1016
  """Structural size + composition of the graph. For Phase 1 measurement;
1012
1017
  derived entirely from already-built data."""
1013
- role_hist = Counter(s.role for s in self._nodes)
1014
- kind_hist = Counter(s.kind for s in self._nodes)
1018
+ nodes = self._ensure_nodes()
1019
+ role_hist = Counter(s.role for s in nodes)
1020
+ kind_hist = Counter(s.kind for s in nodes)
1015
1021
  edge_hist = Counter(e.get("type", "") for e in self._cir.call_graph)
1016
1022
  grounded = sum(
1017
1023
  1
1018
- for s in self._nodes
1024
+ for s in nodes
1019
1025
  if s.in_degree or s.out_degree
1020
1026
  )
1021
1027
  return {
1022
1028
  "cir_hash": self._cir.cir_hash,
1023
- "node_count": len(self._nodes),
1029
+ "node_count": len(nodes),
1024
1030
  "relation_count": len(self._cir.call_graph),
1025
1031
  "endpoint_count": len(self._cir.endpoints),
1026
1032
  "grounded_node_count": grounded,
@@ -1033,7 +1039,7 @@ class ContextGraph:
1033
1039
 
1034
1040
  def __repr__(self) -> str: # pragma: no cover - debug aid
1035
1041
  return (
1036
- f"ContextGraph(nodes={len(self._nodes)}, "
1042
+ f"ContextGraph(nodes={len(self._ensure_nodes())}, "
1037
1043
  f"relations={len(self._cir.call_graph)}, "
1038
1044
  f"endpoints={len(self._cir.endpoints)}, "
1039
1045
  f"hash={self._cir.cir_hash[:12]})"
@@ -27,6 +27,7 @@ after it borrowed a parent's ref once (C3-27).
27
27
  from __future__ import annotations
28
28
 
29
29
  import subprocess
30
+ import sys
30
31
  import tarfile
31
32
  import tempfile
32
33
  from contextlib import contextmanager
@@ -149,4 +150,10 @@ def _extract_safely(tar: tarfile.TarFile, dest: Path) -> None:
149
150
  resolved = (root / member.name).resolve()
150
151
  if not str(resolved).startswith(str(root)):
151
152
  continue
152
- tar.extract(member, dest, filter="data")
153
+ # `filter` was added in Python 3.12 (PEP 706). The explicit path and
154
+ # link checks above provide the compatibility-safe extraction policy
155
+ # for the Python versions still supported by this project.
156
+ if sys.version_info >= (3, 12):
157
+ tar.extract(member, dest, filter="data")
158
+ else:
159
+ tar.extract(member, dest)
@@ -0,0 +1,294 @@
1
+ """Deterministic, local JVM ecosystem evidence for ``migrate-check``.
2
+
3
+ This module deliberately reports observations, not recommendations. It never
4
+ executes a build, resolves dependencies, or accesses a network service.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import json
10
+ import re
11
+ from pathlib import Path
12
+ from typing import Iterable, Optional
13
+
14
+ _SKIP_DIRS = {
15
+ ".git", ".gradle", ".mvn", ".idea", ".vscode", "build", "target",
16
+ "out", "dist", "bin", "node_modules", "__pycache__", "generated-sources",
17
+ }
18
+ _MAX_FILES = 800
19
+ _MAX_BYTES = 256 * 1024
20
+ _MAX_EVIDENCE = 160
21
+ _MAX_SBOM_COMPONENTS = 400
22
+ _TEXT_NAMES = {
23
+ "pom.xml", "build.gradle", "build.gradle.kts", "gradle.properties",
24
+ "settings.gradle", "settings.gradle.kts", "Dockerfile", "docker-compose.yml",
25
+ "docker-compose.yaml", "Jenkinsfile", ".gitlab-ci.yml", "config.yml",
26
+ ".java-version", ".sdkmanrc", "jvm.config",
27
+ }
28
+ _TEXT_SUFFIXES = {
29
+ ".java", ".xml", ".gradle", ".kts", ".properties", ".yml", ".yaml",
30
+ ".sh", ".bat", ".cmd", ".ps1",
31
+ }
32
+ _SBOM_NAMES = {"bom.json", "bom.cdx.json", "bom.cyclonedx.json", "sbom.json", "sbom.spdx.json"}
33
+
34
+ _FRAMEWORK_COORDINATES = {
35
+ "spring-boot": ("org.springframework.boot", re.compile(r"spring-boot(?:-[\w-]+)?")),
36
+ "spring-framework": ("org.springframework", re.compile(r"spring-(?:core|context|web|beans)")),
37
+ "hibernate": ("org.hibernate", re.compile(r"hibernate-[\w-]+")),
38
+ "jakarta-ee": ("jakarta.platform", re.compile(r"jakarta(?:\.\w+)?")),
39
+ "quarkus": ("io.quarkus", re.compile(r"quarkus(?:-[\w-]+)?")),
40
+ "micronaut": ("io.micronaut", re.compile(r"micronaut(?:-[\w-]+)?")),
41
+ }
42
+ _COORD_RE = re.compile(
43
+ r"(?P<group>org\.springframework(?:\.boot)?|org\.hibernate|jakarta\.platform|"
44
+ r"io\.quarkus(?:\.platform)?|io\.micronaut)[:\"'](?P<artifact>[\w.-]+)"
45
+ r"(?:[:\"'](?P<version>[\w.${}+-]+))?"
46
+ )
47
+ _MAVEN_COORD_RE = re.compile(
48
+ r"<groupId>\s*(?P<group>[^<]+)\s*</groupId>\s*"
49
+ r"<artifactId>\s*(?P<artifact>[^<]+)\s*</artifactId>"
50
+ r"(?:\s*<version>\s*(?P<version>[^<]+)\s*</version>)?",
51
+ re.DOTALL,
52
+ )
53
+ _IMPORT_RE = re.compile(r"^\s*import\s+(?P<package>org\.springframework(?:\.boot)?|org\.hibernate|jakarta\.|io\.quarkus|io\.micronaut)")
54
+ _PATTERNS = {
55
+ "module_info": re.compile(r"\bmodule\s+[\w.]+\s*\{"),
56
+ "virtual_threads": re.compile(r"\b(?:Thread\.ofVirtual|Executors\.newVirtualThreadPerTaskExecutor)\b"),
57
+ "scoped_value": re.compile(r"\bScopedValue\b"),
58
+ "native_image": re.compile(r"\bnative-image\b"),
59
+ "jlink": re.compile(r"\bjlink\b"),
60
+ "jpackage": re.compile(r"\bjpackage\b"),
61
+ }
62
+ _JAVA_VERSION_RE = re.compile(
63
+ r"(?:java-version|JAVA_VERSION|jdk|JDK|temurin|corretto|zulu|openjdk)[^\n\r:=-]*[:= -]+(?:['\"]?)(?:Java[- ]?)?(?P<version>\d{1,2})(?:['\"]|\b)",
64
+ re.IGNORECASE,
65
+ )
66
+
67
+
68
+ def _line_evidence(path: str, line: int, text: str, *, kind: str, detail: str) -> dict:
69
+ return {"kind": kind, "path": path, "line": line, "detail": detail, "text": text.strip()[:240]}
70
+
71
+
72
+ def _sbom_inventory(files: list[tuple[Path, str]], limitations: list[str]) -> dict:
73
+ """Read local CycloneDX/SPDX JSON without treating it as a security verdict."""
74
+ documents: list[dict] = []
75
+ for path, rel in files:
76
+ if path.suffix.lower() != ".json":
77
+ continue
78
+ try:
79
+ if path.stat().st_size > _MAX_BYTES:
80
+ continue
81
+ raw = path.read_text(encoding="utf-8", errors="replace")
82
+ data = json.loads(raw)
83
+ except (OSError, ValueError):
84
+ continue
85
+ if not isinstance(data, dict):
86
+ continue
87
+ if data.get("bomFormat") == "CycloneDX":
88
+ fmt, version, entries = "cyclonedx", data.get("specVersion"), data.get("components", [])
89
+ elif isinstance(data.get("spdxVersion"), str) and data["spdxVersion"].startswith("SPDX-"):
90
+ fmt, version, entries = "spdx", data.get("spdxVersion"), data.get("packages", [])
91
+ else:
92
+ continue
93
+ if not isinstance(entries, list):
94
+ limitations.append(f"{rel}: SBOM component list is not an array")
95
+ continue
96
+ components: list[dict] = []
97
+ for entry in entries[:_MAX_SBOM_COMPONENTS]:
98
+ if not isinstance(entry, dict):
99
+ continue
100
+ if fmt == "cyclonedx":
101
+ name = entry.get("name")
102
+ version_value = entry.get("version")
103
+ group = entry.get("group")
104
+ purl = entry.get("purl")
105
+ else:
106
+ name = entry.get("name")
107
+ version_value = entry.get("versionInfo")
108
+ group = None
109
+ purl = next((ref.get("referenceLocator") for ref in entry.get("externalRefs", [])
110
+ if isinstance(ref, dict) and ref.get("referenceType") == "purl"), None)
111
+ if not isinstance(name, str):
112
+ continue
113
+ components.append({
114
+ "name": name,
115
+ "group": group if isinstance(group, str) else None,
116
+ "version": version_value if isinstance(version_value, str) else None,
117
+ "purl": purl if isinstance(purl, str) else None,
118
+ })
119
+ if len(entries) > _MAX_SBOM_COMPONENTS:
120
+ limitations.append(f"{rel}: SBOM components capped at {_MAX_SBOM_COMPONENTS}")
121
+ documents.append({
122
+ "format": fmt,
123
+ "spec_version": version if isinstance(version, str) else None,
124
+ "path": rel,
125
+ "component_count": len(components),
126
+ "components": components,
127
+ })
128
+ return {
129
+ "status": "observed" if documents else "unobserved",
130
+ "documents": sorted(documents, key=lambda item: item["path"]),
131
+ "policy": "local CycloneDX/SPDX JSON parsing only; no CVE, license, signature, or network validation",
132
+ }
133
+
134
+
135
+ def _files(root: Path, source_paths: Optional[Iterable[str]] = None) -> tuple[list[tuple[Path, str]], list[str]]:
136
+ found: list[tuple[Path, str]] = []
137
+ limitations: list[str] = []
138
+ explicit: set[str] = set()
139
+ for rel in source_paths or ():
140
+ normalized = Path(rel).as_posix()
141
+ path = root / normalized
142
+ if path.is_file():
143
+ found.append((path, normalized))
144
+ explicit.add(normalized)
145
+ for dirpath, dirnames, filenames in os.walk(root):
146
+ dirnames[:] = sorted(d for d in dirnames if d not in _SKIP_DIRS)
147
+ for name in sorted(filenames):
148
+ path = Path(dirpath) / name
149
+ rel = path.relative_to(root).as_posix()
150
+ if rel in explicit:
151
+ continue
152
+ if (name in _TEXT_NAMES or path.suffix.lower() in _TEXT_SUFFIXES or
153
+ name in _SBOM_NAMES or "cyclonedx" in name.lower() or
154
+ "spdx" in name.lower() or ".github/workflows/" in f"/{rel}" or
155
+ rel.startswith(".circleci/")):
156
+ found.append((path, rel))
157
+ priority = []
158
+ regular = []
159
+ for item in found:
160
+ rel = item[1]
161
+ if (Path(rel).parent == Path(".") or rel.startswith((".github/", ".circleci/", ".gitlab/")) or
162
+ Path(rel).name in _TEXT_NAMES or Path(rel).name in _SBOM_NAMES):
163
+ priority.append(item)
164
+ else:
165
+ regular.append(item)
166
+ ordered = sorted(priority, key=lambda item: item[1]) + sorted(regular, key=lambda item: item[1])
167
+ if source_paths is None and len(ordered) > _MAX_FILES:
168
+ limitations.append(f"file scan capped at {_MAX_FILES} text files")
169
+ ordered = ordered[:_MAX_FILES]
170
+ return ordered, limitations
171
+
172
+
173
+ def scan_jvm_inventory(
174
+ root: Path,
175
+ build_sources: Iterable[tuple[str, str]] = (),
176
+ *,
177
+ source_paths: Optional[Iterable[str]] = None,
178
+ ) -> dict:
179
+ """Return bounded JVM facts with local file/line provenance."""
180
+ files, limitations = _files(root, source_paths)
181
+ sbom = _sbom_inventory(files, limitations)
182
+ declared: dict[str, list[dict]] = {name: [] for name in _FRAMEWORK_COORDINATES}
183
+ observed: dict[str, list[dict]] = {name: [] for name in _FRAMEWORK_COORDINATES}
184
+ patterns: dict[str, list[dict]] = {name: [] for name in _PATTERNS}
185
+ java_runtime: list[dict] = []
186
+ build_text_map = {path: text for path, text in build_sources}
187
+
188
+ texts: list[tuple[str, str]] = []
189
+ for path, rel in files:
190
+ text = build_text_map.get(rel)
191
+ if text is None:
192
+ try:
193
+ if path.stat().st_size > _MAX_BYTES:
194
+ limitations.append(f"{rel}: file exceeds {_MAX_BYTES} byte read limit")
195
+ continue
196
+ text = path.read_text(encoding="utf-8", errors="replace")
197
+ except OSError:
198
+ limitations.append(f"{rel}: could not be read")
199
+ continue
200
+ texts.append((rel, text))
201
+
202
+ for rel, text in texts:
203
+ is_build = rel.endswith(("pom.xml", "build.gradle", "build.gradle.kts", "gradle.properties"))
204
+ java_block_comment = False
205
+ if rel.endswith("pom.xml"):
206
+ for match in _MAVEN_COORD_RE.finditer(text):
207
+ group, artifact = match.group("group").strip(), match.group("artifact").strip()
208
+ for name, (prefix, artifact_re) in _FRAMEWORK_COORDINATES.items():
209
+ if group.startswith(prefix) and artifact_re.fullmatch(artifact):
210
+ line_no = text[:match.start()].count("\n") + 1
211
+ declared[name].append(_line_evidence(
212
+ rel, line_no, match.group(0), kind="declared_coordinate",
213
+ detail=f"{group}:{artifact}" + (f":{match.group('version').strip()}" if match.group("version") else ""),
214
+ ))
215
+ for line_no, line in enumerate(text.splitlines(), 1):
216
+ detection_line = line
217
+ if rel.endswith(".java"):
218
+ pieces: list[str] = []
219
+ rest = line
220
+ while rest:
221
+ if java_block_comment:
222
+ end = rest.find("*/")
223
+ if end < 0:
224
+ rest = ""
225
+ break
226
+ java_block_comment = False
227
+ rest = rest[end + 2:]
228
+ else:
229
+ start = rest.find("/*")
230
+ if start < 0:
231
+ pieces.append(rest)
232
+ break
233
+ pieces.append(rest[:start])
234
+ java_block_comment = True
235
+ rest = rest[start + 2:]
236
+ detection_line = "".join(pieces).split("//", 1)[0]
237
+ if Path(rel).name == "module-info.java" and detection_line.strip().startswith("module "):
238
+ patterns["module_info"].append(_line_evidence(
239
+ rel, line_no, line, kind="observed_pattern", detail="module-info.java"
240
+ ))
241
+ if is_build:
242
+ for match in _COORD_RE.finditer(line):
243
+ group, artifact = match.group("group"), match.group("artifact")
244
+ for name, (prefix, artifact_re) in _FRAMEWORK_COORDINATES.items():
245
+ if group.startswith(prefix) and artifact_re.fullmatch(artifact):
246
+ declared[name].append(_line_evidence(
247
+ rel, line_no, line, kind="declared_coordinate",
248
+ detail=f"{group}:{artifact}" + (f":{match.group('version')}" if match.group("version") else ""),
249
+ ))
250
+ import_match = _IMPORT_RE.match(detection_line)
251
+ if import_match:
252
+ package = import_match.group("package")
253
+ name = (
254
+ "spring-boot" if package == "org.springframework.boot" else
255
+ "spring-framework" if package == "org.springframework" else
256
+ "hibernate" if package == "org.hibernate" else
257
+ "jakarta-ee" if package == "jakarta." else
258
+ "quarkus" if package == "io.quarkus" else "micronaut"
259
+ )
260
+ observed[name].append(_line_evidence(rel, line_no, line, kind="observed_import", detail=package))
261
+ for name, pattern in _PATTERNS.items():
262
+ if name != "module_info" or Path(rel).name != "module-info.java":
263
+ if pattern.search(detection_line):
264
+ patterns[name].append(_line_evidence(rel, line_no, line, kind="observed_pattern", detail=name))
265
+ if (".github/workflows/" in f"/{rel}" or rel.startswith((".circleci/", ".gitlab/")) or
266
+ Path(rel).name in {"Jenkinsfile", ".gitlab-ci.yml"} or rel.startswith("Dockerfile")):
267
+ match = _JAVA_VERSION_RE.search(line)
268
+ if match:
269
+ java_runtime.append(_line_evidence(
270
+ rel, line_no, line, kind="declared_java_runtime", detail=f"java:{match.group('version')}"
271
+ ))
272
+
273
+ def capped(items: list[dict]) -> list[dict]:
274
+ if len(items) > _MAX_EVIDENCE:
275
+ limitations.append(f"evidence capped at {_MAX_EVIDENCE} items per category")
276
+ return items[:_MAX_EVIDENCE]
277
+
278
+ frameworks = {}
279
+ for name in _FRAMEWORK_COORDINATES:
280
+ frameworks[name] = {
281
+ "declared": capped(sorted(declared[name], key=lambda x: (x["path"], x["line"]))),
282
+ "observed": capped(sorted(observed[name], key=lambda x: (x["path"], x["line"]))),
283
+ "status": "declared_and_observed" if declared[name] and observed[name]
284
+ else "declared_only" if declared[name] else "observed_only" if observed[name] else "unobserved",
285
+ }
286
+ return {
287
+ "schema_version": "jvm-inventory-v1",
288
+ "frameworks": frameworks,
289
+ "patterns": {name: capped(sorted(items, key=lambda x: (x["path"], x["line"]))) for name, items in patterns.items()},
290
+ "java_runtime": capped(sorted(java_runtime, key=lambda x: (x["path"], x["line"]))),
291
+ "sbom": sbom,
292
+ "limitations": sorted(set(limitations)),
293
+ "analysis_policy": "local text evidence only; no build, dependency resolution, network, or recommendations",
294
+ }
@@ -26,6 +26,7 @@ from typing import Any, Iterable, Iterator, Optional, TYPE_CHECKING
26
26
  from sourcecode.degradation import cap_effect
27
27
  from sourcecode.path_filters import is_test_or_fixture_path
28
28
  from sourcecode.jdk_exports import JDK_UNCONDITIONAL_EXPORTS
29
+ from sourcecode.jvm_inventory import scan_jvm_inventory
29
30
 
30
31
  if TYPE_CHECKING:
31
32
  from sourcecode.hibernate_strat import HibernateStratification
@@ -1905,6 +1906,8 @@ class MigrationReport:
1905
1906
  # F-P: Java LTS/licensing inventory, derived from build/runtime hints. Additive
1906
1907
  # field: it informs migration planning but does not participate in readiness_score.
1907
1908
  java_lts_inventory: dict = field(default_factory=dict)
1909
+ # JVM ecosystem evidence. Additive and advisory: it never changes readiness.
1910
+ jvm_inventory: dict = field(default_factory=dict)
1908
1911
  evidence_manifest: dict = field(default_factory=dict)
1909
1912
 
1910
1913
  def finalize(self) -> "MigrationReport":
@@ -2572,6 +2575,7 @@ class MigrationReport:
2572
2575
  "spring_boot_version_detected": self.spring_boot_version_detected,
2573
2576
  "boot3_axis_completed": self.boot3_axis_completed,
2574
2577
  "java_lts_inventory": self.java_lts_inventory,
2578
+ "jvm_inventory": self.jvm_inventory,
2575
2579
  **({"evidence_manifest": self.evidence_manifest} if self.evidence_manifest else {}),
2576
2580
  "summary": self.summary,
2577
2581
  "findings": [f.to_dict() for f in _listed],
@@ -3328,6 +3332,8 @@ def run_migrate_check(
3328
3332
  spring_boot_version=spring_boot_version,
3329
3333
  target_jdk=target_jdk,
3330
3334
  )
3335
+ with _phase("jvm_inventory"):
3336
+ jvm_inventory = scan_jvm_inventory(root, _build_sources, source_paths=file_paths)
3331
3337
 
3332
3338
  # BUG #8: classify each finding's code context (main / test / generated) so the
3333
3339
  # report can keep test fixtures and autogenerated sources out of blocking_count.
@@ -3381,6 +3387,7 @@ def run_migrate_check(
3381
3387
  findings=all_findings,
3382
3388
  hibernate=hibernate_strat,
3383
3389
  java_lts_inventory=java_lts_inventory,
3390
+ jvm_inventory=jvm_inventory,
3384
3391
  limitations=limitations,
3385
3392
  metadata={
3386
3393
  "java_files_scanned": len(file_paths),
sourcecode/parallel.py CHANGED
@@ -266,16 +266,6 @@ def prefill_parse_cache(
266
266
  # a build that forks itself on every analysis.
267
267
  receipt["reason"] = "frozen build — worker processes would re-enter the CLI"
268
268
  return receipt
269
- if os.name == "nt":
270
- # Windows workers use ``spawn`` and must duplicate the parent's IPC
271
- # handles. A denied DuplicateHandle leaves a ProcessPoolExecutor with no
272
- # usable workers; depending on which consumer waits for its cache entry,
273
- # that used to become either silent success or an unbounded wait. The
274
- # sequential loop below is the designed fallback and produces the same
275
- # IR, so prefer a reliable answer to speculative parse parallelism.
276
- receipt["reason"] = "Windows — parsing sequentially to avoid spawn worker-handle failures"
277
- return receipt
278
-
279
269
  # Retire dead generations once, here, rather than letting N workers race to
280
270
  # do it (C3-70's purge is idempotent, but doing it once is cheaper).
281
271
  with suppress(Exception):
@@ -308,7 +298,9 @@ def prefill_parse_cache(
308
298
  # the caller's unchanged sequential loop produces the same IR.
309
299
  if pool is not None:
310
300
  _abort_executor(pool)
311
- receipt["reason"] = f"pool unavailable ({type(exc).__name__}) — parsing sequentially"
301
+ receipt["reason"] = (
302
+ f"pool unavailable ({type(exc).__name__}: {exc}) — parsing sequentially"
303
+ )
312
304
  return receipt
313
305
  if not receipt["reason"]:
314
306
  receipt["reason"] = f"{receipt['parsed']} parsed and {receipt['hit']} already cached"
@@ -33,7 +33,7 @@ RELEASE_DATE = "2026-08-31"
33
33
  #: computation the test re-runs). The nine-day interval before 5.9.0 lowers
34
34
  #: that window to 29 releases over 18 days, so the constant follows the current
35
35
  #: measurement rather than preserving the former short-burst cadence.
36
- RELEASE_RATE_PER_DAY = 1.61
36
+ RELEASE_RATE_PER_DAY = 3.22
37
37
 
38
38
  #: How many releases behind is worth telling the operator about. Four, which on
39
39
  #: the measured cadence is about three days: one release is under a day and warning
@@ -6114,23 +6114,29 @@ def _build_route_surface(
6114
6114
  # Phase 3: inheritance projection — subclasses with a class-level @RequestMapping
6115
6115
  # prefix inherit parent methods that they do not override (same HTTP verb + path suffix).
6116
6116
  if extends_map:
6117
+ # Candidate resolution used to scan every class for every child. On large
6118
+ # repositories this made route projection quadratic in the number of types.
6119
+ # The module prefix is the same discriminator used by the old predicate,
6120
+ # so indexing it preserves its ambiguity rule exactly.
6121
+ def _route_module_key(data: dict) -> tuple[str, str]:
6122
+ source_file = data["source_file"]
6123
+ module = source_file.split("src/", 1)[0] if "src/" in source_file else ""
6124
+ return module, data["fqn"].split(".")[-1]
6125
+
6126
+ _classes_by_module_name: dict[tuple[str, str], list[str]] = {}
6127
+ for _unit, _data in class_info.items():
6128
+ _classes_by_module_name.setdefault(
6129
+ _route_module_key(_data), []
6130
+ ).append(_unit)
6117
6131
  simple_extends: dict[str, str] = {}
6118
6132
  for child_unit, child_data in class_info.items():
6119
6133
  parent_simple = extends_map.get(child_data["fqn"])
6120
6134
  if not parent_simple:
6121
6135
  continue
6122
- child_module = (
6123
- child_data["source_file"].split("src/", 1)[0]
6124
- if "src/" in child_data["source_file"] else ""
6136
+ child_module = _route_module_key(child_data)[0]
6137
+ candidates = _classes_by_module_name.get(
6138
+ (child_module, parent_simple), []
6125
6139
  )
6126
- candidates = [
6127
- unit for unit, data in class_info.items()
6128
- if data["fqn"].split(".")[-1] == parent_simple
6129
- and (
6130
- data["source_file"].split("src/", 1)[0]
6131
- if "src/" in data["source_file"] else ""
6132
- ) == child_module
6133
- ]
6134
6140
  if len(candidates) == 1:
6135
6141
  simple_extends[child_unit] = candidates[0]
6136
6142
 
@@ -6694,7 +6700,12 @@ def build_repo_ir(
6694
6700
  # shared store publishes per repository. Collected in the loop that computes
6695
6701
  # the keys anyway, so it costs a dictionary.
6696
6702
  _repo_keys: "dict[str, str]" = {}
6697
- _per_file: list[tuple[str, str, str, list[str], list[SymbolRecord]]] = []
6703
+ # Keep parse metadata, not source text. Retaining every file body here made
6704
+ # full CIR builds hold a second copy of large repositories until assembly.
6705
+ # Pass 2 rereads the source only for relation/body extraction.
6706
+ _per_file: list[tuple[str, str, list[str], list[SymbolRecord]]] = []
6707
+ _repo_constants: dict[str, str] = {}
6708
+ _ambiguous_constants: set[str] = set()
6698
6709
  # (package, imports, annotation-ref-pairs) for the reference-graph post-pass,
6699
6710
  # collected from BOTH fully-parsed and pre-scan-skipped files (BUG #2).
6700
6711
  _ann_ref_sources: list[tuple[str, list[str], list[tuple[str, str]]]] = []
@@ -6758,6 +6769,15 @@ def build_repo_ir(
6758
6769
  _pc.put(_pkey, package, raw_imports, symbols, _excluded)
6759
6770
  _meta_symbols_excluded += _excluded
6760
6771
  all_symbols.extend(symbols)
6772
+ for _name, _value in _collect_file_constants(source).items():
6773
+ if _name in _ambiguous_constants:
6774
+ continue
6775
+ _previous = _repo_constants.get(_name)
6776
+ if _previous is None:
6777
+ _repo_constants[_name] = _value
6778
+ elif _previous != _value:
6779
+ del _repo_constants[_name]
6780
+ _ambiguous_constants.add(_name)
6761
6781
  # P1-E (parse-coverage reconciliation): a file that declares a type but
6762
6782
  # yielded zero symbols is silently invisible to the graph. Record the
6763
6783
  # candidate here (source is already in memory; masking only runs for the
@@ -6773,7 +6793,7 @@ def build_repo_ir(
6773
6793
  _masked_src = _mask_java(source)
6774
6794
  if _masked_src.count("{") != _masked_src.count("}"):
6775
6795
  _unbalanced_files.append(rel_path)
6776
- _per_file.append((rel_path, source, package, raw_imports, symbols))
6796
+ _per_file.append((rel_path, package, raw_imports, symbols))
6777
6797
  if "@" in source:
6778
6798
  _parsed_refs = _extract_annotation_type_refs(source, package)
6779
6799
  if _parsed_refs:
@@ -6786,19 +6806,62 @@ def build_repo_ir(
6786
6806
 
6787
6807
  # Build {package: {simple_name: FQN}} from every class/interface found.
6788
6808
  _same_pkg_map: dict[str, dict[str, str]] = _build_same_package_map(all_symbols)
6809
+ _type_map_material = repr(
6810
+ sorted(
6811
+ (pkg, name, fqn)
6812
+ for pkg, names in _same_pkg_map.items()
6813
+ for name, fqn in names.items()
6814
+ )
6815
+ ).encode("utf-8", "replace")
6816
+ _relation_resolution_sig = hashlib.sha256(_type_map_material).hexdigest()[:16]
6789
6817
 
6790
6818
  # Pass 2: build relations with same-package type resolution available.
6791
- for _file_index, (rel_path, source, package, raw_imports, symbols) in enumerate(
6819
+ for _file_index, (rel_path, package, raw_imports, symbols) in enumerate(
6792
6820
  _per_file, start=1
6793
6821
  ):
6794
6822
  if progress is not None:
6795
6823
  progress(_file_index, _total_files, "linking")
6796
6824
  same_pkg_types = _same_pkg_map.get(package, {})
6797
- relations = _build_relations(
6798
- symbols, raw_imports, source, package, rel_path,
6799
- same_pkg_types=same_pkg_types,
6800
- pkg_type_map=_same_pkg_map,
6801
- )
6825
+ try:
6826
+ source = (root / rel_path).read_text(encoding="utf-8", errors="replace")
6827
+ except OSError:
6828
+ continue
6829
+ _relation_key = hashlib.sha256(
6830
+ f"relations:{_pc.file_key(rel_path, source, _capture_sig)}:"
6831
+ f"{_relation_resolution_sig}".encode("utf-8", "replace")
6832
+ ).hexdigest()
6833
+ _relation_blob = _pc.get_blob(_relation_key)
6834
+ relations: list[RelationEdge]
6835
+ _relation_cache_valid = False
6836
+ if isinstance(_relation_blob, dict) and isinstance(
6837
+ _relation_blob.get("relations"), list
6838
+ ):
6839
+ try:
6840
+ relations = [
6841
+ RelationEdge(
6842
+ from_symbol=str(edge["from"]),
6843
+ to_symbol=str(edge["to"]),
6844
+ type=str(edge["type"]),
6845
+ confidence=str(edge.get("confidence", "high")),
6846
+ evidence=dict(edge.get("evidence") or {}),
6847
+ )
6848
+ for edge in _relation_blob["relations"]
6849
+ ]
6850
+ _relation_cache_valid = True
6851
+ except (AttributeError, KeyError, TypeError, ValueError):
6852
+ relations = []
6853
+ else:
6854
+ relations = []
6855
+ if not _relation_cache_valid:
6856
+ relations = _build_relations(
6857
+ symbols, raw_imports, source, package, rel_path,
6858
+ same_pkg_types=same_pkg_types,
6859
+ pkg_type_map=_same_pkg_map,
6860
+ )
6861
+ _pc.put_blob(
6862
+ _relation_key,
6863
+ {"relations": [_edge_to_dict(edge) for edge in relations]},
6864
+ )
6802
6865
  if emit_body_facts:
6803
6866
  # V4b: the per-file body-fact extractors are pure functions of the file's
6804
6867
  # bytes, so their combined output is memoised under the same content key.
@@ -6912,6 +6975,11 @@ def build_repo_ir(
6912
6975
  )
6913
6976
  )
6914
6977
 
6978
+ # These pass-local containers are no longer needed after linking. Drop their
6979
+ # references before _assemble creates the canonical projections, reducing the
6980
+ # overlap between the raw-build and canonical-build working sets.
6981
+ del _per_file, _repo_keys, _ann_ref_sources, _ambiguous_constants
6982
+
6915
6983
  # C3-85: from here to the end of the build there is no per-file loop, so the
6916
6984
  # heartbeat kept `linking n/n` on screen — a counter that finished, reading as
6917
6985
  # a stage still running. Measured on keycloak (5 486 files, warm), the stretch
@@ -6931,6 +6999,11 @@ def build_repo_ir(
6931
6999
  seen.add(key)
6932
7000
  unique_relations.append(e)
6933
7001
 
7002
+ # Linking and the cross-file resolution passes are complete. Keep only the
7003
+ # deduplicated relation set for assembly; the pre-dedup list and type closure
7004
+ # otherwise overlap with the canonical projection at the memory peak.
7005
+ del all_relations, _same_pkg_map, _supertypes
7006
+
6934
7007
  # When since is given, route_surface is a diff view (empty [] or list of changes).
6935
7008
  # When since is None, pass None so _build_route_surface builds the full route surface.
6936
7009
  route_diffs_arg: Optional[list[dict]] = (
@@ -6943,7 +7016,7 @@ def build_repo_ir(
6943
7016
  unbalanced_files=_unbalanced_files,
6944
7017
  # Cross-file constant folding for annotation paths: per-file folding only
6945
7018
  # sees constants declared in the same file (see _collect_repo_constants).
6946
- constants=_collect_repo_constants(_per_file),
7019
+ constants=_repo_constants,
6947
7020
  root=root,
6948
7021
  )
6949
7022
 
sourcecode/risk.py CHANGED
@@ -1244,7 +1244,8 @@ class RiskComposer:
1244
1244
  progress: "Optional[Callable[..., Any]]" = None,
1245
1245
  ) -> None:
1246
1246
  from sourcecode.context_cache import shared_cir
1247
- from sourcecode.repository_ir import extract_java_endpoints, find_java_files
1247
+ from sourcecode.repository_ir import find_java_files
1248
+ from sourcecode.canonical_ir import project_endpoint_surface
1248
1249
  from sourcecode.security_posture import (
1249
1250
  endpoint_security_surface,
1250
1251
  infer_security_posture,
@@ -1420,7 +1421,7 @@ class RiskComposer:
1420
1421
  _inside(
1421
1422
  "indexing the validation surface",
1422
1423
  lambda: _constraint_index(
1423
- self.root, self.cir, extract_java_endpoints(self.root)
1424
+ self.root, self.cir, project_endpoint_surface(self.cir)
1424
1425
  ),
1425
1426
  )
1426
1427
  if _named("indexing the validation surface") else None
@@ -294,6 +294,8 @@ SCHEMAS: "tuple[SchemaEntry, ...]" = (
294
294
  SchemaEntry("ask-run-v1", "one recorded run", "runs.py"),
295
295
  SchemaEntry("java-lts-inventory-v1", "the Java LTS inventory in a migration report",
296
296
  "migrate_check.py", emitted_by=("migrate-check",)),
297
+ SchemaEntry("jvm-inventory-v1", "local JVM ecosystem evidence in a migration report",
298
+ "jvm_inventory.py", emitted_by=("migrate-check",)),
297
299
  SchemaEntry("facts-v1", "the fact registry: one fact, one authority (ADR-0008 R11)",
298
300
  "registry.json", note="a registry rather than a payload shape; the "
299
301
  "document itself, under `facts/`"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 5.9.18
3
+ Version: 5.9.22
4
4
  Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/HarounDominique/sourcecode
6
6
  Project-URL: Repository, https://github.com/HarounDominique/sourcecode
@@ -59,7 +59,7 @@ Description-Content-Type: text/markdown
59
59
  > question. All analysis is local and deterministic; see [Privacy](#configuration--privacy)
60
60
  > before inspecting a repository whose telemetry policy you need to control.
61
61
 
62
- ![Version](https://img.shields.io/badge/version-5.9.18-blue)
62
+ ![Version](https://img.shields.io/badge/version-5.9.22-blue)
63
63
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
64
64
 
65
65
  Copyright (c) 2026 Dominique Haroun. All rights reserved. ASK Engine 5.7.0 and
@@ -196,7 +196,7 @@ set `ASK_MAX_RSS_MB=<megabytes>`; if the process exceeds it, ASK refuses the
196
196
  answer with `MEMORY_TOO_LARGE`, including the observed peak and limit. Leave it
197
197
  unset, or set it to `0`, to disable the ceiling explicitly.
198
198
 
199
- **Current performance status.** Release 5.9.17 retains the shared Canonical IR
199
+ **Current performance status.** Release 5.9.22 retains the shared Canonical IR
200
200
  path used by `validation` and now passes that same CIR into labelled `data-exposure`
201
201
  runs; the no-label path remains parse-free. These are implementation properties,
202
202
  not duration promises. It also removes two repeated walks of the same file tree in
@@ -221,6 +221,12 @@ there. Use `ask cache model <repo>`, `--progress stderr`, `audit-report
221
221
  [current reliability queue](docs/DEVELOPMENT-ROADMAP.md#external-audit-intake--aud-616-re-audit-release-5910)
222
222
  before treating a historical timing or bounded flag as a current guarantee.
223
223
 
224
+ A post-remediation re-audit of `5.9.18` also found a separate `pack gate --since`
225
+ regression on Windows: the git-ref materialization path could resolve to Python
226
+ 3.10 and fail in `tarfile` before the stderr-forwarding fix mattered. Release
227
+ `5.9.19` adds the version-compatible extraction path; native Windows repetition
228
+ is still required before treating `pack gate` as production CI evidence.
229
+
224
230
  **CI delivery and workspace licensing.** `migrate-check --format sarif` emits
225
231
  ASK-owned migration findings with source locations and provenance; it does not
226
232
  re-emit imported scanner findings. `ask auth workspace <repo>...` previews the
@@ -236,11 +242,14 @@ entitlement errors in CI and does not change per-repository gates or billing.
236
242
 
237
243
  Release 5.9.17 also carries the boundary-contract hardening: missing consumer repositories,
238
244
  malformed CVE feeds and unresolved `pack gate --since` refs fail as structured
239
- input errors; duplicate same-commit baselines are never overwritten. `posture
240
- --diff` validates unknown profiles when the repository declares profiles. In
241
- `endpoints --compact`, an empty `endpoints` list is explicitly marked as an
242
- intentional omission, not an empty API; unresolved `impact` and `impact-chain`
243
- queries exit non-zero, including on repositories without Java sources.
245
+ input errors; duplicate same-commit baselines are never overwritten. That
246
+ contract is still not enough to make `pack gate` CI-safe on Windows, because the
247
+ materialization path can still hit a Python 3.10 tarfile mismatch in mixed
248
+ environments. `posture --diff` validates unknown profiles when the repository
249
+ declares profiles. In `endpoints --compact`, an empty `endpoints` list is
250
+ explicitly marked as an intentional omission, not an empty API; unresolved
251
+ `impact` and `impact-chain` queries exit non-zero, including on repositories
252
+ without Java sources.
244
253
 
245
254
  **What a warm actually covers.** `ask cache warm` rebuilds the declared root views and their
246
255
  shared structural layers (L1/L2 + the Repository Intelligence Snapshot + the shared Canonical
@@ -267,11 +276,11 @@ brew tap haroundominique/sourcecode && brew install sourcecode
267
276
  # pip / pipx
268
277
  pipx install sourcecode # or: pip install sourcecode
269
278
 
270
- ask version # ask 5.9.18 — and, on a build that has aged,
279
+ ask version # ask 5.9.22 — and, on a build that has aged,
271
280
  # how many releases have probably shipped since
272
281
  ```
273
282
 
274
- > **Package vs. command.** The install package is named `sourcecode` this release
283
+ > **Package vs. command.** The install package is named `sourcecode` in this release
275
284
  > (renaming the distribution is a separate, breaking change). Installing it gives you the
276
285
  > canonical **`ask`** command plus the deprecated **`sourcecode`** alias.
277
286
 
@@ -1,6 +1,6 @@
1
- sourcecode/__init__.py,sha256=S8oxgYH-Q4cv0cFrN88Z1pyCthtwbhP0i_nWVVBKMdo,309
1
+ sourcecode/__init__.py,sha256=rlhdFHgHxU-_wnswNtAc3jkUHnsGhX1taqMHDQJ5P6c,309
2
2
  sourcecode/__main__.py,sha256=qDFVkobwsdZrX0B3stiRqsUNivVpybKO-egzAwCW4xw,165
3
- sourcecode/_build_commit.py,sha256=vnTeW0cnEL6s4BeFQFFObMw4rIAwHS5kiQCKUC8bxcE,11638
3
+ sourcecode/_build_commit.py,sha256=h33fClasWi1xoqBmi7TKbd8bGOaY-_NiL3paWe84zFg,11638
4
4
  sourcecode/access_drift.py,sha256=uYD8ipiEAaTnkz0IhG6ujU_KI3LghdY-6ycurs-BZnc,9355
5
5
  sourcecode/adaptive_scanner.py,sha256=yJBKjNpkY6bpueYJ2YnRezen3sYZDecEt7WaaNWdqug,9466
6
6
  sourcecode/analysis_budget.py,sha256=ZuNEboPF8A6GK-_V7WNt92ezgriga7_Rbr-vvSN4UEE,4183
@@ -29,13 +29,13 @@ sourcecode/cache_observation.py,sha256=5E2BeqiYE5wBCbvDglkc5A9fTNhXkT_OhcDuuCqRu
29
29
  sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
30
30
  sourcecode/caller_metrics.py,sha256=zGc3sHRRwEJgAGmyfqEiH69mD9w5rOqMUhzby8Vmm_w,12599
31
31
  sourcecode/caller_reach.py,sha256=M7Ffs5CtlimihfdSy3HtC4WpDP97bRFX15QYOlOfMJw,16626
32
- sourcecode/canonical_ir.py,sha256=nvOWn916bSEfqd7qbztLXzyyhTXR6l8HRbghhOBLrKI,38381
32
+ sourcecode/canonical_ir.py,sha256=KcbR6Vnmo8UbwSbIn6RDAGdNtfPoxz1c_0IVCaoIuss,38779
33
33
  sourcecode/chain_rules.py,sha256=Bi6UHfgd-GxWswmnHRcPz5jdbAuqka3Zkz_P-MTvqhw,12767
34
34
  sourcecode/change_plan.py,sha256=drAoDsqFplutCLc6Y0xue7sYkTls0SpYh7aOXvttv3I,12934
35
35
  sourcecode/ci_output.py,sha256=X4NRokWWMRF9Ex8OQrEO2g04GzJ00GDP5zYIoqI9YCc,4625
36
36
  sourcecode/cir_graphs.py,sha256=RytqSv1WF8kdlfXj2fPQz16LY4akS2kyThbdWL8beCQ,14835
37
37
  sourcecode/classifier.py,sha256=ft1RGPIChvO84q2QMrEH93x1J15AXNWMh26lnxvoRao,26476
38
- sourcecode/cli.py,sha256=9PJjZid-sozU1bsnP12569hdIWouB4u5VE0kZJBWYhw,956514
38
+ sourcecode/cli.py,sha256=2d-S0OQQnoF8bHll_MfIuFUSyAVtRl_Nopb1WEBd8Vo,956694
39
39
  sourcecode/click_runtime.py,sha256=K2cUdzFhbd6uEUpucWptXE89_z-ZNZGsqgd-34usUXk,1890
40
40
  sourcecode/client_calls.py,sha256=YOPnfRZzRyqmdHvXSVeiunW8cuiuCuWXacNIQRgKMCw,22640
41
41
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
@@ -45,7 +45,7 @@ sourcecode/constraint_diff.py,sha256=hWIO5vsxvHjsgQx23K3r_JMy1W6wQ2Ca4DqB9R_iMnY
45
45
  sourcecode/consumer_join.py,sha256=ERwx9SOu0ZE3NgCBYvB0l67iDLnWMdMpVhlCmbA_3Sw,19618
46
46
  sourcecode/container_wiring.py,sha256=207Oh4ugGiF0JN-toOCfqTEY2S-30zzZE2r1o0K0Cpo,27511
47
47
  sourcecode/context_cache.py,sha256=lIjEGtoZy2eFDvYUVGbG3yhzlIlpdQd-Q5MldXwi0dA,36787
48
- sourcecode/context_graph.py,sha256=a6v2lgl2DxDnPAWE59K3yg6L5YFDak0LIUff3pi9p0c,47327
48
+ sourcecode/context_graph.py,sha256=-l7XHL7m3GRPXLUps5Be7lSCAQWsQMyA8lR__ejJbr8,47726
49
49
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
50
50
  sourcecode/context_summarizer.py,sha256=8a2chqDFvosLrfB2RnK96cnyDMOiuOpi4oK9Rm8Nsfc,6878
51
51
  sourcecode/contract_diff.py,sha256=pOvjLm-2YQ4KWD8G9HsPCs6Lkofkk2e2wve_y_QXjtU,10990
@@ -88,7 +88,7 @@ sourcecode/fqn_utils.py,sha256=XLU7zDkNBXz_RZkIUNfpPmp1nekWtqP-fxV92tDV1vg,2158
88
88
  sourcecode/functional_routes.py,sha256=KXMrDoBwj_GC80w10FvgA3zddIGE1iW9KjpDtCllSSs,16813
89
89
  sourcecode/gate_anchors.py,sha256=26v8wUtzOk1K1Un5uXdUxAu20mqSPX3jo82zuM05ByE,2066
90
90
  sourcecode/git_analyzer.py,sha256=irvEwddbd5_tbA_eYUZebf4mH7_nFhpjwaWAV1f7jUY,15755
91
- sourcecode/git_checkout.py,sha256=pYLQBMKHh43crR_XNRXux86pJ_kqS_mgnXlIsz7ItcI,6024
91
+ sourcecode/git_checkout.py,sha256=aY2lxHfKb5cWcyXGoOrg1dXSH47LGlFBkTcrQgkUPgk,6352
92
92
  sourcecode/github_diff.py,sha256=H1woYKuCjb6S9sofBPpMlOpn0L4LoVxuVP7ddnhsNaA,3848
93
93
  sourcecode/graph_analyzer.py,sha256=Gw2MzLp0P990TGx6D6Ie-WIPnjWVE6X6dNBQnoM9xPU,65857
94
94
  sourcecode/graph_evidence.py,sha256=rENNsYRZeNstX_ExNCLlbHJAruFQwxo5d00x6wO3xwI,15030
@@ -97,11 +97,12 @@ sourcecode/identity_fallback.py,sha256=uRC_j_80uaI4Ue4tf-OhKX4h472dmufYcjEFGSxY2
97
97
  sourcecode/integration_coordinates.py,sha256=rvuApnS7WdRUbeQLg_oKj-Gi0koO2RKdw6HenXNgrDo,12443
98
98
  sourcecode/intent_router.py,sha256=hC_o0VYfSjzSXecFkpiD7vzFgvt44YAw6E_jrMeGiZ4,15555
99
99
  sourcecode/jdk_exports.py,sha256=fCrlwNAXUT9gge_joq6kMnY3zJxYB2pxqy-0w3o3MJI,874
100
+ sourcecode/jvm_inventory.py,sha256=06mlOWSwfs9jBk4lGkCSZHRbapOG41dQ5gopvOVf7dE,14182
100
101
  sourcecode/license.py,sha256=eIEuIVQXywl_S9LOIACOEpJ4M5RyaPvWrqg_jSNOrXA,31088
101
102
  sourcecode/manifest_cache.py,sha256=qoBn7hPeaMjLHi3higfxsjo7oS7noommRqDnpKP0Ya8,4124
102
103
  sourcecode/mcp_nudge.py,sha256=beNjfyb7z09T1rnS4t6Q4qfbLzZQNaP6ddCA4XpmV0s,3256
103
104
  sourcecode/metrics_analyzer.py,sha256=gLoRWKygF18jLgwsqmGXSWopw-f5iO1VbM7Jjdif6ag,22809
104
- sourcecode/migrate_check.py,sha256=BJj_nTnZ0fAIh5X_buL9SR2i5YdilQR3a2mkTgJFwJc,171075
105
+ sourcecode/migrate_check.py,sha256=tUfTpsMUv06JaKpHAbW_TX3ejO_L4iugYCb__lidq4U,171476
105
106
  sourcecode/migration_apply.py,sha256=36E9IUq7RbpGVt6RZ8PlSZKgDoEMHorZrXw6UVWPnpw,8692
106
107
  sourcecode/migration_blast.py,sha256=OPryWkM6-PbvcG3eJFdcWUnlpXBje41nu2BkrZnE2hg,9359
107
108
  sourcecode/non_coverage.py,sha256=Tl04OemPnxnOuWISMF6gAhDYx-P0LP6_l-PbpyvBJb4,30634
@@ -114,7 +115,7 @@ sourcecode/output_encoding.py,sha256=OxiZQtr7uWBDUtblNQviNHsTH1Xo9AZEpsegPlQWKFk
114
115
  sourcecode/output_sink.py,sha256=Eya-PnXxqc3RGulJoLhuC9LUF73SZD0aO5O2B0xAMXU,8762
115
116
  sourcecode/pack_manifest.py,sha256=Qm6et6SQsFCLc-HJ-Uhz1XlcOa77ZDHgks4JVDEENss,20820
116
117
  sourcecode/packs.py,sha256=Oewt8fOkxvr1wwZJc_SOjc6sP_9cmKUzi-WOyVLmb7k,51344
117
- sourcecode/parallel.py,sha256=xzaOOBmqKjbVNwdV2VHXXrWK57wd5sLYhhYRAnKVcdE,13762
118
+ sourcecode/parallel.py,sha256=tXh5TYJMDD7FDwx1uS4kMB1jTWTvHgCC-HCYJllnwls,13173
118
119
  sourcecode/parse_cache.py,sha256=BaPzh-KygmY4OpJQm2YGh6PWztYsj58vHWTu2eSF4s8,38769
119
120
  sourcecode/partial_contract.py,sha256=FZUBACkClvRAmPeYVjxNWeQmP6Y22KW4fPeeTfmT80A,5951
120
121
  sourcecode/path_admission.py,sha256=5IBfmHvOrSwrVLlXXwj9KAEg-ciVaHYHjcpAS8mriPg,5898
@@ -139,7 +140,7 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
139
140
  sourcecode/reference_facts.py,sha256=mx7H-Ax5y8sKAQHrnJCXlgjkUzqZtQTByv9Zy2lxKQQ,15500
140
141
  sourcecode/regress.py,sha256=6rlFP2rsIzVKAA0spJ66To528GyBtnRI2Zg9vAGq8kQ,13264
141
142
  sourcecode/regulatory_pack.py,sha256=xVWd8hYg6MjGfkFP8UWcYeLmNMxEk8k2uSbTcnGMbhg,3421
142
- sourcecode/release_info.py,sha256=YKKYdufxyP8x0b4RvmWmnR-Rcm_8lCHn0D-x_BI1irA,5918
143
+ sourcecode/release_info.py,sha256=Z6QP0_dEiEOHdrThbQQqFisSjBj_tku0pjjUyKXXsjM,5918
143
144
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
144
145
  sourcecode/remediation_diff.py,sha256=fGYqPmQF7bO54L8ivztX5PgmW4fVu54gNcG6OEQ192U,9730
145
146
  sourcecode/remediation_flow.py,sha256=OFlrNCD2dKw1WlX5cHqVl1yrQZHi2TjCThH8Q3X1PwI,10294
@@ -147,10 +148,10 @@ sourcecode/remediation_report.py,sha256=z28x-u618feU2d7ol_FW7IfhUOq6iETCfdpl7zJC
147
148
  sourcecode/remedies.py,sha256=V2_cD4pLq8bKRkzF-3RGFoiu1xWYZS384WDVp1MrlZw,10225
148
149
  sourcecode/rename_refactor.py,sha256=oASZIf7TxcOBy3RQ0FuOe-4HlNW_JOu9RW9JqCxGNBE,17225
149
150
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
150
- sourcecode/repository_ir.py,sha256=xW3ahhhejolAaQL-EMEjOPYmrxhEx5S_3BNGxTEX56U,523074
151
+ sourcecode/repository_ir.py,sha256=GLYSuPuFqwWdIUtZsUNFctl5DgUaOsDKEK_rwnS-F9g,526567
151
152
  sourcecode/resource_budget.py,sha256=Bo-XMvVK9XyHY_S06fVirZrhXgwmnr3XI38-Li55NWY,7767
152
153
  sourcecode/ris.py,sha256=HsLVHZfseEHNqMyxrj8SvrzUK7jE_85lsWjHtsEw9ik,29196
153
- sourcecode/risk.py,sha256=98MhXPM4unAJZFTKKEeAbEjLq4NZ0Wy-66AHQfA3x7c,102506
154
+ sourcecode/risk.py,sha256=rG7fqDIjyhtvB0Qje0d2ZEUl5kemMBwbuOJG8ZJxqrQ,102552
154
155
  sourcecode/risk_model.py,sha256=RoC-G15qZP5BLEwwmcr4eJ0APgvIbh1WTqhTVT-5YFE,10895
155
156
  sourcecode/rule_catalog.py,sha256=QCx6ToFTMCtDt5uuX1uBMLuw3b9Cd92gGRsX34kud4U,5328
156
157
  sourcecode/rule_pass.py,sha256=DTNxD-nAecMJhIzI1LCd9FyRumInQSZjitacNh4Zoc8,14671
@@ -161,7 +162,7 @@ sourcecode/sarif_emit.py,sha256=miFi_DiN58UZyId7DlYzcv-eYLFAA8xdX1AGpoiF4XQ,9353
161
162
  sourcecode/scanner.py,sha256=z3CV0rcGunu0Y8mpNgp07wI7nxT0pxw1BkXRRtI0Rpo,9609
162
163
  sourcecode/schema.py,sha256=61Ux_qRITs_orcrZM20ctV2k1ZfzauqqMgJlZJYAEbE,25087
163
164
  sourcecode/schema_producers.py,sha256=4XzW4wZXnFHOaJT7XenirXGCo0pPZhkdhb7zcDtqFnM,10390
164
- sourcecode/schema_registry.py,sha256=4PkGSP5PK9AxN_s8fy5zpVNINa4BG-RjJqKrCsr19r4,28871
165
+ sourcecode/schema_registry.py,sha256=UYyHwtruOk6TThyf-SXw36n_qFZAOJF-IyuMulWrT6I,29029
165
166
  sourcecode/security_chain.py,sha256=1YZVeFLwy65kntMBhMNvz1cUpOoKCmvEqYICgxs66Gw,6823
166
167
  sourcecode/security_config.py,sha256=LUppRDYQK0oKM4qdEOLObaeU3frFTS-paEZo7dMgtY8,5467
167
168
  sourcecode/security_config_scan.py,sha256=LU8gfNUBB6wjKR8jkK-qRKtUrbkHZ301_Xsk6hSp4XU,35916
@@ -274,13 +275,13 @@ sourcecode/telemetry/consent.py,sha256=-Wb8BFGXEexOs8M0Eja7F-T6ZtLNPeRoZnHcHvlP7
274
275
  sourcecode/telemetry/events.py,sha256=l3Z2SQUO4_C2D2ItXOOdVNimAmHedUHnUxqYFTxnsAE,2900
275
276
  sourcecode/telemetry/filters.py,sha256=-lqT2oorECuqEIdeHb--7AZ6BCaUx0bya720a-Z5x6w,7335
276
277
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
277
- sourcecode/_docs/DEFECT-LEDGER.md,sha256=X4PwvNFYNDxjSIiHPP0si8b3YnE6bX_0MM3DMTKA8PE,1167330
278
+ sourcecode/_docs/DEFECT-LEDGER.md,sha256=27Td56IIsaXPz9udDpLMiMDP4RMeaQTh5-FUqucpee4,1167577
278
279
  sourcecode/_docs/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
279
280
  sourcecode/_docs/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
280
- sourcecode/_docs/USER_GUIDE.md,sha256=0pScky3oj00MBUgXFA21hZ-TRDVFhATbL3itSIs4-Ro,107847
281
- sourcecode-5.9.18.dist-info/METADATA,sha256=eSKyx6_6LTKfEgaxVj_mV4UBVS0FI2_hJv4rTE1110g,66101
282
- sourcecode-5.9.18.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
283
- sourcecode-5.9.18.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
284
- sourcecode-5.9.18.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
285
- sourcecode-5.9.18.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
286
- sourcecode-5.9.18.dist-info/RECORD,,
281
+ sourcecode/_docs/USER_GUIDE.md,sha256=A__FLktxJ8S-VMUAQ5OxPRfUkKQ7yKy6FQbWxiZvc6c,108275
282
+ sourcecode-5.9.22.dist-info/METADATA,sha256=Plv_fedlIoI6bKsHB5Q5zFlOlC_nK-KN7gLa34KVsb8,66673
283
+ sourcecode-5.9.22.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
284
+ sourcecode-5.9.22.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
285
+ sourcecode-5.9.22.dist-info/licenses/LICENSE,sha256=23-fk61oSWGVmkWyka-FIM0rhAPM2hcYHnsaridL8Uo,1859
286
+ sourcecode-5.9.22.dist-info/licenses/NOTICE,sha256=Br-AA8Y3iJo1WXa5uP8QBpzqI5nDu3iGLbFyvDt3BOs,321
287
+ sourcecode-5.9.22.dist-info/RECORD,,