sourcecode 5.8.31__py3-none-any.whl → 5.9.0__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 +1 -1
- sourcecode/_build_commit.py +1 -1
- sourcecode/_docs/DEFECT-LEDGER.md +89 -1
- sourcecode/_docs/USER_GUIDE.md +92 -6
- sourcecode/access_drift.py +232 -0
- sourcecode/c4_render.py +245 -0
- sourcecode/cache_model.py +28 -0
- sourcecode/cli.py +1168 -9
- sourcecode/cve_feed.py +290 -0
- sourcecode/cve_reachability.py +497 -0
- sourcecode/explain.py +25 -6
- sourcecode/file_chunker.py +15 -4
- sourcecode/findings_baseline.py +270 -0
- sourcecode/format_contract.py +7 -0
- sourcecode/intent_router.py +402 -0
- sourcecode/mcp/server.py +66 -8
- sourcecode/non_coverage.py +15 -7
- sourcecode/pr_risk.py +281 -0
- sourcecode/remediation_flow.py +259 -0
- sourcecode/schema_registry.py +16 -0
- sourcecode/schemas/cve-reachability-v1.schema.json +130 -0
- sourcecode/self_audit.py +289 -0
- sourcecode/watch_loop.py +214 -0
- {sourcecode-5.8.31.dist-info → sourcecode-5.9.0.dist-info}/METADATA +77 -18
- {sourcecode-5.8.31.dist-info → sourcecode-5.9.0.dist-info}/RECORD +29 -18
- {sourcecode-5.8.31.dist-info → sourcecode-5.9.0.dist-info}/WHEEL +0 -0
- {sourcecode-5.8.31.dist-info → sourcecode-5.9.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.8.31.dist-info → sourcecode-5.9.0.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.8.31.dist-info → sourcecode-5.9.0.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 = 'b4c3691c4110d863fedcd54a61a08fd2997e400c'
|
|
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,94 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
16
16
|
|
|
17
17
|
## Current Synchronization
|
|
18
18
|
|
|
19
|
+
**External `5.8.32` audit received, 2026-08-26 — `AUD-607`.** Two independent
|
|
20
|
+
reports ([`AUDIT-2026-08-26-EXTERNAL-5.8.32.md`](AUDIT-2026-08-26-EXTERNAL-5.8.32.md)):
|
|
21
|
+
a reverification round (9.7/10) and a comparison round against `5.8.31` (8/10).
|
|
22
|
+
Six previously closed findings hold on their third or fourth consecutive
|
|
23
|
+
re-check, and **no regression is reported by either round**.
|
|
24
|
+
|
|
25
|
+
One new defect is confirmed and closed in `5.9.0`: `AUD-607-C01`
|
|
26
|
+
(P1), where `chunk-file` emitted chunk content that did not match its own
|
|
27
|
+
declared `start_line`/`end_line`. Controlled reproduction found the mechanism
|
|
28
|
+
**wider than reported** — two defects, not one: `_flush_chunk` serialised the
|
|
29
|
+
untruncated pending buffer while declaring a truncated end line, and the
|
|
30
|
+
annotation buffer's clearing guard was **unreachable**, so a field's
|
|
31
|
+
`@Autowired` was carried onto the following method while the field's own
|
|
32
|
+
declaration line dropped out of that chunk. The report's "deliberate undeclared
|
|
33
|
+
overlap" hypothesis is refuted; no overlap contract is published. Its bounding
|
|
34
|
+
battery now asserts content against the declared range, which the previous
|
|
35
|
+
`include_content=False` coverage structurally could not see.
|
|
36
|
+
|
|
37
|
+
`AUD-607-E01` (P2, `explain` returned "No stereotype detected" while listing that
|
|
38
|
+
same class's endpoints) and `AUD-607-I01` (P3, `--files -` asserted "stdin is a
|
|
39
|
+
terminal" for a Git Bash/MSYS redirect it cannot verify) are also closed in
|
|
40
|
+
`5.9.0`. The first is fixed vendor-agnostically by reading the endpoint
|
|
41
|
+
evidence the answer already computed, never by adding JAX-RS to a Spring
|
|
42
|
+
annotation list; the second names the outcome instead of an unverifiable cause,
|
|
43
|
+
the same rule `_confirm` follows for C3-106.
|
|
44
|
+
|
|
45
|
+
**`AUD-607-O01` is not reproduced — second independent refutation, and it
|
|
46
|
+
supersedes `AUD-606-O01`.** The two reports disagreed; measurement resolved it.
|
|
47
|
+
The reverification round re-ran the command itself and confirmed the vendor
|
|
48
|
+
position, and controlled reproduction on the current checkout finds no list
|
|
49
|
+
exceeding the cap on `spring-petclinic` or `BroadleafCommerce`, with the
|
|
50
|
+
`total`/`shown`/`omitted` envelope published on both sides of the diff. The
|
|
51
|
+
`estimated_tokens` figure the second report cites is **not emitted by this
|
|
52
|
+
command's payload**; it belongs to the output-ceiling spill path and describes a
|
|
53
|
+
run that overflowed the ceiling. This row must not be reopened from that figure
|
|
54
|
+
alone.
|
|
55
|
+
|
|
56
|
+
`AUD-606-P01` is closed: the cold-cache improvement held for a second
|
|
57
|
+
consecutive release (`137.8s` then `130s`), so it is no longer a single sample.
|
|
58
|
+
`AUD-607-P01` (`pack gate --since` timing) is inconclusive for the fourth round
|
|
59
|
+
under confirmed process contamination and stays bound to the existing
|
|
60
|
+
measurement protocol — a contaminated sample is evidence of neither a regression
|
|
61
|
+
nor its absence.
|
|
62
|
+
|
|
63
|
+
**Feature intake `AUD-607-X01`…`X09` is CLOSED, one commit per row.** All nine
|
|
64
|
+
are released in `5.9.0` with bounding batteries and full registry entries
|
|
65
|
+
(`b7df333`, `182d95e`, `da3e2da`, `d35b7f8`, `3e42c2c`, `0fc3a15`, `55b93c2`,
|
|
66
|
+
`6463fdf`, `f1fa913`): `cve`, `intent`, `remediate`, `pr-risk`,
|
|
67
|
+
`export --c4 --render`, `watch`, `baseline --kind findings`, `drift` and
|
|
68
|
+
`self-audit`. Six schemas were registered and published alongside them.
|
|
69
|
+
⚠**Three defects were found by building these, not by the audit:** the C4
|
|
70
|
+
renderer's first version drew report metadata (`summary`, `coverage_note`) as
|
|
71
|
+
system architecture; the watch loop stat-ed repository-relative paths against
|
|
72
|
+
the process CWD and so reported "nothing changed" forever; and `self-audit`
|
|
73
|
+
found its own schema identifier unregistered on its first run. Each is covered
|
|
74
|
+
by an assertion now. `X09` ships with its "not recommended" verdict inside the
|
|
75
|
+
payload rather than dropped: the semantic engine does not apply to this
|
|
76
|
+
product's own Python source, and those figures are declared absent rather than
|
|
77
|
+
reported as zeros.
|
|
78
|
+
|
|
79
|
+
**Current `5.9.0` queue:** `AUD-607-U01` (P3, new commands do not name their
|
|
80
|
+
applicable bounding flags in `--help`) and `AUD-607-U02` (P2, unconfirmed:
|
|
81
|
+
`cache warm` reported a component `exit 143` under a global exit `0`). Feature
|
|
82
|
+
intake `AUD-607-X01`…`X09` is recorded in the audit note and the development
|
|
83
|
+
roadmap; **two proposed features were withdrawn on inspection** because they
|
|
84
|
+
already ship as `spring-audit --clusters` (CODEOWNERS ownership) and
|
|
85
|
+
`spring-audit --since --fail-on-new` (new-versus-historical findings). The
|
|
86
|
+
existing `AUD-604-R01` and `AUD-605` release-artifact verification items remain
|
|
87
|
+
unchanged.
|
|
88
|
+
|
|
89
|
+
**External `5.8.31` audit received, 2026-08-26 - `AUD-606`.**
|
|
90
|
+
[`AUDIT-2026-08-26-EXTERNAL-5.8.31.md`](AUDIT-2026-08-26-EXTERNAL-5.8.31.md)
|
|
91
|
+
confirms the new progress and shared-cache controls, and records one actionable
|
|
92
|
+
external-subject follow-up `AUD-606-O01`, where `posture --diff --compact --limit`
|
|
93
|
+
was effectively unchanged on the auditor's nested output; the current checkout
|
|
94
|
+
reproduces no such gap and its bounding batteries pass. It also opens the CLI
|
|
95
|
+
consistency row `AUD-606-R01` for heterogeneous `retrieve` target flags, fixed
|
|
96
|
+
in the current checkout by `be16c04` with help-parity coverage.
|
|
97
|
+
The 137.8s cold run is a single-sample improvement and remains `AUD-606-P01`
|
|
98
|
+
benchmark follow-up, not a parser regression. `AUD-606-P02` (`pack gate` timing)
|
|
99
|
+
and `AUD-606-I01` (`pr-impact --files -` with `/dev/null`) are inconclusive due
|
|
100
|
+
to process contamination and shell-specific stdin behaviour respectively.
|
|
101
|
+
|
|
102
|
+
**Current `5.8.31` queue:** external-subject follow-up `AUD-606-O01`; controlled
|
|
103
|
+
benchmark/portability follow-ups `AUD-606-P01`, `AUD-606-P02` and `AUD-606-I01`.
|
|
104
|
+
The existing `AUD-604-R01` and `AUD-605` release-artifact verification items
|
|
105
|
+
remain unchanged.
|
|
106
|
+
|
|
19
107
|
**External pack audit received, 2026-08-26 - `AUD-605`.**
|
|
20
108
|
[`AUDIT-2026-08-26-EXTERNAL-5.8.28-PACK.md`](AUDIT-2026-08-26-EXTERNAL-5.8.28-PACK.md)
|
|
21
109
|
adds two rows: `AUD-605-G01`, where an unresolved test file broadened pack-gate unknown
|
|
@@ -25,7 +113,7 @@ checkout by `52bcc36` and `f89ae18`, with focused regression batteries passing;
|
|
|
25
113
|
artefact verification remains pending. The pack's `--since` timing is additional evidence
|
|
26
114
|
for `AUD-604-P01`, not a duplicate performance finding.
|
|
27
115
|
|
|
28
|
-
**
|
|
116
|
+
**Previous `5.8.31` pack queue:** artefact verification for `AUD-604-R01`; the two `AUD-605`
|
|
29
117
|
fixes are included in this release and await release-artefact verification.
|
|
30
118
|
|
|
31
119
|
**External `5.8.28` audit received, 2026-08-26 - `AUD-604`.**
|
sourcecode/_docs/USER_GUIDE.md
CHANGED
|
@@ -46,13 +46,13 @@ 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.
|
|
49
|
+
5.9.0, `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.
|
|
55
|
+
the two pack fixes are included in release 5.9.0, with release-artifact verification
|
|
56
56
|
remaining for detached execution and pack evidence.
|
|
57
57
|
|
|
58
58
|
---
|
|
@@ -115,6 +115,8 @@ ask pack list
|
|
|
115
115
|
ask pack assessment . --profile prod --output-dir .ask/packs/assessment
|
|
116
116
|
ask pack gate . --since origin/main --compact --output-dir .ask/packs/gate
|
|
117
117
|
ask pack assessment . --format markdown -o assessment.md
|
|
118
|
+
ask pack assessment . --only risk,posture --output-dir .ask/packs/assessment
|
|
119
|
+
ask pack gate . --pr 123 --output-dir .ask/packs/gate
|
|
118
120
|
```
|
|
119
121
|
|
|
120
122
|
To include deployment evidence, pass the environment artefacts that describe
|
|
@@ -129,8 +131,25 @@ The gate can emit native annotations for CI logs:
|
|
|
129
131
|
```bash
|
|
130
132
|
ask pack gate . --since origin/main --format github-actions
|
|
131
133
|
ask pack gate . --since origin/main --format gitlab
|
|
134
|
+
ask pack gate . --since origin/main --format sarif -o gate.sarif
|
|
132
135
|
```
|
|
133
136
|
|
|
137
|
+
`pack assessment --only` selects a comma-separated subset of the declared
|
|
138
|
+
assessment components. The manifest records omitted components;
|
|
139
|
+
`audit-report` may be selected only together with `risk`.
|
|
140
|
+
|
|
141
|
+
MCP `get_assessment_pack` exposes the same capability through its `only`
|
|
142
|
+
`list[str]` input. It uses the declared component order and validation rules;
|
|
143
|
+
the reproducible CLI command in the `mcp` projection includes the selection.
|
|
144
|
+
|
|
145
|
+
`pack gate --pr NUMBER` resolves changed files through the local `gh` CLI and
|
|
146
|
+
uses the pull request's exact base commit. The base commit must already exist
|
|
147
|
+
locally; the command does not fetch or modify the repository and cannot be
|
|
148
|
+
combined with `--since`.
|
|
149
|
+
|
|
150
|
+
The `sarif` gate format is a bounded SARIF 2.1.0 export of ASK-owned gate
|
|
151
|
+
decisions. It does not re-emit findings produced by third-party scanners.
|
|
152
|
+
|
|
134
153
|
The pack JSON preserves `manifest`, identity, fingerprints, evidence and each
|
|
135
154
|
component's partial status. `PASS`, `BLOCK` and `UNVERIFIED` are data
|
|
136
155
|
decisions; the process exit code is the pipeline signal. When `--output-dir` is
|
|
@@ -139,6 +158,10 @@ component remains a separate JSON artifact. An explicit `--output FILE` takes
|
|
|
139
158
|
precedence. See the
|
|
140
159
|
[Product Route contract](PRODUCT-ROUTE.md) for the boundaries.
|
|
141
160
|
|
|
161
|
+
The MCP `get_change_safety_gate` tool accepts the same two comparison modes:
|
|
162
|
+
`since` for a local Git ref or `pr` for GitHub pull-request resolution through
|
|
163
|
+
the local `gh` CLI. Pass exactly one; PR mode requires the base SHA locally.
|
|
164
|
+
|
|
142
165
|
See [Product Route](PRODUCT-ROUTE.md) for the evidence contract and scope.
|
|
143
166
|
|
|
144
167
|
A distinct set of commands exists to feed **AI coding agents** rather than to be read by a
|
|
@@ -183,7 +206,7 @@ pipx install sourcecode # isolated install, no venv needed
|
|
|
183
206
|
|
|
184
207
|
# Verify
|
|
185
208
|
ask version
|
|
186
|
-
# ask 5.
|
|
209
|
+
# ask 5.9.0
|
|
187
210
|
```
|
|
188
211
|
|
|
189
212
|
Requires Python 3.9+.
|
|
@@ -228,7 +251,7 @@ estimate scaled by file count would be wrong in the direction that costs you a s
|
|
|
228
251
|
and an anchor printed without its release goes on recommending a nightly job for a command
|
|
229
252
|
that has come to finish in seconds (C3-97).
|
|
230
253
|
|
|
231
|
-
|
|
254
|
+
48 commands and seven command groups exist. Four of them carry most of the measured
|
|
232
255
|
value in field use, and they are the ones to learn first:
|
|
233
256
|
|
|
234
257
|
| Start with | Because |
|
|
@@ -1275,16 +1298,24 @@ explaining. The rest do one thing; `ask <command> --help` is the whole story.
|
|
|
1275
1298
|
| Command | Tier | What it does |
|
|
1276
1299
|
|---|---|---|
|
|
1277
1300
|
| `ask migrate-check` | core | Spring Boot 2→3 readiness plus Java LTS/licensing inventory: blockers, effort range, target JDK evidence and explicit licensing-review signals. Full reference: [migrate-check.md](migrate-check.md) |
|
|
1301
|
+
| `ask drift` | supported | What changed in the effective access — since the last capture (`--capture` records; the default compares) or between two profile sets (`--against`). One arithmetic for both axes. Separates `newly_unmatched` from `newly_matched`, and a `removed` route is NOT reported as having become safe. |
|
|
1302
|
+
| `ask watch` | supported | Warm the index once, then answer what each save affected — one JSON line per tick. `--once` is the pre-commit shape; `--ticks`/`--for` bound the loop. A file added since the warm is reported `unresolved`, never `unreachable`, and a failed analysis is an `error` tick rather than an empty change list. |
|
|
1303
|
+
| `ask pr-risk` | supported | One verdict for one diff — does this change make things worse? Joins `pr-impact` reach, endpoint access, the shared risk model and CODEOWNERS. `--since` differences route inventories so `new_endpoints` is a fact rather than a guess. An unresolved changed file caps the verdict at `UNKNOWN`, which fails every gate. |
|
|
1304
|
+
| `ask remediate` | supported | One loop from finding to verified change: audit → recipe → apply in a dedicated worktree → `verify-edit`. Dry by default; `--write` applies. Publishes `mechanisable` against `manual_remainder` with the reason each family cannot be rewritten, and reports `unverified` — never `applied` — when the result could not be checked. |
|
|
1305
|
+
| `ask cve` | supported | Cross the dependency inventory against a **local** OSV advisory feed (`--feed`; no network is taken), then report whether a route can reach the vulnerable package: `reachable`, `imported_not_exposed`, `not_observed` or `unknown`. `not_observed` is not a statement of safety, and nothing here answers "not reachable". |
|
|
1278
1306
|
| `ask explain <Class>` | supported | Human-readable architectural summary for one class (text by design, not JSON) |
|
|
1279
|
-
| `ask export` | supported | Tool-agnostic views for downstream tooling: `--c4`, `--module-graph`, `--by-directory`, `--integrations` |
|
|
1307
|
+
| `ask export` | supported | Tool-agnostic views for downstream tooling: `--c4`, `--module-graph`, `--by-directory`, `--integrations`. `--c4 --render mermaid\|svg` draws the same model instead of emitting it — presentation over the existing payload, with the node cap written on the diagram. |
|
|
1280
1308
|
| `ask repo-ir` | supported | Deterministic symbol-level IR for the whole repository |
|
|
1281
1309
|
| `ask cold-start` | supported | Repository Intelligence Snapshot bootstrap context, returned from the persisted RIS |
|
|
1310
|
+
| `ask baseline … --kind findings` | supported | The defect population as a persisted series, in the same store as the architectural baseline: `capture` records it, `diff` reports new/resolved/persisting against the last capture, and `trend` says how long each still-open defect has been open. `first_seen` is the earliest STORED capture — not the day it was introduced. |
|
|
1282
1311
|
| `ask prepare-context <task>` | supported | Task-shaped context: `onboard`, `delta`, `fix-bug`, `generate-tests`. See [Typical workflows](#typical-workflows) |
|
|
1283
1312
|
| `ask chunk-file <file>` | supported | Split a large Java file into semantic chunks an agent can consume |
|
|
1313
|
+
| `ask intent "<question>"` | supported | Resolve a plain-language question to the typed `retrieve` query that answers it (scored lexical match against a declared vocabulary — no model, no generation). Prints the query; `--run` executes it. A question fitting two intents too closely is refused with both named. |
|
|
1284
1314
|
| `ask rename-class` | supported | Word-boundary Java rename across the repository |
|
|
1285
1315
|
| `ask schema <name>` | supported | Print a published JSON Schema for this tool's output |
|
|
1316
|
+
| `ask self-audit` | supported | Check the SHIPPED build's own coherence — cache model, format contract, schema registry and output affordances. Declares explicitly that the semantic engine does not apply to this product's own source (ASK analyses Java; this build is Python), rather than reporting zeros. `ask selftest` is the different question: the defect ledger run against a real repository. |
|
|
1286
1317
|
| `ask archetype` | experimental | Evidence-based architectural archetype across four dimensions |
|
|
1287
|
-
| `ask retrieve` | parked | Typed knowledge queries over the model — kept working, no longer developed |
|
|
1318
|
+
| `ask retrieve` | parked | Typed knowledge queries over the model — kept working, no longer developed. Symbol-oriented queries accept `--target/-t`; `--module/-m`, `--subsystem/-s` and `--endpoint/-e` remain compatible domain aliases. |
|
|
1288
1319
|
| `ask baseline capture\|diff\|trend` | supported | Versioned architectural metrics over time; see [`ask trend`](#ask-trend-dir--how-the-architecture-moved) |
|
|
1289
1320
|
| `ask cache status\|warm\|model\|clear\|freshness` | supported | Cache inspection; `ask cache model` states what a warm buys each command |
|
|
1290
1321
|
| `ask config` · `ask version` · `ask activate` · `ask auth` · `ask telemetry` · `ask mcp` | supported | Configuration, version, licence, authentication, telemetry (on by default), MCP integration |
|
|
@@ -1359,6 +1390,51 @@ ask endpoints /repo --compact # the census without the rows: coun
|
|
|
1359
1390
|
ask endpoints /repo --output endpoints.json
|
|
1360
1391
|
```
|
|
1361
1392
|
|
|
1393
|
+
### Closing the loop: find it, judge it, fix it, prove it
|
|
1394
|
+
|
|
1395
|
+
The seven newest commands are joins of pieces that already shipped, and they share one
|
|
1396
|
+
rule: where the answer cannot be decided, they say so rather than return a reassuring zero.
|
|
1397
|
+
|
|
1398
|
+
```bash
|
|
1399
|
+
# 1. Which advisories a route can actually REACH — not which are on the classpath.
|
|
1400
|
+
# The feed is a local file; no network is taken during analysis, so a CI gate can
|
|
1401
|
+
# never pass because a fetch timed out.
|
|
1402
|
+
ask cve . --feed ./osv-maven.json --reachable-only --fail-on reachable
|
|
1403
|
+
|
|
1404
|
+
# 2. Ask in your own words. It SELECTS one of retrieve's typed queries and prints it;
|
|
1405
|
+
# --run executes it. It never invents an answer.
|
|
1406
|
+
ask intent "what breaks if I change OrderService?"
|
|
1407
|
+
|
|
1408
|
+
# 3. Judge the change, not the repository. UNKNOWN outranks PASS and fails the gate.
|
|
1409
|
+
git diff --name-only main | ask pr-risk . --files - --since main --fail-on block
|
|
1410
|
+
|
|
1411
|
+
# 4. Fix the mechanical part, and prove the tree still holds.
|
|
1412
|
+
ask remediate . # the plan; writes nothing
|
|
1413
|
+
ask remediate . --write --allow-network
|
|
1414
|
+
|
|
1415
|
+
# 5. Keep the receipts, so "how long has this been open?" has an answer.
|
|
1416
|
+
ask baseline capture . --kind findings
|
|
1417
|
+
ask baseline trend . --kind findings
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
Two of them run on a clock rather than on demand:
|
|
1421
|
+
|
|
1422
|
+
```bash
|
|
1423
|
+
# In CI, after each deploy: what opened up since the last capture?
|
|
1424
|
+
ask drift . --profile prod --capture
|
|
1425
|
+
ask drift . --profile prod --fail-on-exposure
|
|
1426
|
+
|
|
1427
|
+
# In the editor: what did this save affect?
|
|
1428
|
+
ask watch . --once
|
|
1429
|
+
```
|
|
1430
|
+
|
|
1431
|
+
And two answer about the tool rather than the repository:
|
|
1432
|
+
|
|
1433
|
+
```bash
|
|
1434
|
+
ask self-audit --fail-on fail # does the INSTALLED build's registry agree with itself?
|
|
1435
|
+
ask selftest . # the published defect ledger, run against your repo
|
|
1436
|
+
```
|
|
1437
|
+
|
|
1362
1438
|
### MCP server (AI agent integration)
|
|
1363
1439
|
|
|
1364
1440
|
```bash
|
|
@@ -1443,6 +1519,16 @@ the **RIS**, builds the **shared Canonical IR** and fills the **parse cache**. I
|
|
|
1443
1519
|
`--exclude` change *what is analysed*, so they miss the warmed core and rescan — that is the
|
|
1444
1520
|
171s the field measured after a 103s warm.
|
|
1445
1521
|
|
|
1522
|
+
Select the root views needed by a workflow explicitly:
|
|
1523
|
+
|
|
1524
|
+
```bash
|
|
1525
|
+
ask cache warm . --views compact
|
|
1526
|
+
ask cache warm . --views compact,agent
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
Without `--views`, all declared root views are warmed. `cache model` reports the
|
|
1530
|
+
same per-view contract and names any view that was skipped.
|
|
1531
|
+
|
|
1446
1532
|
What invalidates what:
|
|
1447
1533
|
|
|
1448
1534
|
- **Any change to the analysed files** → every layer keys on one signature of the tree state:
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""access_drift.py — what changed in the effective access, and when.
|
|
2
|
+
|
|
3
|
+
`AUD-607-X08`. `posture --diff dev:prod` already answers *"do these two
|
|
4
|
+
environments expose different things?"*, and `baseline trend` already keeps a
|
|
5
|
+
series. Neither answers the question an operator actually gets paged about:
|
|
6
|
+
*what changed in prod's effective access since the last deploy?* — because the
|
|
7
|
+
first is a comparison of two profile sets at one instant, and nobody was
|
|
8
|
+
recording the instants.
|
|
9
|
+
|
|
10
|
+
One diff serves both axes. A snapshot is `route → access class` for a profile
|
|
11
|
+
set, so comparing two profile sets and comparing one profile set across time are
|
|
12
|
+
the same operation on different pairs, and the two answers can never disagree
|
|
13
|
+
about what "newly exposed" means.
|
|
14
|
+
|
|
15
|
+
**Direction is not symmetry.** A route that moved from a matched rule to
|
|
16
|
+
`no_rule_matched` is not the same event as the reverse, and collapsing both into
|
|
17
|
+
"changed" is how an alert that matters gets read as noise. The payload separates
|
|
18
|
+
`newly_unmatched` from `newly_matched`.
|
|
19
|
+
|
|
20
|
+
**A route that vanished did not become safe.** It may have been deleted, renamed
|
|
21
|
+
or moved behind a profile that is no longer active, and this analysis cannot tell
|
|
22
|
+
those apart — so `removed` is its own bucket and says so.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Optional
|
|
32
|
+
|
|
33
|
+
SCHEMA_VERSION = "1.0"
|
|
34
|
+
SCHEMA_ID = "access-drift-v1"
|
|
35
|
+
|
|
36
|
+
#: Stored snapshots share the baseline directory; the prefix keeps the three
|
|
37
|
+
#: series (architecture, findings, access) from reading each other's documents.
|
|
38
|
+
FILENAME_PREFIX = "access-"
|
|
39
|
+
|
|
40
|
+
#: The access class published when the posture found no rule deciding a route.
|
|
41
|
+
UNMATCHED = "no_rule_matched"
|
|
42
|
+
|
|
43
|
+
#: Keys of `effective_access` that are not access classes.
|
|
44
|
+
_NOT_A_CLASS = frozenset({"chains", "summary"})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _utc_now() -> str:
|
|
48
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def routes_from_posture(posture: "dict") -> "dict[str, str]":
|
|
52
|
+
"""`"VERB path" → access class`, from a posture payload.
|
|
53
|
+
|
|
54
|
+
The lists under `effective_access` are keyed by access class and hold
|
|
55
|
+
`VERB:path:controller:handler` strings. Only the verb and path identify the
|
|
56
|
+
route across two runs: a handler that moved class is the same route to a
|
|
57
|
+
caller, and keying on the handler would report a refactor as a new exposure.
|
|
58
|
+
"""
|
|
59
|
+
access = ((posture or {}).get("endpoints") or {}).get("effective_access") or {}
|
|
60
|
+
routes: "dict[str, str]" = {}
|
|
61
|
+
for access_class, value in access.items():
|
|
62
|
+
if access_class in _NOT_A_CLASS or not isinstance(value, list):
|
|
63
|
+
continue
|
|
64
|
+
for entry in value:
|
|
65
|
+
parts = str(entry).split(":")
|
|
66
|
+
if len(parts) < 2:
|
|
67
|
+
continue
|
|
68
|
+
verb, path = parts[0].strip().upper(), parts[1].strip()
|
|
69
|
+
if not path:
|
|
70
|
+
continue
|
|
71
|
+
routes[f"{verb} {path}"] = access_class
|
|
72
|
+
return routes
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def build_snapshot(
|
|
76
|
+
root: Path,
|
|
77
|
+
profiles: "set[str]",
|
|
78
|
+
*,
|
|
79
|
+
posture: "Optional[dict]" = None,
|
|
80
|
+
properties: "Optional[dict]" = None,
|
|
81
|
+
commit: "Optional[str]" = None,
|
|
82
|
+
) -> dict:
|
|
83
|
+
"""One capture of the effective access under a profile set."""
|
|
84
|
+
from sourcecode.architectural_baseline import git_commit
|
|
85
|
+
|
|
86
|
+
if posture is None:
|
|
87
|
+
from sourcecode.posture import build_posture
|
|
88
|
+
|
|
89
|
+
posture = build_posture(Path(root), set(profiles or ()), properties)
|
|
90
|
+
|
|
91
|
+
routes = routes_from_posture(posture)
|
|
92
|
+
by_class: "dict[str, int]" = {}
|
|
93
|
+
for access_class in routes.values():
|
|
94
|
+
by_class[access_class] = by_class.get(access_class, 0) + 1
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"schema_version": SCHEMA_VERSION,
|
|
98
|
+
"schema": SCHEMA_ID,
|
|
99
|
+
"kind": "access",
|
|
100
|
+
"captured_at": _utc_now(),
|
|
101
|
+
"commit": commit if commit is not None else git_commit(Path(root)),
|
|
102
|
+
"repo": Path(root).name,
|
|
103
|
+
"profiles": sorted(profiles or ()),
|
|
104
|
+
"counts": {
|
|
105
|
+
"routes": len(routes),
|
|
106
|
+
"by_access_class": dict(sorted(by_class.items())),
|
|
107
|
+
},
|
|
108
|
+
"routes": dict(sorted(routes.items())),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def snapshot_filename(snapshot: "dict") -> str:
|
|
113
|
+
profiles = "-".join(snapshot.get("profiles") or ()) or "default"
|
|
114
|
+
stem = snapshot.get("commit") or snapshot.get("captured_at", "capture")
|
|
115
|
+
safe = "".join(c if (c.isalnum() or c in "-_.") else "-" for c in f"{profiles}-{stem}")
|
|
116
|
+
return f"{FILENAME_PREFIX}{safe}.json"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def write_snapshot(snapshot: "dict", out_dir: Path) -> Path:
|
|
120
|
+
from sourcecode import readonly as _readonly
|
|
121
|
+
from sourcecode.architectural_baseline import protect_ask_dir
|
|
122
|
+
|
|
123
|
+
out_dir = Path(out_dir)
|
|
124
|
+
_readonly.guard(out_dir, what="access history directory")
|
|
125
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
protect_ask_dir(out_dir)
|
|
127
|
+
path = out_dir / snapshot_filename(snapshot)
|
|
128
|
+
path.write_text(
|
|
129
|
+
json.dumps(snapshot, indent=2, sort_keys=True, ensure_ascii=False) + "\n",
|
|
130
|
+
encoding="utf-8",
|
|
131
|
+
)
|
|
132
|
+
return path
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def load_series(out_dir: Path, *, profiles: "Optional[set]" = None) -> "list[dict]":
|
|
136
|
+
"""Stored access snapshots, oldest first, optionally for one profile set."""
|
|
137
|
+
out_dir = Path(out_dir)
|
|
138
|
+
if not out_dir.is_dir():
|
|
139
|
+
return []
|
|
140
|
+
series: "list[dict]" = []
|
|
141
|
+
wanted = sorted(profiles) if profiles is not None else None
|
|
142
|
+
for path in sorted(out_dir.glob(f"{FILENAME_PREFIX}*.json")):
|
|
143
|
+
try:
|
|
144
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
145
|
+
except (OSError, ValueError):
|
|
146
|
+
continue
|
|
147
|
+
if not isinstance(payload, dict) or payload.get("kind") != "access":
|
|
148
|
+
continue
|
|
149
|
+
if wanted is not None and list(payload.get("profiles") or ()) != wanted:
|
|
150
|
+
continue
|
|
151
|
+
payload["_source_file"] = path.name
|
|
152
|
+
series.append(payload)
|
|
153
|
+
series.sort(key=lambda s: str(s.get("captured_at") or ""))
|
|
154
|
+
return series
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def diff_snapshots(base: "dict", head: "dict", *, axis: str = "time") -> dict:
|
|
158
|
+
"""What moved between two access snapshots.
|
|
159
|
+
|
|
160
|
+
`axis` only labels the answer: `time` for one profile set across captures,
|
|
161
|
+
`environment` for two profile sets at one instant. The arithmetic is identical,
|
|
162
|
+
which is the point — the two can never disagree about "newly exposed".
|
|
163
|
+
"""
|
|
164
|
+
base_routes: "dict[str, str]" = dict(base.get("routes") or {})
|
|
165
|
+
head_routes: "dict[str, str]" = dict(head.get("routes") or {})
|
|
166
|
+
|
|
167
|
+
added = sorted(set(head_routes) - set(base_routes))
|
|
168
|
+
removed = sorted(set(base_routes) - set(head_routes))
|
|
169
|
+
shared = sorted(set(base_routes) & set(head_routes))
|
|
170
|
+
|
|
171
|
+
newly_unmatched: "list[dict]" = []
|
|
172
|
+
newly_matched: "list[dict]" = []
|
|
173
|
+
reclassified: "list[dict]" = []
|
|
174
|
+
for route in shared:
|
|
175
|
+
before, after = base_routes[route], head_routes[route]
|
|
176
|
+
if before == after:
|
|
177
|
+
continue
|
|
178
|
+
row = {"route": route, "from": before, "to": after}
|
|
179
|
+
if after == UNMATCHED:
|
|
180
|
+
newly_unmatched.append(row)
|
|
181
|
+
elif before == UNMATCHED:
|
|
182
|
+
newly_matched.append(row)
|
|
183
|
+
else:
|
|
184
|
+
reclassified.append(row)
|
|
185
|
+
|
|
186
|
+
added_rows = [{"route": r, "access": head_routes[r]} for r in added]
|
|
187
|
+
added_unmatched = [r for r in added_rows if r["access"] == UNMATCHED]
|
|
188
|
+
|
|
189
|
+
changed = (
|
|
190
|
+
len(added) + len(removed) + len(newly_unmatched)
|
|
191
|
+
+ len(newly_matched) + len(reclassified)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
"schema_version": SCHEMA_VERSION,
|
|
196
|
+
"schema": SCHEMA_ID,
|
|
197
|
+
"axis": axis,
|
|
198
|
+
"base": {
|
|
199
|
+
"captured_at": base.get("captured_at"),
|
|
200
|
+
"commit": base.get("commit"),
|
|
201
|
+
"profiles": list(base.get("profiles") or ()),
|
|
202
|
+
"routes": len(base_routes),
|
|
203
|
+
},
|
|
204
|
+
"head": {
|
|
205
|
+
"captured_at": head.get("captured_at"),
|
|
206
|
+
"commit": head.get("commit"),
|
|
207
|
+
"profiles": list(head.get("profiles") or ()),
|
|
208
|
+
"routes": len(head_routes),
|
|
209
|
+
},
|
|
210
|
+
"counts": {
|
|
211
|
+
"added": len(added),
|
|
212
|
+
"added_without_a_matching_rule": len(added_unmatched),
|
|
213
|
+
"removed": len(removed),
|
|
214
|
+
"newly_unmatched": len(newly_unmatched),
|
|
215
|
+
"newly_matched": len(newly_matched),
|
|
216
|
+
"reclassified": len(reclassified),
|
|
217
|
+
"changed_total": changed,
|
|
218
|
+
},
|
|
219
|
+
"drifted": bool(changed),
|
|
220
|
+
"added": added_rows,
|
|
221
|
+
"removed": [{"route": r, "access": base_routes[r]} for r in removed],
|
|
222
|
+
"newly_unmatched": newly_unmatched,
|
|
223
|
+
"newly_matched": newly_matched,
|
|
224
|
+
"reclassified": reclassified,
|
|
225
|
+
"non_coverage": [
|
|
226
|
+
"a route in `removed` did not become safe: it may have been deleted, "
|
|
227
|
+
"renamed, or moved behind a profile that is no longer active, and this "
|
|
228
|
+
"comparison cannot tell those apart",
|
|
229
|
+
"routes are identified by verb and path, so a handler that moved class "
|
|
230
|
+
"is the same route — a refactor is not reported as a new exposure",
|
|
231
|
+
],
|
|
232
|
+
}
|