sourcecode 5.8.25__py3-none-any.whl → 5.8.26__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.
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.8.25"
7
+ __version__ = "5.8.26"
@@ -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 = 'b6e0e0f157687a962ce66627afe2e9b388fb2f06'
3
+ BUILD_COMMIT = 'e2da5d84a30bc2e8c60325e1e2d48a8d736b8ad7'
4
4
  CLOSURE_PROVENANCE = {'Window': {'commit': '', 'status': 'uncited'}, '`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'}, '`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': 113, 'present': 88, 'absent': 0, 'unresolved': 0, 'uncited': 25, 'unknown': 0}
@@ -16,6 +16,75 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
16
16
 
17
17
  ## Current Synchronization
18
18
 
19
+ **Corpus ampliado recibido, 2026-08-23 — 17 hallazgos nuevos en intake.**
20
+ [`AUDIT-2026-08-23-CORPUS-AMPLIADO.md`](AUDIT-2026-08-23-CORPUS-AMPLIADO.md) registra cuatro
21
+ P0 (`AUD-CA-001`, `002`, `003`, `014`), ocho P1, cuatro P2 y un P3. La evidencia externa
22
+ aporta reproducción y contraste manual, pero estas filas permanecen **pendientes de
23
+ reproducción contra checkout/wheel**: no se declaran corregidas ni se atribuyen al código
24
+ auditado. La prioridad inmediata es seguridad no modelada, colisiones de FQN y superficie
25
+ HTTP Micronaut; después siguen contratos de inventario, cobertura y precisión de salida.
26
+
27
+ **Correcciones iniciadas, 2026-08-23.** `AUD-CA-001` (declaración segura de Shiro),
28
+ `AUD-CA-005` (autoridad de `src/main`), `AUD-CA-013` (sobrecargas y anclas de
29
+ `explain`) y `AUD-CA-017` (contador estructurado) están corregidos en commits atómicos
30
+ de `5.8.25`. `AUD-CA-014` tiene una corrección parcial: Micronaut ya aparece como
31
+ superficie detectada-no-modelada y cuantificada, pero el soporte HTTP sigue pendiente.
32
+ Los restantes hallazgos permanecen abiertos hasta reproducirlos y cubrirlos con pruebas.
33
+
34
+ **Estado autoritativo del repositorio, 2026-08-23 — `5.8.25`, `HEAD 3d56d11`.** Este
35
+ snapshot supersede cualquier estado de release indicado en las secciones históricas de
36
+ este ledger. La batería completa pasa en verde cuando se configuran ambos almacenes
37
+ temporales: `SOURCECODE_CONTEXT_CACHE_DIR` para CIR/parse y `SOURCECODE_CACHE_DIR` para
38
+ snapshots/RIS. Usar sólo el primer directorio produce falsos fallos de persistencia.
39
+
40
+ Los commits `125e486`, `735d766`, `434d0e2`, `cd3f8f1`, `e9f5861`, `49187f9` y `3d56d11` están incluidos
41
+ en el checkout actual. Cubren rutas
42
+ Windows, selftest con `ask.exe`, resolución bounded de comandos y flags, fallback de
43
+ contexto, rutas POSIX del workspace, salida estable del job Windows, fingerprint sin estado
44
+ runtime de caché, sweep final del parse cache, gate CI de migraciones, trazabilidad explícita
45
+ de benchmarks históricos y aislamiento de observabilidad por invocación. Por tanto,
46
+ `ASK-UX-001`, `ASK-UX-002` y `ASK-ENV-001` ya no deben describirse como fallos sin
47
+ corrección: quedan **pendientes de verificación contra el wheel publicado y de confirmación
48
+ en GitHub Windows**.
49
+
50
+ La cola abierta actual es: validación externa de `ASK-DET-002`, `B-26` y `ASK-GATE-001`,
51
+ regeneración reproducible de cifras `B-11`, `ASK-PERF-003` (medición aislada aún
52
+ necesaria), `B-13`/`B-14` (reconciliación contra artefacto exacto), y la deuda P3 de filas
53
+ sin cita y `B-30`. No quedan fallos locales reproducidos pendientes en esta batería.
54
+
55
+ Las menciones posteriores a `5.8.24`, “release/commit pending” o “no audit, no release”
56
+ son fotografías históricas de sus respectivas iteraciones. Se mantienen para trazabilidad,
57
+ pero no representan el estado vigente ni deben usarse para decidir el siguiente release.
58
+
59
+ **External `5.8.25` regression audit received, 2026-08-23 — triage recorded.**
60
+ [`AUDIT-2026-08-23-EXTERNAL-5.8.25.md`](AUDIT-2026-08-23-EXTERNAL-5.8.25.md) separates the
61
+ Windows/MSAS wheel evidence from this checkout. The only new confirmed product defect is
62
+ `ASK-DET-002`: root `content_id` changes between cold and warm runs because `_cache` runtime
63
+ state is included in the answer fingerprint. `ASK-UX-001`/`ASK-UX-002` and
64
+ `ASK-GATE-001` remain actionable contract residues in the audited artefact; `ASK-ENV-001`
65
+ is a Windows-specific witness still requiring reproduction. `B-26` is recorded as a fleet
66
+ capacity issue (559.48 MB against 512 MB), not as a finding-analysis error. The reported
67
+ performance deltas are not accepted as regressions until isolated measurements control cache
68
+ state and concurrency. `B-13`/`B-14` require reconciliation against the exact wheel and
69
+ repository pair because the source re-audit disagrees with the external field result.
70
+
71
+ The validated commercial queue is now: deterministic/provenance envelope, bounded UX and
72
+ hint correctness, coherent CI gates, Windows envelope verification, then PR semantic gate,
73
+ bounded agent context, Java/Spring migration inventory, HTTP exposure provenance and SARIF
74
+ enrichment. The complete CTO assessment and acceptance criteria are in the linked audit;
75
+ they are not a claim that every auditor suggestion is a next-iteration fix.
76
+
77
+ **ThingsBoard field audit, 2026-08-23 — five findings documented and corrected in the
78
+ working tree; included in `5.8.25` and commit `2c551db`.**
79
+ [`AUDIT-2026-08-23-THINGSBOARD-FINDINGS.md`](AUDIT-2026-08-23-THINGSBOARD-FINDINGS.md)
80
+ records the evidence and acceptance tests for the missing negative scope (`AUD-THB-001`),
81
+ TX-006 precision (`AUD-THB-002`), ambiguous `none_detected` exposure semantics
82
+ (`AUD-THB-003`), incomplete evidence bounds in the generated report (`AUD-THB-004`) and
83
+ contradictory migration summary wording (`AUD-THB-005`). The audit found no runtime
84
+ regression, crash, write leakage or mutation of the audited checkout. The five authorities
85
+ and focused regression tests were updated and released; these rows are product gaps/defects, not
86
+ claims that ThingsBoard itself is broken.
87
+
19
88
  **`AUD-601` queue closed, 2026-08-23 — seven rows, one commit each, no release.** Six closed
