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 +1 -1
- sourcecode/_build_commit.py +1 -1
- sourcecode/_docs/DEFECT-LEDGER.md +69 -0
- sourcecode/audit_report.py +43 -0
- sourcecode/baseline_autocapture.py +9 -4
- sourcecode/cache_model.py +3 -4
- sourcecode/cli.py +114 -15
- sourcecode/envelope.py +4 -2
- sourcecode/explain.py +43 -1
- sourcecode/format_contract.py +3 -3
- sourcecode/migrate_check.py +21 -2
- sourcecode/non_coverage.py +46 -1
- sourcecode/packs.py +85 -1
- sourcecode/parse_cache.py +16 -0
- sourcecode/posture.py +17 -1
- sourcecode/product_info.py +25 -1
- sourcecode/repository_ir.py +118 -50
- sourcecode/sarif_emit.py +156 -0
- sourcecode/schema_registry.py +5 -0
- sourcecode/security_posture.py +12 -0
- sourcecode/selftest.py +8 -2
- sourcecode/serializer.py +21 -2
- sourcecode/spring_impact.py +101 -0
- sourcecode/spring_tx_analyzer.py +8 -0
- sourcecode/workspace.py +1 -1
- {sourcecode-5.8.25.dist-info → sourcecode-5.8.26.dist-info}/METADATA +4 -4
- {sourcecode-5.8.25.dist-info → sourcecode-5.8.26.dist-info}/RECORD +31 -30
- {sourcecode-5.8.25.dist-info → sourcecode-5.8.26.dist-info}/WHEEL +0 -0
- {sourcecode-5.8.25.dist-info → sourcecode-5.8.26.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.8.25.dist-info → sourcecode-5.8.26.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.8.25.dist-info → sourcecode-5.8.26.dist-info}/licenses/NOTICE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/_build_commit.py
CHANGED
|
@@ -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 = '
|
|
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
|
sourcecode/audit_report.py
CHANGED
|
@@ -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=
|
|
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=
|
|
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=
|
|
507
|
-
baseline_file=
|
|
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
|
-
#
|
|
749
|
-
#
|
|
750
|
-
|
|
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
|
-
|
|
545
|
+
commands = getattr(node, "commands", None)
|
|
546
|
+
if not isinstance(commands, dict):
|
|
547
547
|
break
|
|
548
|
-
child =
|
|
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
|
-
|
|
581
|
+
commands = getattr(node, "commands", None)
|
|
582
|
+
if not isinstance(commands, dict):
|
|
583
583
|
break
|
|
584
|
-
child =
|
|
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
|
-
|
|
2067
|
+
ctx = None
|
|
2068
|
+
|
|
2068
2069
|
if ctx is None:
|
|
2069
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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=
|
|
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
|
-
|
|
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,
|
sourcecode/format_contract.py
CHANGED
|
@@ -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
|