sourcecode 5.8.32__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 +70 -0
- sourcecode/_docs/USER_GUIDE.md +58 -5
- sourcecode/access_drift.py +232 -0
- sourcecode/c4_render.py +245 -0
- sourcecode/cache_model.py +28 -0
- sourcecode/cli.py +1153 -4
- 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/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.32.dist-info → sourcecode-5.9.0.dist-info}/METADATA +61 -14
- {sourcecode-5.8.32.dist-info → sourcecode-5.9.0.dist-info}/RECORD +28 -17
- {sourcecode-5.8.32.dist-info → sourcecode-5.9.0.dist-info}/WHEEL +0 -0
- {sourcecode-5.8.32.dist-info → sourcecode-5.9.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.8.32.dist-info → sourcecode-5.9.0.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.8.32.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,76 @@ 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
|
+
|
|
19
89
|
**External `5.8.31` audit received, 2026-08-26 - `AUD-606`.**
|
|
20
90
|
[`AUDIT-2026-08-26-EXTERNAL-5.8.31.md`](AUDIT-2026-08-26-EXTERNAL-5.8.31.md)
|
|
21
91
|
confirms the new progress and shared-cache controls, and records one actionable
|
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
|
---
|
|
@@ -206,7 +206,7 @@ pipx install sourcecode # isolated install, no venv needed
|
|
|
206
206
|
|
|
207
207
|
# Verify
|
|
208
208
|
ask version
|
|
209
|
-
# ask 5.
|
|
209
|
+
# ask 5.9.0
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
Requires Python 3.9+.
|
|
@@ -251,7 +251,7 @@ estimate scaled by file count would be wrong in the direction that costs you a s
|
|
|
251
251
|
and an anchor printed without its release goes on recommending a nightly job for a command
|
|
252
252
|
that has come to finish in seconds (C3-97).
|
|
253
253
|
|
|
254
|
-
|
|
254
|
+
48 commands and seven command groups exist. Four of them carry most of the measured
|
|
255
255
|
value in field use, and they are the ones to learn first:
|
|
256
256
|
|
|
257
257
|
| Start with | Because |
|
|
@@ -1298,14 +1298,22 @@ explaining. The rest do one thing; `ask <command> --help` is the whole story.
|
|
|
1298
1298
|
| Command | Tier | What it does |
|
|
1299
1299
|
|---|---|---|
|
|
1300
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". |
|
|
1301
1306
|
| `ask explain <Class>` | supported | Human-readable architectural summary for one class (text by design, not JSON) |
|
|
1302
|
-
| `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. |
|
|
1303
1308
|
| `ask repo-ir` | supported | Deterministic symbol-level IR for the whole repository |
|
|
1304
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. |
|
|
1305
1311
|
| `ask prepare-context <task>` | supported | Task-shaped context: `onboard`, `delta`, `fix-bug`, `generate-tests`. See [Typical workflows](#typical-workflows) |
|
|
1306
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. |
|
|
1307
1314
|
| `ask rename-class` | supported | Word-boundary Java rename across the repository |
|
|
1308
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. |
|
|
1309
1317
|
| `ask archetype` | experimental | Evidence-based architectural archetype across four dimensions |
|
|
1310
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. |
|
|
1311
1319
|
| `ask baseline capture\|diff\|trend` | supported | Versioned architectural metrics over time; see [`ask trend`](#ask-trend-dir--how-the-architecture-moved) |
|
|
@@ -1382,6 +1390,51 @@ ask endpoints /repo --compact # the census without the rows: coun
|
|
|
1382
1390
|
ask endpoints /repo --output endpoints.json
|
|
1383
1391
|
```
|
|
1384
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
|
+
|
|
1385
1438
|
### MCP server (AI agent integration)
|
|
1386
1439
|
|
|
1387
1440
|
```bash
|
|
@@ -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
|
+
}
|