20
89
  and one (`AUD-601-X01`) held with the half it was owed. Suite **12 702 / 0 red**,
21
90
  `__version__` unchanged at `5.8.24`. ⚠ **Three more reported mechanisms are corrected by the
@@ -22,6 +22,36 @@ from sourcecode.partial_contract import floor_counts, read_count
22
22
  AUDIT_REPORT_SCHEMA = "audit-report-v1"
23
23
 
24
24
 
25
+ def _evidence_bounds(
26
+ *, root: Path, profiles: Optional[set[str]], risk: dict[str, Any], posture: dict[str, Any]
27
+ ) -> dict[str, Any]:
28
+ """Publish the limits beside every bundled executive conclusion."""
29
+ identity = repository_identity(root)
30
+ non_coverage: list[Any] = []
31
+ for payload in (risk, posture):
32
+ block = payload.get("non_coverage") if isinstance(payload, dict) else None
33
+ if isinstance(block, dict):
34
+ non_coverage.extend(block.get("items") or [])
35
+ elif isinstance(block, list):
36
+ non_coverage.extend(block)
37
+ partial = bool(risk.get("partial") or risk.get("_partial"))
38
+ return {
39
+ "status": "partial" if partial else "bounded",
40
+ "repository": identity,
41
+ "commit": identity.get("git_head") or identity.get("commit") or "unknown",
42
+ "tree_state": identity.get("tree_state") or identity.get("git_status") or "unknown",
43
+ "profiles": sorted(profiles) if profiles else ["default"],
44
+ "scope": "risk + Spring posture evidence packaged by audit-report",
45
+ "unsupported_surfaces": list(dict.fromkeys(str(item) for item in non_coverage)),
46
+ "partial": partial,
47
+ "claim_rule": (
48
+ "absence from a partial, unresolved or unsupported population is not "
49
+ "evidence of absence; only explicitly modeled and complete populations "
50
+ "may support an exclusion"
51
+ ),
52
+ }
53
+
54
+
25
55
  def _canonical_json(data: dict[str, Any]) -> bytes:
26
56
  return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
27
57
 
@@ -121,6 +151,9 @@ def build_audit_report(
121
151
  "non_coverage": (risk.get("non_coverage") or {}).get("items", []),
122
152
  },
123
153
  }
154
+ unsigned["evidence_bounds"] = _evidence_bounds(
155
+ root=root, profiles=profiles, risk=risk, posture=posture
156
+ )
124
157
  # C3-89. A bundle whose evidence was cut says so **before** it is signed, and
125
158
  # says it where a reader of the summary sees it — a signature over a truncated
126
159
  # payload that does not declare the truncation is the worst artifact this
@@ -263,6 +296,16 @@ def render_markdown(report: dict[str, Any]) -> str:
263
296
  "## Limits",
264
297
  "",
265
298
  str((report.get("limits") or {}).get("statement", "")),
299
+ "",
300
+ "## Evidence Bounds",
301
+ "",
302
+ f"- Status: `{(report.get('evidence_bounds') or {}).get('status', 'unknown')}`",
303
+ f"- Commit: `{(report.get('evidence_bounds') or {}).get('commit', 'unknown')}`",
304
+ f"- Tree state: `{(report.get('evidence_bounds') or {}).get('tree_state', 'unknown')}`",
305
+ f"- Profiles: `{', '.join((report.get('evidence_bounds') or {}).get('profiles') or [])}`",
306
+ f"- Unsupported surfaces declared: `{len((report.get('evidence_bounds') or {}).get('unsupported_surfaces') or [])}`",
307
+ "",
308
+ str((report.get("evidence_bounds") or {}).get("claim_rule", "")),
266
309
  ])
267
310
  signature = report.get("signature")
268
311
  if isinstance(signature, dict):
@@ -52,6 +52,11 @@ ENV_FLAG: str = "ASK_BASELINE_AUTOCAPTURE"
52
52
  _TRUE = frozenset({"1", "true", "yes", "on"})
53
53
  _FALSE = frozenset({"0", "false", "no", "off"})
54
54
 
55
+
56
+ def _published_path(path: Path) -> str:
57
+ """Serialize a filesystem path with the stable separator used by payloads."""
58
+ return path.as_posix()
59
+
55
60
  # Actions.
56
61
  CAPTURED: str = "captured"
57
62
  SKIPPED: str = "skipped"
@@ -408,7 +413,7 @@ def decide(
408
413
  if already_captured is not None:
409
414
  return CaptureDecision(
410
415
  SKIPPED, COMMIT_ALREADY_CAPTURED,
411
- baseline_file=str(already_captured), **common
416
+ baseline_file=_published_path(already_captured), **common
412
417
  )
413
418
  return CaptureDecision(
414
419
  CAPTURED, HISTORY_PRESENT if history_exists else EXPLICITLY_ENABLED, **common
@@ -498,12 +503,12 @@ def run(
498
503
  except Exception as exc: # a side effect never decides the host command's verdict
499
504
  return CaptureDecision(
500
505
  SKIPPED, WRITE_FAILED,
501
- history_dir=str(hist), commit=plan_.commit,
506
+ history_dir=_published_path(hist), commit=plan_.commit,
502
507
  history_size=plan_.history_size, error=f"{type(exc).__name__}: {exc}",
503
508
  )
504
509
  return CaptureDecision(
505
510
  CAPTURED, plan_.reason,
506
- history_dir=str(hist), commit=plan_.commit,
507
- baseline_file=str(written),
511
+ history_dir=_published_path(hist), commit=plan_.commit,
512
+ baseline_file=_published_path(written),
508
513
  history_size=(plan_.history_size or 0) + 1,
509
514
  )
sourcecode/cache_model.py CHANGED
@@ -745,10 +745,9 @@ def field_anchor_note(row: "Optional[CommandCache]") -> "Optional[str]":
745
745
  return "build not recorded"
746
746
  if measured_on == __version__:
747
747
  return None
748
- # Short on purpose: this rides along every `here:` line and every row of
749
- # `cache model`, and the build is the whole fact. The sentence that explains
750
- # what an older build means is `field_currency()['statement']`, printed once.
751
- return f"on {measured_on}"
748
+ # Keep the warning local to every advice line: consumers must not mistake a
749
+ # historical measurement for a current release benchmark.
750
+ return f"historical; on {measured_on}; re-measure before using as a current benchmark"
752
751
 
753
752
 
754
753
  def field_subject(row: "CommandCache") -> str:
sourcecode/cli.py CHANGED
@@ -534,7 +534,6 @@ def _resolve_bounded_invocation(invocation: str) -> "tuple[str, bool]":
534
534
  arrives with a first-look count and the line is still something to paste.
535
535
  """
536
536
  try:
537
- import click
538
537
  import typer.main as _typer_main
539
538
 
540
539
  from sourcecode.output_ceiling import _BOUNDING_FLAGS, as_typed
@@ -543,9 +542,10 @@ def _resolve_bounded_invocation(invocation: str) -> "tuple[str, bool]":
543
542
  node: Any = _typer_main.get_command(app)
544
543
  consumed = 0
545
544
  for token in tokens:
546
- if not isinstance(node, click.Group):
545
+ commands = getattr(node, "commands", None)
546
+ if not isinstance(commands, dict):
547
547
  break
548
- child = node.commands.get(token)
548
+ child = commands.get(token)
549
549
  if child is None:
550
550
  break
551
551
  node, consumed = child, consumed + 1
@@ -571,7 +571,6 @@ def _declares_a_bound(invocation: str) -> bool:
571
571
  print belongs with the ones the reader has to plan for.
572
572
  """
573
573
  try:
574
- import click
575
574
  import typer.main as _typer_main
576
575
 
577
576
  from sourcecode.output_ceiling import _BOUNDING_FLAGS
@@ -579,9 +578,10 @@ def _declares_a_bound(invocation: str) -> bool:
579
578
  node: Any = _typer_main.get_command(app)
580
579
  consumed = 0
581
580
  for token in invocation.split():
582
- if not isinstance(node, click.Group):
581
+ commands = getattr(node, "commands", None)
582
+ if not isinstance(commands, dict):
583
583
  break
584
- child = node.commands.get(token)
584
+ child = commands.get(token)
585
585
  if child is None:
586
586
  break
587
587
  node, consumed = child, consumed + 1
@@ -2064,9 +2064,36 @@ def _active_command_context() -> "tuple[str, Optional[Path]]":
2064
2064
 
2065
2065
  ctx = click.get_current_context(silent=True)
2066
2066
  except Exception:
2067
- return "", None
2067
+ ctx = None
2068
+
2068
2069
  if ctx is None:
2069
- return "", None
2070
+ # Console scripts and embedded entry points can emit after Click has
2071
+ # released its context. Derive the command from argv rather than
2072
+ # publishing `unknown` for an otherwise valid answer.
2073
+ argv = list(sys.argv[1:])
2074
+ known: set[str] = set()
2075
+ try:
2076
+ known.update(
2077
+ str(info.name or "") for info in app.registered_commands
2078
+ if info.name
2079
+ )
2080
+ known.update(
2081
+ str(info.name or "") for info in app.registered_groups
2082
+ if info.name
2083
+ )
2084
+ except Exception:
2085
+ pass
2086
+ command = ""
2087
+ for index, token in enumerate(argv):
2088
+ if token.startswith("-") or token not in known:
2089
+ continue
2090
+ command = token
2091
+ if token in {"retrieve", "cache", "auth", "mcp", "telemetry", "baseline", "pack"}:
2092
+ if index + 1 < len(argv) and argv[index + 1] in known:
2093
+ command = f"{token} {argv[index + 1]}"
2094
+ break
2095
+ target = Path(_get_detected_path() or Path.cwd())
2096
+ return command or _ROOT_COMMAND_NAME, target
2070
2097
 
2071
2098
  names: list[str] = []
2072
2099
  node: Any = ctx
@@ -3573,7 +3600,7 @@ def _pack_output_path(
3573
3600
  """
3574
3601
  if output_path is not None or output_dir is None:
3575
3602
  return output_path
3576
- suffix = {"json": ".json", "yaml": ".yaml", "markdown": ".md"}.get(
3603
+ suffix = {"json": ".json", "yaml": ".yaml", "markdown": ".md", "sarif": ".sarif"}.get(
3577
3604
  format, ".json"
3578
3605
  )
3579
3606
  return output_dir / f"pack{suffix}"
@@ -4359,7 +4386,7 @@ def pack_gate_cmd(
4359
4386
  "--output-dir",
4360
4387
  help="Write component artifacts and, unless --output is set, the aggregate pack file.",
4361
4388
  ),
4362
- format: str = _format_option(help="Output format: json, yaml, github-actions or gitlab."),
4389
+ format: str = _format_option(help="Output format: json, yaml, github-actions, gitlab or sarif."),
4363
4390
  copy: bool = _copy_option(),
4364
4391
  output_path: Optional[Path] = _output_option(),
4365
4392
  ) -> None:
@@ -4382,6 +4409,17 @@ def pack_gate_cmd(
4382
4409
  output = (render_pack_gate_github_actions(data)
4383
4410
  if format == "github-actions" else render_pack_gate_gitlab(data))
4384
4411
  _emit_command_output(output, output_path, copy, stamp_envelope=False)
4412
+ elif format == "sarif":
4413
+ from sourcecode.sarif_emit import pack_gate_to_sarif
4414
+ _emit_command_output(
4415
+ _serialize_dict(pack_gate_to_sarif(data), "json"),
4416
+ _pack_output_path(
4417
+ None if data.get("no_write_substitution") else output_dir,
4418
+ output_path,
4419
+ format,
4420
+ ),
4421
+ copy,
4422
+ )
4385
4423
  else:
4386
4424
  _emit_command_output(
4387
4425
  _serialize_dict(data, format),
@@ -5018,6 +5056,15 @@ def main(
5018
5056
  ask /path/to/repo --compact analyze specific path
5019
5057
  ask --agent agent-optimized output (full detail)
5020
5058
  """
5059
+ # Cache observations belong to one CLI invocation. Typer's test runner and
5060
+ # embedded callers reuse the Python process, so leaving the counters alive
5061
+ # makes the next payload describe earlier lookups and can change its size.
5062
+ try:
5063
+ from sourcecode import cache_observation
5064
+
5065
+ cache_observation.reset()
5066
+ except Exception:
5067
+ pass
5021
5068
  _apply_jobs(jobs)
5022
5069
  # A root option so the ceiling is one policy rather than sixty flags, and an
5023
5070
  # environment variable so it survives into a subcommand's own parse: Click
@@ -11283,7 +11330,7 @@ def spring_audit_cmd(
11283
11330
  help="Write output to a file instead of stdout.",
11284
11331
  ),
11285
11332
  format: str = _format_option(
11286
- help="Output format: json (default), yaml, github-comment, or github-actions."
11333
+ help="Output format: json (default), yaml, github-comment, github-actions, or sarif."
11287
11334
  ),
11288
11335
  copy: bool = _copy_option(),
11289
11336
  scope: str = typer.Option(
@@ -11565,6 +11612,9 @@ def spring_audit_cmd(
11565
11612
  ).finalize()
11566
11613
  if format == "github-comment":
11567
11614
  output = _render_spring_audit_github_comment(empty_result, min_severity)
11615
+ elif format == "sarif":
11616
+ from sourcecode.sarif_emit import spring_audit_to_sarif
11617
+ output = _serialize_dict(spring_audit_to_sarif(empty_result.to_dict()), "json")
11568
11618
  else:
11569
11619
  output = _serialize_dict(empty_result.to_dict(), format)
11570
11620
  _emit_command_output(output, output_path, copy,
@@ -11991,7 +12041,11 @@ def spring_audit_cmd(
11991
12041
  )
11992
12042
  if _sel is not None:
11993
12043
  data["_filter"] = _sel
11994
- output = _serialize_dict(data, format)
12044
+ if format == "sarif":
12045
+ from sourcecode.sarif_emit import spring_audit_to_sarif
12046
+ output = _serialize_dict(spring_audit_to_sarif(data), "json")
12047
+ else:
12048
+ output = _serialize_dict(data, format)
11995
12049
  finally:
11996
12050
  # C3-54: `done` must not claim the answer exists before it does. The
11997
12051
  # field log showed `[ask] done (6.8m)` printed, then the process torn
@@ -12240,7 +12294,7 @@ def verify_cmd(
12240
12294
  output_path: Optional[Path] = typer.Option(
12241
12295
  None, "--output", "-o", help="Write the report JSON to a file instead of stdout."
12242
12296
  ),
12243
- format: str = _format_option(help="Output format: json (default), yaml, or github-actions."),
12297
+ format: str = _format_option(help="Output format: json (default), yaml, github-actions, or sarif."),
12244
12298
  fail_on: str = typer.Option(
12245
12299
  "new",
12246
12300
  "--fail-on",
@@ -12552,6 +12606,9 @@ def verify_cmd(
12552
12606
  count=_verify_count,
12553
12607
  reason=(report.unverified_reason or "") if _verify_status == "unverified" else "",
12554
12608
  )
12609
+ elif format == "sarif":
12610
+ from sourcecode.sarif_emit import verify_to_sarif
12611
+ output = _serialize_dict(verify_to_sarif(payload), "json")
12555
12612
  else:
12556
12613
  output = _serialize_dict(payload, format)
12557
12614
 
@@ -13571,6 +13628,14 @@ def migrate_apply_cmd(
13571
13628
  ),
13572
13629
  min_severity: str = typer.Option("low", "--min-severity"),
13573
13630
  include_hygiene: bool = typer.Option(False, "--include-hygiene"),
13631
+ ci: bool = typer.Option(
13632
+ False,
13633
+ "--ci/--no-ci",
13634
+ help=(
13635
+ "Gate CI on migration_blocking_count from the pre-apply plan. "
13636
+ "A failing gate never creates a migration worktree."
13637
+ ),
13638
+ ),
13574
13639
  output_path: Optional[Path] = typer.Option(None, "--output", "-o"),
13575
13640
  format: str = _format_option(help="Output format: json or yaml."),
13576
13641
  copy: bool = _copy_option(),
@@ -13622,16 +13687,23 @@ def migrate_apply_cmd(
13622
13687
  "status": "not_requested",
13623
13688
  "compile_requested": bool(compile_result),
13624
13689
  },
13690
+ "gate": _migration_gate(report, ci),
13625
13691
  }
13626
- if not write:
13692
+ if not write or (ci and data["gate"]["exit_code"]):
13627
13693
  data["execution"]["next_step"] = (
13628
13694
  "Review this plan, then rerun with --write --rewrite-version X.Y.Z. "
13629
13695
  "No source or branch was created."
13630
13696
  )
13631
13697
  _emit_command_output(
13632
13698
  _serialize_dict(data, format), output_path, copy,
13633
- success_msg="migration plan generated; nothing was changed",
13699
+ success_msg=(
13700
+ "migration gate failed; nothing was changed"
13701
+ if ci and data["gate"]["exit_code"]
13702
+ else "migration plan generated; nothing was changed"
13703
+ ),
13634
13704
  )
13705
+ if ci and data["gate"]["exit_code"]:
13706
+ raise typer.Exit(code=1)
13635
13707
  return
13636
13708
 
13637
13709
  from sourcecode import readonly as _readonly
@@ -14195,6 +14267,21 @@ def _migration_blast_radius(
14195
14267
  )
14196
14268
 
14197
14269
 
14270
+ def _migration_gate(report: object, enabled: bool) -> dict[str, object]:
14271
+ """Publish the migration CI decision from the report's blocking authority."""
14272
+ count = int(getattr(report, "migration_blocking_count", 0) or 0)
14273
+ return {
14274
+ "enabled": bool(enabled),
14275
+ "verdict": "fail" if count else "pass",
14276
+ "exit_code": 1 if count else 0,
14277
+ "blocking_count": count,
14278
+ "basis": (
14279
+ "--ci gates on migration_blocking_count: findings in the applicable "
14280
+ "Boot/Jakarta/Spring migration dimensions; advisory hygiene is excluded"
14281
+ ),
14282
+ }
14283
+
14284
+
14198
14285
  @app.command("migrate-check")
14199
14286
  def migrate_check_cmd(
14200
14287
  path: Path = typer.Argument(
@@ -14264,6 +14351,14 @@ def migrate_check_cmd(
14264
14351
  "(hygiene_findings, summary.*) but not listed."
14265
14352
  ),
14266
14353
  ),
14354
+ ci: bool = typer.Option(
14355
+ False,
14356
+ "--ci/--no-ci",
14357
+ help=(
14358
+ "Gate CI on migration_blocking_count. Without --ci the command remains "
14359
+ "advisory and exits 0 after a complete report."
14360
+ ),
14361
+ ),
14267
14362
  target_jdk: Optional[int] = typer.Option(
14268
14363
  None,
14269
14364
  "--target-jdk",
@@ -14506,6 +14601,7 @@ def migrate_check_cmd(
14506
14601
  # here that the variable is advice on this command, rather than inferring a
14507
14602
  # bug from a deadline that did not fire.
14508
14603
  report.metadata["analysis_budget"] = _budget_disclosure("migrate-check")
14604
+ report.metadata["gate"] = _migration_gate(report, ci)
14509
14605
 
14510
14606
  payload: Optional[dict[str, Any]] = None
14511
14607
  if table:
@@ -14611,6 +14707,9 @@ def migrate_check_cmd(
14611
14707
  f"{_snapshot_note}"
14612
14708
  ),
14613
14709
  )
14710
+ if ci:
14711
+ _gate = (payload or report.to_dict()).get("metadata", {}).get("gate", {})
14712
+ raise typer.Exit(code=int(_gate.get("exit_code", 0)))
14614
14713
 
14615
14714
 
14616
14715
  # ── Spring Impact Chain ───────────────────────────────────────────────────────
sourcecode/envelope.py CHANGED
@@ -275,10 +275,12 @@ def _volatile_keys() -> "frozenset[str]":
275
275
  # analysis report drift, which is the false positive that would have
276
276
  # made this id worth ignoring.
277
277
  conditions = {name for name, _label, _axes in _CONDITION_FIELDS}
278
- return frozenset(_CLOCK_KEYS) | conditions | {_META_KEY}
278
+ # `_cache` is runtime provenance attached by cache layers. It can change
279
+ # between cold and warm runs without changing the analysis answer.
280
+ return frozenset(_CLOCK_KEYS) | conditions | {_META_KEY, "_cache"}
279
281
  except Exception:
280
282
  return frozenset({
281
- _META_KEY, "generated_at", "run_id", "timings", "cache_layers",
283
+ _META_KEY, "_cache", "generated_at", "run_id", "timings", "cache_layers",
282
284
  })
283
285
 
284
286
 
sourcecode/explain.py CHANGED
@@ -80,6 +80,7 @@ class ClassExplanation:
80
80
  stereotype: str
81
81
  purpose: str
82
82
  public_methods: list[str] = field(default_factory=list)
83
+ public_methods_detail: list[dict] = field(default_factory=list)
83
84
  incoming_callers: list[str] = field(default_factory=list)
84
85
  outgoing_deps: list[str] = field(default_factory=list)
85
86
  events_published: list[str] = field(default_factory=list)
@@ -134,7 +135,7 @@ class ClassExplanation:
134
135
  import dataclasses
135
136
 
136
137
  capped_fields = (
137
- "public_methods", "incoming_callers", "outgoing_deps",
138
+ "public_methods", "public_methods_detail", "incoming_callers", "outgoing_deps",
138
139
  "events_published", "events_consumed", "transactions",
139
140
  "security_constraints", "rest_endpoints",
140
141
  )
@@ -160,6 +161,12 @@ class ClassExplanation:
160
161
  "stereotype": self.stereotype,
161
162
  "purpose": self.purpose,
162
163
  "public_methods": self.public_methods,
164
+ "public_methods_detail": self.public_methods_detail,
165
+ "public_methods_meta": {
166
+ "unit": "declared method",
167
+ "population": "public/protected methods and Spring @Bean factory members",
168
+ "basis": "canonical IR method nodes for the explained class",
169
+ },
163
170
  "incoming_callers": self.incoming_callers,
164
171
  "incoming_callers_count": self.incoming_callers_count,
165
172
  **declared_tags("explain.incoming_callers_count"),
@@ -187,6 +194,7 @@ class ClassExplanation:
187
194
  stereotype=d.get("stereotype", ""),
188
195
  purpose=d.get("purpose", ""),
189
196
  public_methods=list(d.get("public_methods", [])),
197
+ public_methods_detail=list(d.get("public_methods_detail", [])),
190
198
  incoming_callers=list(d.get("incoming_callers", [])),
191
199
  outgoing_deps=list(d.get("outgoing_deps", [])),
192
200
  events_published=list(d.get("events_published", [])),
@@ -434,6 +442,34 @@ def _build_public_methods(class_fqn: str, raw_nodes: list[dict]) -> list[str]:
434
442
  return sorted(set(methods))
435
443
 
436
444
 
445
+ def _build_public_method_details(class_fqn: str, raw_nodes: list[dict]) -> list[dict]:
446
+ """Return one evidence row per public/protected method declaration."""
447
+ prefix = class_fqn + "#"
448
+ details: list[dict] = []
449
+ for node in raw_nodes:
450
+ fqn = str(node.get("fqn") or "")
451
+ if not fqn.startswith(prefix):
452
+ continue
453
+ kind = node.get("symbol_kind") or node.get("type") or ""
454
+ if kind == "bean":
455
+ eligible = True
456
+ elif kind in ("method", "endpoint", "constructor", ""):
457
+ eligible = "public" in (node.get("modifiers") or []) or "protected" in (
458
+ node.get("modifiers") or []
459
+ )
460
+ else:
461
+ eligible = False
462
+ if not eligible:
463
+ continue
464
+ details.append({
465
+ "name": fqn[len(prefix):],
466
+ "signature": str(node.get("signature") or ""),
467
+ "source_file": str(node.get("source_file") or node.get("declaring_file") or "") or None,
468
+ "line": node.get("line"),
469
+ })
470
+ return sorted(details, key=lambda row: (row["name"], row["signature"], row["source_file"] or ""))
471
+
472
+
437
473
  def _caller_class_fqns(
438
474
  class_fqn: str, cir: "CanonicalRepositoryIR", graph: "ContextGraph"
439
475
  ) -> list[str]:
@@ -784,6 +820,11 @@ def explain_class(
784
820
  public_methods = _section_of(
785
821
  "public_methods", lambda: _build_public_methods(class_fqn, raw_nodes), []
786
822
  )
823
+ public_methods_detail = _section_of(
824
+ "public_methods_detail",
825
+ lambda: _build_public_method_details(class_fqn, raw_nodes),
826
+ [],
827
+ )
787
828
  incoming_callers = _section_of(
788
829
  "incoming_callers", lambda: _build_callers(class_fqn, cir, graph), []
789
830
  )
@@ -819,6 +860,7 @@ def explain_class(
819
860
  stereotype=stereotype,
820
861
  purpose=purpose,
821
862
  public_methods=public_methods,
863
+ public_methods_detail=public_methods_detail,
822
864
  incoming_callers=incoming_callers,
823
865
  incoming_callers_count=len(incoming_callers),
824
866
  outgoing_deps=outgoing_deps,
@@ -32,7 +32,7 @@ FORMAT_REGISTRY: "dict[str, tuple[str, ...]]" = {
32
32
  "main": ("json", "yaml"),
33
33
  "pack-list": ("json", "yaml"),
34
34
  "pack-assessment": ("json", "yaml", "markdown"),
35
- "pack-gate": ("json", "yaml", "github-actions", "gitlab"),
35
+ "pack-gate": ("json", "yaml", "github-actions", "gitlab", "sarif"),
36
36
  "repo-ir": ("json", "yaml"),
37
37
  # C3-59: these eleven published `--format` and validated nothing, so
38
38
  # `ask risk . --format bogus` answered with JSON and no word about the value
@@ -57,7 +57,7 @@ FORMAT_REGISTRY: "dict[str, tuple[str, ...]]" = {
57
57
  "risk": ("json", "yaml"),
58
58
  "trend": ("json", "yaml"),
59
59
  "timeline": ("json", "yaml"),
60
- "verify": ("json", "yaml", "github-actions"),
60
+ "verify": ("json", "yaml", "github-actions", "sarif"),
61
61
  "verify-edit": ("json", "yaml"),
62
62
  "impact": ("json", "yaml"),
63
63
  "endpoints": ("json", "yaml"),
@@ -74,7 +74,7 @@ FORMAT_REGISTRY: "dict[str, tuple[str, ...]]" = {
74
74
  "migrate-check": ("json", "text", "github-actions"),
75
75
  "migrate-recipe": ("json", "yaml"),
76
76
  "migrate-apply": ("json", "yaml"),
77
- "spring-audit": ("json", "yaml", "github-comment", "github-actions"),
77
+ "spring-audit": ("json", "yaml", "github-comment", "github-actions", "sarif"),
78
78
  "explain": ("text", "json"),
79
79
  # `prepare-context` and the three commands that delegate to it gained yaml
80
80
  # with C3-59: the conversion happens in the shared emit seam, so it is the