sourcecode 5.8.20__py3-none-any.whl → 5.8.21__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/_docs/DEFECT-LEDGER.md +231 -5
- sourcecode/_docs/USER_GUIDE.md +9 -2
- sourcecode/audit_report.py +18 -0
- sourcecode/baseline_autocapture.py +18 -1
- sourcecode/canonical_ir.py +41 -5
- sourcecode/cli.py +635 -85
- sourcecode/compare.py +3 -0
- sourcecode/defect_classes/__init__.py +19 -0
- sourcecode/defect_classes/registry.json +4 -5
- sourcecode/envelope.py +81 -0
- sourcecode/error_schema.py +5 -0
- sourcecode/explain_endpoint.py +7 -0
- sourcecode/migrate_check.py +211 -114
- sourcecode/non_coverage.py +31 -4
- sourcecode/output_bounds.py +13 -0
- sourcecode/partial_contract.py +21 -0
- sourcecode/path_filters.py +67 -4
- sourcecode/perf.py +54 -2
- sourcecode/phased_run.py +28 -0
- sourcecode/posture.py +35 -6
- sourcecode/prepare_context.py +91 -12
- sourcecode/release_info.py +1 -1
- sourcecode/rename_refactor.py +26 -8
- sourcecode/repository_ir.py +239 -24
- sourcecode/resource_budget.py +77 -0
- sourcecode/risk.py +139 -12
- sourcecode/schema_registry.py +6 -0
- sourcecode/schemas/endpoints-v1.schema.json +15 -0
- sourcecode/schemas/envelope-v1.schema.json +10 -0
- sourcecode/selftest.py +179 -24
- sourcecode/serializer.py +66 -6
- sourcecode/spring_findings.py +32 -8
- sourcecode/spring_security_audit.py +23 -11
- sourcecode/summarizer.py +148 -9
- sourcecode/validation_surface.py +8 -0
- sourcecode/verify_edit.py +242 -37
- sourcecode/verify_repo.py +22 -2
- {sourcecode-5.8.20.dist-info → sourcecode-5.8.21.dist-info}/METADATA +15 -6
- {sourcecode-5.8.20.dist-info → sourcecode-5.8.21.dist-info}/RECORD +44 -43
- {sourcecode-5.8.20.dist-info → sourcecode-5.8.21.dist-info}/WHEEL +0 -0
- {sourcecode-5.8.20.dist-info → sourcecode-5.8.21.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.8.20.dist-info → sourcecode-5.8.21.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.8.20.dist-info → sourcecode-5.8.21.dist-info}/licenses/NOTICE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -16,7 +16,93 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
16
16
|
|
|
17
17
|
## Current Synchronization
|
|
18
18
|
|
|
19
|
-
**Latest attached audits — audit of release `5.8.
|
|
19
|
+
**Latest attached audits — audit of release `5.8.20`, 2026-08-21, two independent rounds,
|
|
20
|
+
and the first pass since the sixth to open a `P1`.** Round A (MSAS, `banyan-v2` @
|
|
21
|
+
`3dde0376`, 3 342 Java files — the same subject bit for bit for the fourth consecutive
|
|
22
|
+
round — 44 commands, 110 invocations) scores **8.17/10 under a new seven-axis rubric that
|
|
23
|
+
is not comparable with the 8.95 of the previous pass**: determinism and CLI ergonomics
|
|
24
|
+
carry 25 % combined where the earlier rubric did not isolate them, and the round publishes
|
|
25
|
+
its own counterfactual under the old rubric at **≈8.7**. Round B (a 16-repository bank,
|
|
26
|
+
~245 invocations, 44 of 50 commands) scores **8.0/10 against 8.8**, and says in its own
|
|
27
|
+
words that the fall is *new surface, not drift*: WebFlux functional routing, the merge gate
|
|
28
|
+
against a real edit, byte-level determinism and the per-command budget contract had never
|
|
29
|
+
been probed. **Both rounds record zero tracebacks across ≈355 invocations, zero unauthorised
|
|
30
|
+
writes over 17 of 17 trees, and — apart from one row — zero content regressions.** The
|
|
31
|
+
ninth-pass queue is verified closed from outside, 8 of 9 by Round B and 7 of 7 by Round A;
|
|
32
|
+
the one exception is `build_commit`, **reopened below on its sixth witness**.
|
|
33
|
+
|
|
34
|
+
**Three rows lead the queue, and the order is regression first, then the two broken
|
|
35
|
+
promises.** `AUD-596-R01` is the only true regression either round sustains: `migrate-check`
|
|
36
|
+
at **×1.93** over eight samples with 11 % dispersion, on a command that is cache-independent
|
|
37
|
+
by its own model, so the *"warm that was not warm"* failure mode that retracted two
|
|
38
|
+
regressions last pass does not apply here. `AUD-596-B01`: the two commands sold as CI gates
|
|
39
|
+
pass a pull request that opens two unauthenticated routes — one returning arbitrary
|
|
40
|
+
environment variables — and `blast_radius` does not merely miss them, it publishes
|
|
41
|
+
`affected_entry_points: 0` at `confidence: "medium"`, a confident zero in the product built
|
|
42
|
+
against confident zeroes. `AUD-596-B02`: on functional routing one route population ships as
|
|
43
|
+
four different numbers, and two payload notes assert identities (`endpoints_analyzed ==
|
|
44
|
+
semantic_model_endpoints`, `endpoints_analyzed <= endpoints.total`) that are false in the
|
|
45
|
+
field — the thesis of the user guide, measured.
|
|
46
|
+
|
|
47
|
+
**Queue status after 5.8.21: those three rows and `AUD-596-A01` are closed, one commit
|
|
48
|
+
each, and the queue's own leading claim did not survive contact.** `AUD-596-R01` **does not
|
|
49
|
+
reproduce**: A/B on the reported repository, six passes per version on one machine, gives
|
|
50
|
+
ratio **1.00** (3.475 s against 3.480 s), and the two smaller anchors agree — the ×1.93 is
|
|
51
|
+
not a release, and the absolute figures differ from the report's by ~3x on *every* version,
|
|
52
|
+
which is a machine. What the row asked for as its own precondition did ship: `migrate-check`
|
|
53
|
+
now publishes per-phase `timings` accounting for 99.4 % of its clock, so the next such claim
|
|
54
|
+
is checkable from the payload instead of from a stopwatch. `B24` closes with it on the same
|
|
55
|
+
call site. `AUD-596-B01` and `AUD-596-A01` closed in full, and `A01` turned out to be
|
|
56
|
+
**inflation and not only drift** — 683 published where 668 beans exist. `AUD-596-B02` closed
|
|
57
|
+
the figure that carried the severity (0 → 173) and **deliberately did not take** the half
|
|
58
|
+
that would have collapsed the remaining 158/173 delta: measured, that change costs
|
|
59
|
+
`review-pr` 10.8 s → over 200 s, so the delta is published with its basis instead. Two of
|
|
60
|
+
`B02`'s four mechanisms were found by the battery rather than by the report, and one report
|
|
61
|
+
claim (`validation` 198) is corrected against source: it is a different population the
|
|
62
|
+
payload already names. Details, with the numbers, in each row below.
|
|
63
|
+
|
|
64
|
+
**Second batch after 5.8.21: the eleven `P2` rows and four of the fourteen `P3`s are
|
|
65
|
+
closed, one commit each — fifteen rows, `B16` · `B03` · `B10` · `A03` · `X01` · `B06` ·
|
|
66
|
+
`B07` · `A02` · `A04` · `X02` · `B09` · `D05` · `D07` · `D13` · `D02` — with the suite at
|
|
67
|
+
**11 410 passed, 22 skipped, 0 failed** and no regression at any step.** Three closures
|
|
68
|
+
carry a **refusal with its price** rather than a half-made change: `X02` does not serve a
|
|
69
|
+
bounded substitute mid-emit (that is the shared emit contract, not this row's two
|
|
70
|
+
surfaces, and the waste is re-measured: 44 675 ms then a refusal, and warm the remedy
|
|
71
|
+
costs what the refusal cost); `B09` does not add a rule that *creates* a finding from a
|
|
72
|
+
concatenated sink (a new claim rather than a corrected one — it earns its place with a
|
|
73
|
+
false-positive measurement over the corpus); and `B09` keeps `update`/`execute` out of the
|
|
74
|
+
sink vocabulary on the record, so the row's third witness stays invisible by decision.
|
|
75
|
+
**Four mechanisms were found by the battery rather than by the report**, each worse than
|
|
76
|
+
filed: `A02`'s predicate found no controller on *every* repository, not only on the
|
|
77
|
+
monolith, because the bounded view keeps the census and empties the rows (`dubbo`: 5 rows
|
|
78
|
+
in the full view, 0 in `--compact`); `D07`'s empty string never reached the parser at all,
|
|
79
|
+
because `posture` gated on `if profile`; `A03`'s two authorities differed by an entire
|
|
80
|
+
suffix layer that had never left `verify-edit`; and `X02`'s `--compact` reached the bean
|
|
81
|
+
lists while the endpoint lists under `effective_access` — the ones that make the answer
|
|
82
|
+
large — were outside the bound. **Three claims are corrected against measurement**: `1e9`
|
|
83
|
+
is not an invalid budget (`B03`), `stack_fit` is *absent* rather than `null` (`D13`), and
|
|
84
|
+
`risk --help`'s concatenation promise failed on the vocabulary rather than on the analysis
|
|
85
|
+
(`B09`). `B07` is the only closure verified across a **37-repository corpus**: 5 project
|
|
86
|
+
summaries changed, all from junk to the project's own description, 32 unchanged.
|
|
87
|
+
|
|
88
|
+
**No D rows remain open.** `D01`, `D03`, `D08`, `D10`, `D11`, `D12` and `D14` were
|
|
89
|
+
closed on 2026-08-22 by the fix commits recorded in their rows. `D04`, `D06` and `D09` were
|
|
90
|
+
already closed by commits `040f027`, `784cd83` and `fd3d63f`. The third face of `X02` and
|
|
91
|
+
the rule half of `B09` remain refused above with their measurements rather than left silent.
|
|
92
|
+
|
|
93
|
+
**Two reported mechanisms are corrected before filing, and half of one row is refuted.**
|
|
94
|
+
`migrate-check` does not *"appear in the budget authority and implement nothing"*: two tiers
|
|
95
|
+
exist in code — `analysis_budget.BUDGETED_ANALYSIS_COMMANDS` (8 commands) and
|
|
96
|
+
`phased_run.BUDGET_RUNNER_COMMANDS` (3), with `budget_scope()` already answering
|
|
97
|
+
`phase_runner` against `advisory_only` — and the defect is that **only one of the two tiers
|
|
98
|
+
is ever published**. The gating half is worse than reported rather than better: of the three
|
|
99
|
+
commands that do phase-run, `risk` and `audit-report` publish a `gate_exit_code_basis`
|
|
100
|
+
naming `--ci` and `--allow-partial` and **declare neither flag**. And `--profile NOPE` does
|
|
101
|
+
not compute silently: `posture.py:1394-1396` publishes `profiles_not_declared`; what survives
|
|
102
|
+
of that row is the empty string, which the parser documents as a different answer from *no
|
|
103
|
+
flag* and every consumer then flattens back into it.
|
|
104
|
+
|
|
105
|
+
**Previous attached audits — release `5.8.18`, 2026-08-21, two independent rounds,
|
|
20
106
|
the first pass in this record that closes with no P0 and no P1 open, and **closed end to
|
|
21
107
|
end in `5.8.19`**.** Round A (MSAS,
|
|
22
108
|
3 342 Java files, the same HEAD bit for bit for the third consecutive round, 18 commands,
|
|
@@ -32,7 +118,7 @@ shipped ledger still marked `open` — which is itself a row (`AUD-594-X04`) and
|
|
|
32
118
|
in place below. **The ninth-pass queue is closed in `5.8.19`: twelve rows, one atomic
|
|
33
119
|
commit each, with every regression assertion written over the catalogue that already
|
|
34
120
|
exists rather than over the reported symbol — the rule three passes of evidence now
|
|
35
|
-
support.
|
|
121
|
+
support. That queue is closed; the open queue in this ledger is the tenth pass's.**
|
|
36
122
|
|
|
37
123
|
**What is left has moved once more, and it is no longer a defect of design.** The seventh
|
|
38
124
|
pass ended with contracts that were *absent*; the eighth with contracts that *contradicted
|
|
@@ -109,6 +195,144 @@ with neither parseable stdout nor its requested artifact) or `BUG-6`
|
|
|
109
195
|
queue position. `BUG-1` and `BUG-5` gain witnesses on public OSS repositories, recorded
|
|
110
196
|
under their own rows rather than as new ones.
|
|
111
197
|
|
|
198
|
+
### Tenth Audit Pass: `5.8.20` findings / MSAS (round A) + 16-repository bank (round B) / 2026-08-21
|
|
199
|
+
|
|
200
|
+
**Open queue.** Ordered as the correction order is: the regression first, then the two rows
|
|
201
|
+
that break a promise the product sells, then contract and cost, then declaration. Round A's
|
|
202
|
+
ids are `ASK-*`, Round B's are `B-*`; where both rounds found one defect the row carries
|
|
203
|
+
both, and where a finding is a new witness for a row this ledger already holds it is named
|
|
204
|
+
in *Mapped, not new* below rather than filed twice.
|
|
205
|
+
|
|
206
|
+
**Scores, and why they are not comparable with the last pass.** Round A: **8.17/10** over
|
|
207
|
+
seven axes (correctness 25 %, surface coverage 15 %, determinism 10 %, performance 15 %,
|
|
208
|
+
CLI ergonomics 15 %, agent consumability 10 %, isolation 10 %), where the previous rubric
|
|
209
|
+
had five and isolated neither determinism nor ergonomics — the same evidence under the old
|
|
210
|
+
rubric is **≈8.7** against 8.95, and the round says so in its own §4. Round B: **8.0/10**
|
|
211
|
+
against 8.8, and its §1 attributes the fall to four surfaces never probed before rather
|
|
212
|
+
than to drift. **The analysis is again not the weak part**: Round A verifies 7 of 7 claims
|
|
213
|
+
by hand, four exact to file and line, including the one where `grep -c` is wrong and the
|
|
214
|
+
product is right (`ProcesoNominaRestController` = 48, because the class declares two paths
|
|
215
|
+
and `route_census.expansion_rows: 1120` says so); Round B verifies 7 of 7 controls, two
|
|
216
|
+
exact to line, and its injected-defect probe produces four correct findings with the
|
|
217
|
+
container semantics nobody else models (`TX-001`: *a CGLIB proxy cannot intercept a private
|
|
218
|
+
method, so `@Transactional` is silently ignored*).
|
|
219
|
+
|
|
220
|
+
**What the queue does not contain.** Zero tracebacks in ≈355 invocations; zero unauthorised
|
|
221
|
+
writes across 17 trees (16 in the bank, MSAS in round A), verified with `git status
|
|
222
|
+
--ignored --short` before and after; every one of 42 non-zero exits carrying the typed
|
|
223
|
+
envelope; 25 of 25 error paths under 0.9 s; determinism byte-for-byte on six commands in
|
|
224
|
+
round B and five runs of `endpoints --compact` — including `--no-cache` and `--jobs 2` — in
|
|
225
|
+
round A. `posture` is the exception, and it is `AUD-596-A01`.
|
|
226
|
+
|
|
227
|
+
| ID | Severity | Current status | Required direction |
|
|
228
|
+
|---|---|---|---|
|
|
229
|
+
| `AUD-596-R01` / `B-05` | **P1 — regression** | **closed 5.8.21 — the regression does NOT reproduce, and what shipped is the half this row filed as its own precondition (`c909d18`).** A/B on the reported repository (spring-security @ tip, 4 415 `.java` against the row's 4 403), six passes each, one machine, `PYTHONPATH` pinned per worktree so the editable `.pth` cannot measure HEAD twice: **5.8.18 median 3.475 s against HEAD median 3.480 s, ratio 1.00** (5.8.18: 4.13 3.62 3.48 3.47 3.47 3.47 · HEAD: 3.52 3.54 3.43 3.44 3.47 3.55), dispersion 1.19 and 1.03, inside the 1.25 the protocol requires. The row's two smaller anchors agree: dubbo (4 049) 5.25 s on both versions, halo (1 349) 1.78 s on both. The absolute figures differ from the report's by ~3x on **every** version, which is a machine and not a release; the ratio is version-to-version on one machine and it is 1.00. Diffing 5.8.18..HEAD over the command's path agrees: `migrate_check.py` is untouched and the `cli.py` hunk inside `migrate_check_cmd` is `--top-n` reaching the JSON format (`S-04`), which selects after the scan. The second acceptance half — *publish the new anchor with its provenance* — is those numbers. **The precondition shipped**: `run_migrate_check` takes an optional `perf.PhaseTimings`, nine phases are recorded, and `metadata.timings` publishes them with the same unit and `unaccounted_ms` doctrine as `spring-audit` — on spring-security it accounts for **99.4 %** of the clock (`java_source_scan` 68 %, `hibernate_stratification` 14 %, a second full read of every `.java`). The recorder is the caller's, so a run that asks for nothing is timed for nothing and the default payload stays byte-identical. Regression `tests/test_migrate_check_timings_aud596_r01.py`, 4 tests. *Original note:* **open.** The only true regression either round sustains. `migrate-check ./spring-security` (4 403 Java files): **6.02 s on 5.8.18 → 11.6 s median on 5.8.20, ×1.93**, eight samples (10 244–13 298 ms, 11 % dispersion, over the ×1.25 threshold), `--compact` identical (11 792 / 11 613 / 11 732). The measurement survives the failure mode that retracted two regressions last pass: this command is cache-independent in the product's own `cache model`, so *warm that is not warm* cannot explain it. Scaling is linear and the slope moved: 524 files → 1.59 s, 1 349 → 4.04 s, 4 403 → 11.6 s, ≈2.6 ms/file against ≈1.37 ms/file implied by the 5.8.18 anchor. Round A sees the same shape from the other side — `migrate-check --compact` on MSAS costs 9.0 s cold and **18.9 s then 20.6 s warm**, which is not explained by the command gaining nothing from a warm cache. | Return to ≤7.5 s (×1.25 of the anchor) or publish the new anchor with its provenance. **`B24` is the precondition**: `migrate-check` is an advisory-only member of the budget tuple, runs no phases and therefore publishes no per-phase `timings`, so there is nothing to read from outside — instrumenting it is the first half of this fix, not a separate row. |
|
|
230
|
+
| `AUD-596-B01` / `B-01`, harder witness for `BUG-6` | **P1** | **closed 5.8.21 — both defects (`567d09d`); `B-30` stays open.** Reproduced exactly before the fix (`verdict: pass`, `exit_code: 0`, `reasons: []`, six axes `changed: false`) and closed as two. **(1)** `route_surface_changed` is the missing axis, diffing `cir.endpoints` — the one authority every endpoint surface projects from, so it covers every routing style the model admits rather than the annotation style somebody would have thought to assert — keyed on the handler, so a verb or path move reads as *moved* and not as one removal plus one addition. Three-way, following the `build_config_changed` precedent: **True** (break, exit 1) when a published route is removed or moved, **None** (unverified, exit 2) when routes are added and none removed, because a new public endpoint is a runtime-behaviour change and whether the surface was meant to widen is not decidable from a source diff, and **False** only when the two populations were computed and compared. Every route in the verdict carries the access policy it declares of its own, with a `basis` saying that is the endpoint's own declaration and not the request chain — the axis does not inherit the confident zero it was written to remove. **(2)** `_blast_radius` unions two populations by handler, reached and created, with a `basis` naming both: the fixture goes `0` / `confidence: medium` → **2 / high**. The created half is identified against HEAD's route population rather than by matching a changed path against `endpoint.source_file` — that field is optional on `route_surface` and empty on the reproduction repository, so a `source_file` join would have shipped this half doing nothing. With no HEAD model the created count is `None` with its reason and `affected_entry_points` is declared a floor. The clean-tree short circuit now builds its clean axes from `_BLOCKING_AXES` instead of a second hand-written tuple. Regression `tests/test_verify_edit_route_surface_aud596_b01.py`, 9 tests, parametric over routing **styles** (`S-01`) with a HEAD route per style so a style the model cannot read fails on the HEAD half rather than passing green. ⚠ **`B-30` is not closed here**: `verify --init` still derives 20 `forbid_finding` rules and not one surface contract. *Original note:* **open.** A new file with `GET /internal/dump-config` returning `System.getenv(key)` from a `@RequestParam` and `POST /internal/exec` leaves `verify-edit` at `verdict: pass`, `exit_code: 0`, `reasons: []`, all six axes `changed: false`, and `verify` at 0 — while `endpoints` on the same tree goes 17 → 19. **Verified in source, and it is two defects.** (1) `_BLOCKING_AXES` (`verify_edit.py:507-510`) holds no route-population axis, and `_security_axis` (`:665-705`) diffs `SEC-*` finding ids only, so a route that raises no finding is `changed: False` — *computed clean*, not *not measured* — and the payload carries no `non_coverage` to say the population is out of scope, unlike `posture`, `endpoints` and `spring-audit`. (2) `_blast_radius` (`:873-906`) counts entry points **reached by** the changed files, so a new file that **is** an entry point yields `{"affected_entry_points": 0, "confidence": "medium"}`: a confident zero, in the command whose own doctrine forbids one. The only honest signal on that tree comes from `posture` (`{total: 19, guarded_by_own_annotation: 0, depends_on_request_chain: 19}`) and no gate consumes it. `B-30` travels with this row: `verify --init` prints *"Endpoints seen: 17 · types seen: 39"* in the header of the contracts it derives and then derives 20 `forbid_finding` rules and not one surface contract — the gate cannot break on something the deriver never declared. | An axis `route_surface_changed` diffing the route population of HEAD against the working tree — added / removed / verb or path changed, each with its access policy — and `blast_radius.entry_points` including routes the edit **creates**. If the decision is not to cover it, `verify-edit` publishes `non_coverage` saying so and the help stops promising *"did the working-tree edits change runtime behavior?"* unqualified. Regression written over `coverage.detected_styles`, not over `annotation` (`S-01`). |
|
|
231
|
+
| `AUD-596-B02` / `B-02`, class residual of `R2` | **P1** | **closed 5.8.21 on the figure that carried the severity; one half deliberately not taken, with its measurement (`aa8b0df`).** `endpoints.semantic_model_endpoints` on halo: **0 → 173**, equal to `spring-audit.metadata.endpoints_analyzed` (173) and `posture.endpoints.total` (173). Controls unchanged: mall 246, spring-security 3. **The whitelist is gone.** `_SEMANTIC_MODEL_ENDPOINT_SOURCES` was a hand-written catalogue of admission classes living beside `canonical_ir`'s own admission rule and never compared against it; `canonical_ir.route_is_semantically_modeled()` is now that rule, extracted from the projection so there is one implementation, and `modeled_route_ids()` deduplicates by endpoint id exactly as the projection does. The modelled figure is measured over the surface the model is actually built from, assembled from the same three producers `build_repo_ir` uses. **A tag was on the wrong object**, found by the battery and not by the report: `test_util` scope went on the endpoint entry and not on the route, so the admission rule — which reads `route["scope"]` — could not see it. Notes are generated: `semantic_model_endpoints_by_source` names what reached the model on this repository, `semantic_model_vs_total` publishes `{semantic_model_endpoints, total, delta, basis}` so the relation is computed rather than asserted, and `spring-audit`'s `endpoints_analyzed_note` stops claiming `<= the endpoints command total` (it was 173 against 158) and stops naming three styles whose `detected_styles` rows read 0. ⚠ **Not taken, and this is the open half**: one route surface for both callers — moving the functional-route extension from `build_repo_ir`'s call site into `_build_route_surface` — would bring `endpoints.total` to 173 and close the delta rather than publish it. It was built and withdrawn: measured, it costs `review-pr . --since HEAD~3` **10.8 s → over 200 s** on this repository, single-threaded at 99 % CPU, reproducible, cause not yet identified. A 17x regression is not the price of collapsing a delta the payload now states honestly. ⚠ **One report claim corrected against source**: `validation.endpoints[] = 198` is not a fourth answer to the route question — it is `spec_derived_routes` from a standalone OpenAPI spec on disk, and the payload already declares it (`summary.spec_derived_routes: 198`, `endpoints_with_body_basis`, and an `axis_note` separating it from `body_endpoints_in_code: 81`). A different population, named, with its basis. Regression `tests/test_endpoint_population_parity_aud596_b02.py`, 26 tests, parametric over `coverage.supported_styles` read off the shipped registry (`S-01`), one fixture per style, failing when a supported style has no fixture. The pre-existing `test_semantic_model_subset_is_the_declared_source_set` asserted the whitelist against itself, which is why it stayed green while the whitelist said the model held 0 of halo's 173; rewritten to assert the measured population. *Original note:* **open.** On `halo` (1 349 Java files, routing 100 % WebFlux functional) one route population ships as four numbers: `endpoints.total` 158 (`by_source.webflux_functional: 158`), `endpoints.functional_routing.modeled_routes` 173, `posture.endpoints.total` 173, `spring-audit.metadata.endpoints_analyzed` 173, `validation.endpoints[]` 198, and `endpoints.semantic_model_endpoints` **0**. Two published identities are therefore false in the field: `endpoints_analyzed_note` says the value *"is <= the `endpoints` command total"* (173 ≤ 158) and `semantic_model_endpoints_note` says it *"is the count `spring-audit` reports"* (0 ≠ 173); a third describes the 173 as *"Spring-MVC/JAX-RS annotated handlers plus OpenAPI-spec-recovered routes"* while `coverage.detected_styles` in the same payload reads `spring_mvc_annotations: 0, jax_rs: 0, openapi_spec: 0`. **Mechanism in source:** `_SEMANTIC_MODEL_ENDPOINT_SOURCES = {"annotation", "openapi-spec", "jax_rs"}` (`repository_ir.py:7508-7510`) is a hand-written whitelist that excludes `webflux_functional`, while the CIR route surface admits those same routes (`:5780-5795`, `:8423`) and the security model classifies all 173 of them; the notes are hand-written beside it (`repository_ir.py:8494`, `spring_security_audit.py:1429`). Both control repositories agree the defect is functional-routing-only: `mall` gives 246 four times, `spring-security` 3. **The severity is that the discrepant population is the security one** — `spring-audit` claims to have analysed 173 routes for security on a repository whose semantic model indexes 0 of them. | One catalogued figure for the route population, every other derived from it, and every note generated from the catalogue rather than written beside it. If `webflux_functional` reaches the security model it belongs in `semantic_model_endpoints`; if it does not, `endpoints_analyzed` must be 0 with the reason. Parity test per member of `coverage.detected_styles`, one fixture per style, `validation` included (198 over 158). |
|
|
232
|
+
| `AUD-596-A01` / `ASK-DET-001` | **P1** | **closed 5.8.21 — and the figure was not only unstable, it was inflated (`fa508b4`).** The loop counted graph NODES whose `fqn` happens to name a Spring bean, and the graph carries the same `fqn` more than once: on shenyu **683 bean-node rows over 668 distinct bean definitions**, fifteen duplicates, each a type declared once reaching the node list twice — so `population_total` published 683 beside a `BeanGraph.spring_beans()` catalogue computed in the same run that held 668. thingsboard shows the same shape at graph level, 1 104 duplicated fqns over 28 834 nodes. The population is closed before it is counted: one entry per bean definition, ordered by (fqn, file, line) **at population close**, not at serialisation. Measured after, three runs each: shenyu **683 → 668** stable, halo 354 stable, `unconditional` stops moving with it. **The durable half shipped and it is the reason three passes could not see this**: the four drifting payloads weighed 142 852 B exactly — 1908 and 1904 occupy the same bytes — so the size-identity regression test was structurally incapable of catching it. `_meta.content_id` is a fingerprint of the answer with clocks and run conditions removed, attached in `attach_meta` where identity already lives, so no command can forget it; `run_id` cannot do this job, being built from the INPUTS and equal across two runs that disagree. The excluded set is borrowed from `regress._CLOCK_KEYS` + `_CONDITION_FIELDS` rather than restated — including `cache_layers` made a cold run and a warm run of one analysis report drift, the false positive that would have made the signal worth ignoring. Regression `tests/test_posture_population_determinism_aud596_a01.py`, 9 tests, including the injected duplicate node, reversed node order, and the equal-serialised-length control on the fingerprint. ⚠ **Not taken**: the `selftest` row the required direction asks for — its precondition is `AUD-596-A02`, which was not in this queue. *Original note:* **open.** `posture.summary.population_total` is not deterministic on one unchanged tree: **1908 / 1904 / 1904 / 1908** in four runs at `3dde0376`, with `unconditional` moving in lockstep (1903/1899/1899/1903) and `conditional_beans` fixed at 5 — so the drift is confined to the non-conditional bean discovery, Δ = 4 constant, with no correlation to warm or cold and no concurrency between invocations. `population` and `population_unit` are byte-identical strings across the four, which makes this the literal case `AUD-590-B02` forbids: two figures sharing a `(unit, population)` and disagreeing. **And the four payloads weigh 142 852 B exactly**, so the size-identity check three consecutive passes used as their regression test cannot see it — 1908 and 1904 occupy the same bytes. `cache model` states that every layer keys on the exact tree state, so the answer must not move. | Order the bean-definition aggregation before it is counted — at population close, by FQN + file + line, not at serialisation — and publish the population as an ordered list derived from a catalogue. Second half, and the durable one: **a `content_id` on every payload** (`spring-audit` already publishes one), so *"no drift"* stops being an assertion about payload size. Assertion: every key with a row in the figure catalogue holds one value over N runs at one `tree_state`, and it is a `selftest` row — which makes `AUD-596-A02` its precondition. |
|
|
233
|
+
| `AUD-596-A02` / `ASK-SELF-001` | **P2** | **closed 5.8.21 (`4dfadc8`), and the second mechanism is worse than the round reported.** `_busiest_controller` is the one place either row chooses its subject: it reads `endpoints.exposure.by_controller`, the census the command publishes in **both** views so the ceiling cannot refuse it, and keeps three outcomes apart that were collapsing into one — a subject, an error naming the exit code, and a repository that genuinely publishes no controller. When the census contradicts the skip (`distinct: 446` with nothing listed) the row **fails**, because a reason one surface of this binary refutes is not a reason to skip. **The mechanism the report did not have**: the bounded view keeps the census and empties the rows, so the predicate beside the row found nothing on *every* repository, large or small — measured on `dubbo`, 5 rows carrying a `controller` in the full view, 0 in `--compact`, one controller in the census in both. Measured after: both rows answered `not_applicable` on `dubbo` before and both `pass` now, on `org.apache.dubbo.spring.boot.actuate.endpoint.mvc.DubboMvcEndpoint`. Regression `tests/test_selftest_chooses_its_subject_from_the_census_a02.py`, 7 tests. *Original note:* **open.** Two of the three `selftest` rows added in `5.8.19` return `not_applicable` with a reason that another command of the same binary refutes on the same tree: `AUD-590-B02` → *"no controller in this repository to measure fan-in against"* against `endpoints.exposure.by_controller.distinct: 446`, and `AUD-594-X03` → *"no symbol in this repository to query"* against an `impact-chain` that publishes `direct_caller_count: 554` with its `_unit` and `_population`. The criterion the rows were written under is *"a green run over nothing is how a defect gets closed twice"*, and `not_applicable` over a repository that does offer the subject is that run. **The row that would have caught `AUD-596-A01` is one of the two.** Field verification cited for these rows was on `halo`, not on a monolith whose controllers declare their paths in a class-level `@RequestMapping` with no verb-specific annotation (MSAS: 1 067 `@RequestMapping(method=…)`, zero `@GetMapping`). | Derive the subject from the census `endpoints` already publishes (`exposure.by_controller`), never from a predicate beside the row; and turn `not_applicable` into `fail` when another surface of the same binary contradicts the stated reason. Direct input to `S-03`. |
|
|
234
|
+
| `AUD-596-X01` / `ASK-GATE-001`, gating half of `B24` | **P2** | **closed 5.8.21 (`e59c655`).** `risk` and `audit-report` declare `--ci` and `--allow-partial` from one factory — same spelling, same help — and `partial_contract.gate_process_exit_code` is the one rule the three `BUDGET_RUNNER_COMMANDS` share. What `--ci` means on the two new commands is **stated rather than assumed**: they gate on *completeness*, because neither publishes a pass/fail verdict of its own and completeness is exactly the fact the shared basis describes. A findings gate for `risk` would be a new policy, not this row; the help says so where a reader meets the flag, since a gate whose meaning has to be guessed is this defect one level up. `_partial.exit_code` is now the code the process actually leaves with rather than a literal `0`, and for `audit-report` the stamp happens before the signature, so a bundle built from a cut `--from-risk` file no longer inherits the other process's decision. The answer is written before the gate is read on both commands. Measured: `risk --ci` under a cut exits **75**, `--ci --allow-partial` 0, no `--ci` 0, a complete `--ci` 0. The test population is `BUDGET_RUNNER_COMMANDS`, so a fourth phase-running command joins the sweep by existing. Regression `tests/test_gate_basis_names_flags_that_exist_x01.py`, 12 tests. *Original note:* **open, and the tier half is not filed here — it is `B24`, open since the 5.8.7 audit, re-witnessed in place with the same corrected mechanism this pass derived independently.** **What is new here, and it is worse than the round reported.** `AUD-595-A03` closed the cut-run gate on `spring-audit` and published its declaration from a shared module — so the two other commands that phase-run inherited the *declaration* and not the *flags*. Of the three commands that do phase-run, only `spring-audit` declares `--ci` and `--allow-partial` (`cli.py:10172`, `:10177`); `risk` and `audit-report` publish `GATE_EXIT_CODE_BASIS` (`partial_contract.py:85-91`) — a shared constant naming both flags — and reject both: `ASK_MAX_ANALYSIS_SECONDS=1 ask risk . --ci` exits 2 with `No such option: --ci`, while the same run's payload publishes `gate_exit_code: 75, exit_code: 0`. Both commands exhausted a 300 s budget on MSAS (301.2 s and 312.3 s) and `audit-report` writes *"Total defects: 0 (floor — this run was cut short)"* into its markdown under exit 0. | Move `--ci` / `--allow-partial` to the shared decorator of the `BUDGET_RUNNER_COMMANDS`, **or** generate `gate_exit_code_basis` from the flags the emitting command actually declares — a basis that names a flag its own command rejects is the `AUD-594-X01` shape with the generator on the other side. |
|
|
235
|
+
| `AUD-596-X02` / `ASK-UX-001` + `ASK-UX-002` + `B-21` | **P2** | **closed 5.8.21 on two of its three faces (`f6ebf50`); the third is refused with its price.** (1) Each recommended line carries the bounding flag its own command declares — the intersection of that command's declared options with `output_ceiling`'s vocabulary, the same intersection the refusal offers — so `posture . --diff dev:prod --compact` and `endpoints . --compact` are what the front page recommends, and the block says the lines are bounded and how to get the whole answer. (2) `POSTURE_BOUNDED_LISTS` held the bean lists and not the endpoint lists under `effective_access`, so on a repository whose *undecided* population is what makes the answer large the flag reached nothing; `undecided`, `no_rule_matched` and the three `chains/*_files` lists join it. Measured on thingsboard (3 714 Java files), `posture . --diff dev:prod`: **136 843 B** unbounded, **38 775 B** with `--compact` before, **31 701 B** after, with `undecided_cap: {total: 33, shown: 0, omitted: 33}` declaring the cut. The `ASK-10` battery grows from *declares a flag* to *the flag bounds the answer*, measured at every depth a `--diff` payload has. **(3) Not taken:** *estimate before serialising, and serve the bounded variant in the same pass*. The waste is real and re-measured — `posture ./thingsboard` costs 44 675 ms and then refuses, and warm the remedy costs what the refusal cost (4 828 ms against 4 877 ms) — but serving a substitute answer mid-emit is a change to the emit contract every command shares, not to this row's two surfaces. It stays open with the measurement rather than half-made. Regression `tests/test_the_bounding_flag_bounds_the_path_x02.py`, 13 tests. *Original note:* **open.** Three faces of one mechanism: the output ceiling is known to the product and not consulted by the surfaces that recommend and remedy. (1) The generated *"Start here — ordered for this repository (3 342 Java files, cache warm)"* block lists three commands under **"Runs now"** and two of them exit 1 here, both with `OUTPUT_TOO_LARGE`: `posture . --diff dev:prod` (~71K estimated tokens over a 50K ceiling) and `endpoints .` (~612K). The generator knows the repository size and the ceiling; *"Runs now"* is false by construction for two of three, on the first screen an evaluator reads. (2) The remedy the error itself proposes is a no-op on that command: `posture --diff dev:prod --compact` returns the **identical** `estimated_bytes: 284464` and `estimated_tokens: 71116`, and the hint machinery is honest enough to record `flags_already_passed: ["--compact"]` and stop offering it — while nothing says the flag does not bound this path. Contrast `endpoints`, where `--compact` takes 612K estimated tokens to 11 681 B. **This half is the class residual of `ASK-10`**: its invariant is derived from the CLI registry and asserts that a command able to exceed the ceiling *declares* a bounding flag — `posture` declares `--compact` and passes, and nothing measures whether the flag bounds the path it was passed on. (3) The ceiling is applied after the analysis, so the refused answer is paid for: `posture ./thingsboard` costs 44 675 ms and then refuses; warm, the remedy costs the same as the refusal (4 828 ms against 4 877 ms). | Filter the generated recommendation by the ceiling the product already computes, or attach the bounding flag to the recommended line. Either `--compact` bounds `posture --diff` — the two-profile diff is precisely the case that wants a summary — or the hint declares that it does not, and `tests/test_bounding_flag_registry_ask10.py` grows from *declares a flag* to *the flag bounds the answer*: for each member of the population, a payload over the ceiling must come back under it with the declared flag, per path the flag reaches. Estimate before serialising, and serve the bounded variant in the same pass, announcing the substitution. |
|
|
236
|
+
| `AUD-596-B06` / `B-06` | **P2** | **closed 5.8.21 (`8107927`).** `bounded_view` publishes the delta computed from the two row shapes over the answer being served — never from a list beside them — in the `sibling_view.omitted_blocks` shape this product already settled on. It separates two kinds of absence a reader cannot otherwise tell apart: `omitted_fields` are gone from this view, `relocated_fields` are published once under `rules` instead of once per row, and `full_answer` names the invocation that returns everything. The two fields that made it a *false* answer rather than a short one are kept: `severity_effective`, the output of the model the command exists to run, and `blind_axes`, the declared unknowns the same payload's `note` promises. Measured on `risk ./dubbo`: six fields named as omitted (`endpoints`, `endpoints_reached`, `endpoints_reached_basis`, `reach_scope`, `severity_declared`, `witness_count`), two named as relocated, and the row now carries the score the model produced. Regression `tests/test_bounded_view_declares_its_delta_b06.py`, 7 tests. *Original note:* **open.** `ask risk ./mall` writes 167 546 B to stdout and 624 609 B to `-o`, same flags, same 50 rows: the default view drops **10 of 18 fields per row**, including `severity_effective` — the output of the model the command exists to run — and `blind_axes`, the declared unknowns. The same payload's `note` says *"An axis that could not be measured … names the axis it was blind on"*, and in that view the row does not. The whole declaration is one token: `default_output: "bounded"` — no field list, no basis, no pointer to `--output`. The reduction itself is good work (prose factored into a `rules` dictionary by `rule_id`, and `risks_cap` is one of the better cap blocks in the product); the defect is the undeclared omission. **The pattern already ships two commands away**: `sibling_view.omitted_blocks`, computed from both view constructors. | `bounded_view: {omitted_fields, basis, full_answer: "--output FILE"}` computed from the two constructors, never hand-listed; and either condition the `note` on the view or keep `blind_axes` in it. Sweep: every command with two views publishes the key delta in the smaller one. |
|
|
237
|
+
| `AUD-596-B07` / `B-07`, class residual of `AUD-592-R02` | **P2** | **closed 5.8.21 (`e060485`).** The link filter measured density against the **raw** paragraph, hrefs and all, so five links carrying 300 characters of URL read as prose — and density is the wrong measure in both directions, since `dubbo` lists seven language ports inside one real sentence at 25 visible characters per link. What separates a bar from a description is what is left when the links are removed, so that is what `_looks_like_navigation` measures, with two shapes decided outright: three anchor links are a table of contents whatever their length, three linked images are a badge strip. The preference rule: when what sits under a README's own title is a bar rather than prose, the title is the description and the walk stops there — only a level-1 heading, held to the same floor and the same filters, so *"Installation options"* is not promoted and *"Table of Contents"* is 17 characters. A bar written on one line never became a paragraph at all, so those titles are remembered as they are dropped. `_clean_inline_markup` unescapes HTML entities, which is where ` ` came from. **Measured over the 37-repository corpus: 5 changed, all from junk to the project's own description** — `thingsboard` (link bar → the H1), `alfresco-community-repo` and `keycloak-config-cli` (tables of contents), `examples` (a nav list), `open-banking-gateway` (a badge row); 32 unchanged. The regression is parametric over README **shapes** — centred div, pipe bar, setext title over a bar, table of contents, badge strip — because this class returned on the 17th repository after being closed over 8. Regression `tests/test_readme_navigation_is_not_a_description_b07.py`, 10 tests. *Original note:* **open.** `project_summary` on `thingsboard` — the first field an agent reads — is the README's navigation bar: *"Full-stack project: 💡 Get started • 🌐 Website • 📚 Documentation …"*, HTML entities included. `summary_basis` names the correct section and discards it: the H1 *"Open-source IoT platform for data collection, processing, visualization, and device management."* is the description, and the five declared filters (licence, marketing, operations, startup, tool-output) pass the link bar underneath it. `AUD-592-R02` closed this over 8 repositories and was called *the model of how to close a `*_basis`*; it returns on the 17th. | A navigation-bar filter — link density, ≥3 list separators between links, or non-link text below a floor — plus a preference rule: when the paragraph under the named section fails, fall back to the heading `summary_basis` already identified. Regression parametric over README **shapes** (centred `div`, banner, setext, badge table, link bar), not over `thingsboard`. |
|
|
238
|
+
| `AUD-596-B09` / `B-09` | **P2 — product decision** | **closed 5.8.21 on the half that was a false claim (`5b8146c`); the rule is declined with its reason.** The mechanism was not the shape test — the α argument-shape atom is exact and reads no source — it was the **vocabulary**: `_QUERY_CONSTRUCTION_CALLS` held the JPA and raw-JDBC statement *factories* (`createQuery`, `prepareStatement`) and none of the calls that take the finished SQL, so a repository that never calls `createQuery` had no sink to match. It now holds `executeQuery`/`executeUpdate`/`executeLargeUpdate` and the `JdbcTemplate` query methods; measured on a fixture carrying the row's first two witnesses, `{PetService#byName: [queryForList], PetService#byCity: [executeQuery]}` where the old set matched nothing. `update` and `execute` stay out **on the record**: they are the commonest method names in Java, this layer carries an argument's shape and not its text, and the receiver's type is not resolved here — so `repository.update(prefix + id)` and `jdbc.update("DELETE FROM " + table)` are the same observation, and claiming the first is a query sink is the confident falsehood this product is built against. The row's third witness therefore stays invisible, declared rather than overlooked. The word *known* now points at a list: `risk` publishes `query_sink_vocabulary` — every call that counts, the two left out and why — and `risk --help` and `NC-001` name that block, README's table regenerated from the authority. **Not taken:** a rule that *creates* a finding from a concatenated sink. That is a new claim rather than a corrected one, and a new security rule earns its place with a false-positive measurement over the corpus, not with a patch beside a row. Regression `tests/test_query_sink_vocabulary_b09.py`, 13 tests. *Original note:* **open.** Three textbook sinks in a Spring service — `jdbc.queryForList("… WHERE last_name = '" + lastName + "'")`, `Statement.executeQuery("… WHERE city = '" + city + "'")` and `jdbc.update("DELETE FROM " + table)` — produce **0 findings**. The rule is honest about itself (`spring-audit --help`: *"SEC-008 SQL built by MyBatis string interpolation"*, and all five SEC-008 on `mall` are `${orderByClause}` in `*Mapper.xml`); the promise that is not honest is the composed axis. `risk --help` offers *"a query may be merely concatenated, or may carry an annotated HTTP input through positional calls to a known query sink"*, and on a JPA/JdbcTemplate repository with no MyBatis XML the `query_construction` axis can take no value at all, because there is no finding to compose. `NC-001` excludes *general request-input-to-sink taint analysis*, which is a different exclusion: these three need no dataflow, only the concatenation shape the rule already recognises in XML. | Cheap half: align the words — if the axis cannot see Java concatenation, `risk --help` and `NC-001` say so explicitly. Full half: a rule over a concatenated argument to a known sink (`JdbcTemplate.*`, `EntityManager.createNativeQuery/createQuery`, `Statement.execute*`, a concatenated `@Query`), with the sink and the expression as evidence — the same syntactic order as the `${}` it already matches. |
|
|
239
|
+
| `AUD-596-B03` / `B-03` | **P2** | **closed 5.8.21 (`a8cb814`).** `_parse_analysis_budget_env()` is the one authority over the variable and classifies it into four answers — unset, valid, not a number, not a duration — and `_enforce_analysis_budget`, already the single gate every budgeted command passes before any analysis is paid for, refuses the last two with the `INVALID_INPUT` envelope and the exit 2 an invalid `--format` returns, naming the variable, the value and what a value looks like. The class floor stays advice (C3-55): a budget too small to finish is a choice, one that is not a number is not. An empty or whitespace value stays *unset*. **One correction to the row**: `1e9` is not in this class — `float("1e9")` succeeds and a billion seconds is a budget, large enough never to be reached, which is the variable working. Measured before the fix, the mechanism produces two classes, not three. The sweep the row asked for ships with it, its population read from `BUDGETED_ANALYSIS_COMMANDS`: eight commands x {not a number, 0, negative, valid}, plus the refusal end to end. Regression `tests/test_budget_env_refuses_what_is_not_a_budget_b03.py`, 53 tests. *Original note:* **open.** `ASK_MAX_ANALYSIS_SECONDS` accepts three kinds of invalid value with three behaviours and refuses none. `0` and `-5` produce an advisory (*"the configured budget is 0s … Running it anyway"*) and a complete run; `abc`, `60s` and `1e9` produce **no stderr, no advisory and no error** — the budget simply ceases to exist. Verified in source: `_analysis_budget` (`cli.py:1723-1733`) does `float(raw)` and on `ValueError` sets `configured = None`, which is indistinguishable from *unset*. `ASK_MAX_ANALYSIS_SECONDS=60s` in a CI YAML — the likeliest typo, since every other duration in that file is written that way — silently disables the one control the help presents as the CI budget, and a negative budget is echoed back as if it were a setting. | Not an integer, or ≤ 0 → exit 2 with the same `INVALID_INPUT` envelope any flag gets, naming the variable and the value received; or `0` documented as *unlimited* and the advisory saying that. Parametric sweep: the eight budgeted commands × {non-integer, 0, negative, valid}. |
|
|
240
|
+
| `AUD-596-B10` / `B-10` | **P2** | **closed 5.8.21 (`5cb994d`).** `perf.mark_command_start()` is called by `main_entry` — the earliest point in the process that knows a command is starting — and every `PhaseTimings` built afterwards measures from there. The mark is per command rather than per import, so a host serving several commands from one process re-marks, and a block built with no mark keeps the old self-relative measurement, which is what an embedded caller and a unit test want; `wall_basis` says which of the two happened. Measured on the row's own failure mode, `ASK_MAX_ANALYSIS_SECONDS=5 ask risk ./dubbo`: `wall_ms: 10 571.14` against `_partial.elapsed_seconds: 10.53`. The invariant the row asked for holds and is asserted on a cut run and a complete one. Regression `tests/test_timings_cover_the_whole_command_b10.py`, 7 tests. *Original note:* **open.** `timings.wall_ms` is wrong by ~100× on exactly the runs that cost the most. `ASK_MAX_ANALYSIS_SECONDS=5 ask risk ./spring-security`: process 7 942 ms, `_partial.elapsed_seconds: 6.52`, and `timings: {wall_ms: 54.92, measured_ms: 0, unaccounted_ms: 54.92, unaccounted_pct: 100.0}` under a declared unit of *"milliseconds of THIS process's wall clock, by phase"*. On a complete run the model is exact (`risk ./mall`: external 22 841 ms, `wall_ms: 20 992.58`, phases summing 14 725.99, unaccounted 29.9 %). The clock starts after the budget cut and `phases: {}` — the inverse of the diagnostic that made `AUD-595-B01` findable. | On a cut run, `wall_ms` covers the whole process and completed phases keep their measurements, or `timings: null` with a basis saying the model does not describe a cut. Assertion: `timings.wall_ms >= _partial.elapsed_seconds × 1000` wherever both exist. |
|
|
241
|
+
| `AUD-596-B16` / `B-16` | **P2 — cheap, and it is this product's own doctrine** | **closed 5.8.21 (`1507f69`).** The derivation set is empty and the minimum of an empty set is undefined, not maximal: `finalize()` answers `unknown` and the basis names which empty case happened — no finding of any severity, or N findings none of which are high/critical. The two read differently to anyone deciding whether to trust the report, and the old sentence described neither. The cut-run cap three blocks below still wins, because *"this run was cut short"* is the more specific reason. The test that pinned `high` pinned the defect; it now pins the contract. Regression `tests/test_confidence_over_empty_set_b16.py`, 6 tests. *Original note:* **open.** `spring-audit` on `struts` and `spring-petclinic` (both `spring_detected: true`, `total_findings: 0`, every severity 0) publishes `confidence_level: "high"` with `confidence_basis: "Derived from the confidence of the high/critical findings in this report."` — derived from an empty set. Source: `spring_findings.py:226-229`, `if not high_findings: conf_level = "high"`. **Three lines below it the same function gets the identical question right** for a cut run (`C2-31`: a number describing how much to trust an answer must not go up when the answer shrinks) and caps at `low` with a basis that says which case happened. | `unknown`, or `high` with a basis that says *no high/critical findings to derive from* — the `selftest` rule (*never pass, because a green run over nothing is how a defect gets closed twice*) applied to the field where it is computed. |
|
|
242
|
+
| `AUD-596-A03` / `ASK-C1-001` | **P2 — class C1** | **closed 5.8.21 (`bffa3a6`).** Two authorities, one fact — and the fuller of the two rules is now the authority. `path_filters.changes_an_answer` holds the whole admission question (tool-state directory, pruned directory, inert suffix); `verify-edit`'s `_can_move_a_verdict` is the caller that kept its name rather than the copy that kept the knowledge, and `dirty_exclusion_reason` asks it too, so the working-set census every surface publishes gains two reasons it could not give: `inert-artefact` and `pruned-directory`. `freshness` publishes what it could not say: `stale_reason` in words for each of the four states, `disregarded_changes`, and `working_set_basis` — the sentence that lets a reader check the split against their own `git status`. Measured on a fixture: a `REPORT.md` and a `.claude/settings.json` leave the snapshot **FRESH** with two paths named as disregarded; one changed `.java` beside them is **STALE** with *"1 uncommitted path(s) the analysis reads differ from HEAD"*; the count both commands publish is the same number. Regression `tests/test_one_admission_rule_for_a_changed_path_a03.py`, 25 tests. *Original note:* **open.** Same tree, same session, two authorities, opposite verdicts. `cache freshness .` → `STALE`, with `Current HEAD == RIS HEAD == 3dde0376`, `Delta: 0 commit(s) behind`, `Uncommitted: True`. `verify-edit .` → `pass`, *"no file that any axis reads differs from HEAD. 2 path(s) do differ and none is source, wiring or a descriptor this command analyses"*, `disregarded_changes: [".claude/settings.json", "ASK-AUDIT-5.8.18.md"]`. The invalidation `cache model` documents is *"any change to the analysed files"*, and these are not analysed files. One of the two already knows how to filter. **Cost of believing the wrong one:** a rebuild of 170.8 s (root) or 31.6 s (`posture`) because an uncommitted `.md` is in the tree; on a runner with `.claude/`, `.idea/` or a report checked out, `freshness` says `STALE` permanently. | Reuse the `verify-edit` admission filter and publish a `stale_reason`, separating `STALE` (analysed files changed) from `FRESH` (N non-analysed paths differ). |
|
|
243
|
+
| `AUD-596-A04` / `ASK-UX-003` + `ASK-UX-004` | **P2** | **closed 5.8.21 (`f0aac3c`), both halves.** (1) `--no-write` and `--dry-run` ask this command for the same thing — derive the artefact, write nothing — so the pair is honoured rather than refused, decided before the analysis starts, and the substitution is announced where the answer is (`write_mode`: requested / served_as / basis) rather than only on stderr; the payload names `would_write`. (2) `--dry-run` is scoped to the command instead of to `--init`: it reaches `--capture-baseline`, which computes the violations, writes nothing and publishes what it would have recorded and where. `COMMANDS_THAT_WRITE` had two rows for `verify` and only the first mentioned the guard; both name `--dry-run` now, and `tests/test_no_write_mode.py` resolves every flag either row cites against the command's own parameters. None of the four no-write invocations can return `rc=1` any more — the code that means *violations blocked* — and that is asserted per invocation. Regression `tests/test_verify_write_guards_a04.py`, 11 tests. *Original note:* **open.** Two write-guard defects on `verify`, one of cost and one of scope. (1) `verify . --init --no-write` spends **110.5 s to return 0 bytes and `rc=1`**, where `--init --dry-run` does the equivalent work in 52.9 s and returns 15 481 B. The refusal's own hint promises *"the same answer is produced with the artefact"* and this invocation produced no answer; the incompatibility is decidable in `argv` at t=0; and `rc=1` collides with the contract `verify --help` publishes (*0 = pass, 1 = violations blocked, 2 = unverified*), so a pipeline reads *"there are violations"* when what happened was a refusal to write. (2) `verify . --dry-run --capture-baseline` **attempts the write** — only `ASK_READONLY=1` stopped it, and the refusal names `.ask/contracts-baseline.json`. `--dry-run` is documented as scoped to `--init`, so the scope is declared, but the combination is not rejected and the name carries a universal expectation. Aggravating: `.ask/` installs its own `.gitignore` with `*`, so a plain `git status` cannot show the result. | Validate at `argv`: treat `--no-write + --init` as `--dry-run` (print what would be written, `rc=0`), or refuse in argument validation at ~0 s cost with an exit code outside `verify`'s 0/1/2 contract — 75 already means *the gate could not run*. Make `--dry-run` global to the command, or reject the `--capture-baseline` combination naming its real scope. |
|
|
244
|
+
| `AUD-596-D01` / `B-13` + `B-14` | **P3 — highest leverage of the P3s** | **open.** Repository identity is published by 4 of 28 payloads and spelled six ways. `repo_id`: `spring-audit` (+`scope`, +`git_head`), `endpoints` (+`scope`), `cold-start` (+`git_head`), `migrate-check` (+`git_head`). Other spellings: `repository` (`audit-report`), `head_sha` (`verify`, `verify-edit`), `path` (`modernize`), `target`+`scope` (`plan`), `generated_from` (`archetype`). **None at all**: `posture`, `risk`, `validation`, `onboard`, `prepare-context`, `fix-bug`, `review-pr`, `timeline`, `impact-chain`, `selftest` — 12 of 23 JSON payloads, starting with the first command the help recommends. Second axis: `schema_version` absent from 7 of 23, and `plan` spells it `schema`. `B-14` is the same row seen from one command: `data-exposure`'s refusal is **byte-identical across `mall`, `struts` and `spring-petclinic`** (md5 `2e4074cdf763c3c91a2f400f024ca37c`, 2 812 B) — an excellent refusal that never says what it is about. | One identity block injected by the envelope, not by each call site — which is exactly what `AUD-595-A07` fixed for `endpoints` alone, and why it reappears here. Catalogue assertion over the payload-producing commands. |
|
|
245
|
+
| `AUD-596-D02` / `B-08`, residual of `AUD-594-N04` | **P3** | **closed 5.8.21 (`e7f73c3`).** The comparison is against every block this view carries at any depth, so a block that moved is reported in `relocated_blocks` with the path it is at here, and `omitted_blocks` keeps only what is genuinely absent; the other direction is asked the same way. `_key_paths` is bounded to two levels on purpose — a block is a section of the answer, not every key inside one, and walking to the leaves would make `sibling_view` compare field names and call a value a block. Measured on the row's own subject, `ask ./mall --agent`: **15 omitted → 10**, with the five over-declared blocks named beside the parent they moved under. Regression `tests/test_a_relocated_block_is_not_an_omitted_one_d02.py`, 7 tests. *Original note:* **open.** `ask ./mall --agent` names 15 blocks in `sibling_view.omitted_blocks` and **5 of them are in the same payload**, nested: `/project/language_version`, `/project/deployment`, `/signals/code_notes`, `/signals/mybatis`, `/signals/spring_profiles` — a 33 % over-declaration. The basis says the list is *"computed from both view constructors over the same analysis"*, and it is: over **top-level keys**, while the agent view regroups several blocks under `project` and `signals`. An agent that trusts it re-invokes `--compact` for data it already holds. | Compare over flattened key paths, or rename to `omitted_top_level_keys` and add a regrouping note naming each moved block's new parent. |
|
|
246
|
+
| `AUD-596-D03` / `ASK-AGT-001` | **P3** | **open.** `validation.summary.endpoints_with_body: 0` beside `body_endpoints_in_code: 1255` on MSAS. Not a false statement — the disambiguation travels in the payload (`endpoints_with_body_basis` calls it a legacy alias for *routes with a declared constraint surface*, `axis_note` explains the gap, and Round A verified the finding by hand: 840 `@RequestBody`, 0 `@Valid`, 0 `javax/jakarta.validation`, 0 `ConstraintValidator`) — and Round B reads `C1-49` as closed for that reason. The residue is the shape: it is the **first key of the summary**, its name states the opposite of what it counts, and it carries `_basis` but no `_unit`. The criterion `AUD-594-X03` closed under is *every catalogued figure emits `*_unit` and `*_population`*, and its sweep walks `FAN_IN_FIGURES` only, so this family is outside the population that would have caught it. | Give the legacy key its `_unit` and `_population` from the same catalogue, and extend the `AUD-594-X03` sweep from `FAN_IN_FIGURES` to every published-figure catalogue. |
|
|
247
|
+
| `AUD-596-D04` / `B-18` | **P3** | **closed 2026-08-22 (`040f027`).** The original Boot 2→3 framing defect is fixed: a detected Boot major ≥ 3 makes the boot3 dimension inapplicable, clears its scores, recomputes `headline_blocker` and publishes the coverage explanation. The Boot 3→4 path remains a separate roadmap item. |
|
|
248
|
+
| `AUD-596-D05` / `B-19` | **P3 — Windows** | **closed 5.8.21 (`fd2bf68`).** Three tree walks in `migrate_check` built the directory part with `str(Path)` — the operating system's separator — and then joined the file name with a literal `/`. `_posix_rel` is the convention every other module in this engine already writes at that seam, named, in the one module that had three copies of the bug instead of the fix. The catalogue assertion the row asked for ships with it: over a real `migrate-check` payload, **no path-valued field anywhere in the document holds a backslash**, and the three call sites are held to the helper rather than to review. Regression `tests/test_no_path_field_carries_a_backslash_d05.py`, 4 tests. *Original note:* **open.** Separators mixed **inside one string**: `migrate-check ./jhipster-sample-app` emits `"source_file": "src\\main\\webapp\\WEB-INF/web.xml"` while every other finding in the same payload uses `/`. It breaks string comparison against git paths, against `verify-edit.changed_files` and against `pr-impact --files`, which is how these payloads are joined in a pipeline. | Path fields normalised to POSIX and relative to the analysed root. Assertion over the payload catalogue: no path field contains a backslash. |
|
|
249
|
+
| `AUD-596-D06` / `B-20` | **P3 — third appearance** | **closed 2026-08-22 (`784cd83`).** `endpoints` now publishes `source_file` and `declaration_line` for each route, including the registration site for functional routes, with provenance where the path is not a literal. |
|
|
250
|
+
| `AUD-596-D07` / `B-17`, half refuted | **P3** | **closed 5.8.21 (`c3ff822`) on its surviving half.** A value that names no profile is refused where it is parsed, with the `INVALID_INPUT` envelope and the exit 2 any flag gets, and the refusal names the flag it came from — including *which side* of `--diff a:b` was empty. **A second flattening the row did not have**: `posture` reached the parser through `if profile`, which is false for the very empty string this row is about, so the value never got as far as `_profile_set`. The empty set can no longer reach a consumer from the CLI, so the docstring stops claiming a distinction downstream drops; the old sentence stays quoted as history beside the measurement of why it failed. Regression `tests/test_an_empty_profile_is_not_no_profile_d07.py`, 15 tests. *Original note:* **open on its surviving half.** `--profile ""` is silently identical to passing no flag: byte-identical payload, `profiles_requested: []`. `_profile_set` (`cli.py:1397-1408`) documents the distinction it is there to keep — *"None and the empty set are different answers: no profile set was named, versus a named set that is empty"* — and every consumer then writes `sorted(profiles or set())`, which flattens the second back into the first. **Refuted half, measured against source:** the report's *"an undeclared profile is computed without a note"* is not the case — `posture.py:1394-1396` publishes `profiles_not_declared: ["NOPE"]` with the reason that a profile no artefact mentions is almost always a typo. Do not re-derive it. | Empty string ⇒ `INVALID_INPUT`. Keep the parser's distinction downstream or delete it from the docstring; one of the two, not both. |
|
|
251
|
+
| `AUD-596-D08` / `B-22`, class residual of `AUD-591-A06` | **P3** | **open.** `compare ./spring-petclinic ./spring-framework-petclinic --path .` costs **80 541 ms** to answer `resolution: "not_a_candidate", resolved: false` — *"names a directory, not a change candidate"*, a verdict decidable from the string. The cost tracks `--path`, not the rejection: 5 755 ms with `--path ./mall`, 700 ms with `--path ./spring-petclinic`. Second half: 0 of 2 candidates resolved still exits 0, with the warning only on stderr. | Validate candidate shape before building the model — the same fix `AUD-591-A06` applied at the other call site. `resolved_count: 0` ⇒ non-zero exit or a top-level field, not a stderr line. |
|
|
252
|
+
| `AUD-596-D09` / `B-23`, surviving half of `B26` | **P3** | **closed 2026-08-22 (`fd3d63f`).** `onboard` now labels the population as `non_test_java_sources`, matching the Java-specific count and the taxonomy already used by the advisory and `--progress`. |
|
|
253
|
+
| `AUD-596-D10` / `B-29` | **P3 — scale** | **closed 2026-08-22 (`10f2827`).** Every JSON response publishes `_meta.memory` with `unit`, `peak_rss_mb` and the configured ceiling. Costly analyses enforce `ASK_MAX_RSS_MB` after the measured run and refuse with `MEMORY_TOO_LARGE`, peak and limit in the same structured error shape as the output ceiling. `0`/unset leaves the limit disabled. | Peak RSS **1.4 GB** in one invocation (`endpoints ./thingsboard`, 3 714 Java files), sampled every 10 s: 693 → 861 → 1 021 → 1 400 MB. |
|
|
254
|
+
| `AUD-596-D11` / `ASK-UX-005` | **P3 — exit-code contract** | **open.** `INVALID_INPUT` exits **1 or 2 depending on which layer caught it**, and the payload cannot tell them apart: `endpoints ./no-such-dir-xyz` → 1, `impact NoSuchClassZZZ .` → 1, `impact-chain … --depth 0` → 2, `endpoints . --jobs 0` → 2, `endpoints . --format bogusfmt` → 2, all five with `"code": "INVALID_INPUT"`. The real rule is *grammar (Click/Typer) = 2, semantics = 1*, and `verify --help` has already spent 2 on *"unverified (no contracts declared, contracts unreadable, or the repository could not be analysed — never a silent pass)"* — so a mistyped flag in a CI `verify` is indistinguishable from *"we have not adopted contracts yet"*. | A distinct code for the grammar class (e.g. `INVALID_USAGE`) so the payload and the exit code are derivable from each other, and the table published in `ask schema`. |
|
|
255
|
+
| `AUD-596-D12` — six hints that send the reader back to the error | **P3 — one patch** | **open.** All six are in-product text that names the wrong next step, and each is one call site. (1) `delta` / `contract-diff` reject *"refs and checkouts name the same two states two different ways"* and then print a hint that concatenates the two alternatives without a separator — `ask delta <base> <head> --base-ref <ref> --head-ref <ref>` — which is exactly the rejected combination; the same command's other branch separates them with ` \| `, so the convention exists (`ASK-DOC-001`). (2) `export . --c4` refuses with *"No smaller inline variant is available with the current flags"* while `export .` with no mode lists three smaller sections (`--by-directory`, `--module-graph`, `--integrations`); `hint_basis.declared_flags` is empty, so the generator is reading flags rather than the command's own modes (`ASK-DOC-002`). (3) `rename-class --from m3informatica.saint.services.CommonService` is rejected as *"must be a Java class name (PascalCase)"* — the FQN the rest of the product recommends and that `impact`'s own candidate list prints — with the two generic strings of the whole run (`hint: "Inspect the command input and retry."`, `expected: "A valid command result."`), where ~20 other typed errors state the accepted shape (`ASK-UX-006`). (4) `regress . HEAD` reports `[Errno 13] Permission denied: '.'` — the only untranslated OS exception of the run — when the real fact is that `.` is a directory and not a JSON payload (`ASK-UX-007`). (5) `explain-endpoint --method GET` answers *"Check the input value, path, or flag and try again"* while the signature `{route} [path]` is known (`B-25`). (6) `explain-endpoint "FOO /owners"` reports an invalid HTTP verb as an absent route, the same sentence a legitimately missing route gets (`B-24`). | One usage constructor per command, fed by the command's own signature and modes; `regress` checks `is_dir()` before `open()`; an out-of-set verb gets its own error naming the admitted verbs. `C4-27` (fuzzy suggestion pointing at an unrelated flag) reproduces in the same family and is not re-filed. |
|
|
256
|
+
| `AUD-596-D13` / `B-28` | **P3** | **closed 5.8.21 (`1014c72`).** Both cases answer now, from the one authority, on all three publishers (`spring-audit`, `posture`, `risk`). The modelled case is deliberately short — there are no axes to enumerate, because none of them is reporting an absence of model — and states that the Spring-shaped axes measure here, so an absence they report is an absence of control. Omitting it was a deliberate saving (CL-19 wanted the modelled payload byte-identical) and the consumer paid for it: a missing key cannot be told apart from a build that does not publish the field, so *"your stack is modelled"* and *"this version is too old to say"* read the same. **One correction to the row**: the field is not `null` in the payload, it is *absent* — `null` is what a reader sees through `payload.get("stack_fit")`, which is the same defect from the consumer's side and is why the row is right. *Original note:* **open.** `spring-audit.stack_fit` is `null` on every repository where Spring is detected (`spring-petclinic`, `mall`, `struts`), and populated only where it is not (`tutorials/algorithms-modules`), where its text is exemplary (*"read every one of their answers as unknown, not as none"*). `ask schema schemas-v1` lists `spring-audit` as the producer of `stack-fit-v1`, so a consumer is told the sub-schema exists and gets `null` from the case it was written for. | If the block is informational only where Spring is absent, say so: `{spring_detected: true, statement: "…"}` instead of `null`. |
|
|
257
|
+
| `AUD-596-D14` / `B-15`, narrow half of `B20` | **P3 — declared, with a window** | **open, and half of it is a decision rather than a defect.** `ask schema schemas-v1`: 47 identifiers, `non_conforming: 13`, `ambiguous_identifiers: 1`. The ambiguous one is `1.0`, spelled by **seven** schemas (`core-analysis-v1`, `impact-chain-v1`, `pr-impact-v1`, `migration-blast-v1`, `spring-impact-v1`, `event-topology-v1`, `test-gap-ranking-v1`), so pinning `schema_version == "1.0"` admits seven incompatible payloads. The window half is already declared: `pending_renames_window = {announced_in: "5.8.5", takes_effect_in: "6.0.0", change_id: "BC-002"}` with *"pin the canonical name to be unaffected by the rename"*. | Disambiguate `1.0` before `6.0.0`, or declare in the registry that the identifier is not pinnable and say what to pin instead. |
|
|
258
|
+
|
|
259
|
+
**Mapped, not new — witnesses on rows this ledger already holds. Do not file twice.**
|
|
260
|
+
|
|
261
|
+
- **`AUD-592-D04` (`build_commit`) is reopened in place**, in the eighth-pass table below, on
|
|
262
|
+
its sixth consecutive witness. The
|
|
263
|
+
new evidence is what the previous closure could not see: `ask version` still answers
|
|
264
|
+
`provenance.build_commit: "unrecorded"`, and `<site-packages>/sourcecode/_build_commit.py`
|
|
265
|
+
**does not exist in the installed wheel** — the reader (`product_info.py:26-48`) and the
|
|
266
|
+
writer (`build_stamp.py`, `hatch_build.py`) both ship, and the intermediate file, the only
|
|
267
|
+
one that can work in an installed wheel, does not. The chain therefore falls to
|
|
268
|
+
`git -C <site-packages> rev-parse HEAD`, which a `pipx` install has no checkout for. This
|
|
269
|
+
is why the row is not closable from the suite: it is a build-time behaviour, and `S-06`
|
|
270
|
+
is its assertion. Consequence recorded by both rounds: **not one of ≈355 measurements in
|
|
271
|
+
this pass is attributable to a commit.**
|
|
272
|
+
- **`BUG-5` / `D-09` (stale cost anchors), seventh report, now aggravated.** `cache model`
|
|
273
|
+
publishes 19 anchors: 17 measured on `5.1.0`, one on `4.18.0`, one on `5.0.0`. New this
|
|
274
|
+
pass: they are no longer confined to `cache model` — the budget advisory prints them as
|
|
275
|
+
operational guidance (*"166.7 s at 3 342 files (field, cold cache, on 5.1.0)"*), which is
|
|
276
|
+
where an operator sizes a CI budget. Measured against them in this pass: `risk` anchored
|
|
277
|
+
at 77.2 s exhausted a 300 s budget (301.2 s), `audit-report` 312.3 s, `verify-edit`
|
|
278
|
+
anchored at 119.5 s took 2.9 s. Blocked on `AUD-592-D04`: an anchor cannot carry
|
|
279
|
+
provenance while the build cannot name itself.
|
|
280
|
+
- **`ASK-17` (parse store cannot hold a workspace warm)** gains its bank witness: 28 396
|
|
281
|
+
entries / **536.26 MB with 5 of 16 repositories warm**, over a 512 MB budget, *"the next
|
|
282
|
+
sweep evicts"*. With 16 repositories and >52 000 Java files, LRU eviction makes any
|
|
283
|
+
*"warm"* measurement a lottery — which is the same measurement hazard `AUD-595-B01`
|
|
284
|
+
closed one layer up.
|
|
285
|
+
- **`C3-128` (`timeline` above 120 s with no ceiling)**: 158 618 ms on `spring-security`
|
|
286
|
+
`--since HEAD~3` under `ASK_MAX_ANALYSIS_SECONDS=5`, and 433 500 ms on MSAS `--since
|
|
287
|
+
HEAD~5`. The behaviour is correct against the declaration (the command is not in the
|
|
288
|
+
budgeted tuple); the row is that it is the only gate-shaped command with no ceiling of
|
|
289
|
+
any kind.
|
|
290
|
+
- **`BUG-6`** is superseded in evidence by `AUD-596-B01`, which reproduces it with a harder
|
|
291
|
+
witness (a route opening, not a finding removal). It keeps its number and its position.
|
|
292
|
+
- **`C4-27`** reproduces inside `AUD-596-D12` (5) and (3); **`C1-49`** is read as closed by
|
|
293
|
+
Round B and leaves the residue filed as `AUD-596-D03`; **`B26`** leaves the residue filed
|
|
294
|
+
as `AUD-596-D09`.
|
|
295
|
+
|
|
296
|
+
**Verified and not defects — do not spend effort.** `--compact` and `--agent` inside their
|
|
297
|
+
published ceilings on 6 repositories (max 25 371 B against 30 000 / 40 000) and on MSAS
|
|
298
|
+
(23 460 B and 37 028 B). `_partial` on `spring-audit` behaving exactly as `AUD-595-A03`
|
|
299
|
+
required, verified in-process: `--ci` → 75, `--ci --allow-partial` → 0. `scope` over a
|
|
300
|
+
16-repository CWD publishing `nested_repositories: 16`, `is_repository_root: false`.
|
|
301
|
+
`scan_truncated.cap_note` reconciling the two ceilings at the point of use. The write guard
|
|
302
|
+
refusing on 3 of 3 paths and naming artefact, role and remedy, with `.ask/.gitignore`
|
|
303
|
+
auto-created. `enrich.unresolved` naming a reason per result it could not place. The refusal
|
|
304
|
+
of the false zero, sampled in six commands in Round A (`risk_score: null` with *"nothing was
|
|
305
|
+
measured, which is not the same as measuring zero"*; `answered: false` in `plan`;
|
|
306
|
+
`tests_at_risk` marked placeholder in `compare`; `risk_level: "UNKNOWN"` on a malformed diff;
|
|
307
|
+
`counts_are_floor: true` on the cut `risk`; `zero_result_reason` enumerating the seven
|
|
308
|
+
constructs searched on a Java-free path).
|
|
309
|
+
|
|
310
|
+
**Process findings of this pass.**
|
|
311
|
+
|
|
312
|
+
1. **The sweep rule holds where it was applied, and only there — fourth consecutive
|
|
313
|
+
confirmation.** Zero recurrences from classes closed as a parametric sweep (write guards
|
|
314
|
+
3/3 and 8/8, numeric bounds, error-path cost 25/25 under 0.9 s, selection flags). Every
|
|
315
|
+
row in this queue that is a *return* is a fix that stopped at one call site: `scope`
|
|
316
|
+
fixed for `endpoints` and missing from 12 payloads (`AUD-596-D01`), *analyse-then-reject*
|
|
317
|
+
fixed in `impact` and back in `compare` (`AUD-596-D08`), the README `*_basis` closed over
|
|
318
|
+
8 repositories and failing on the 17th (`AUD-596-B07`), `sibling_view` computed over the
|
|
319
|
+
wrong key depth (`AUD-596-D02`).
|
|
320
|
+
2. **Byte-size identity is not a regression test, and this pass is the proof.** Three
|
|
321
|
+
consecutive passes certified *"no drift"* partly from payload sizes; `AUD-596-A01` moved a
|
|
322
|
+
core figure by 4 under a byte-identical 142 852 B, because `1908` and `1904` are the same
|
|
323
|
+
width. The remedy is already in the product for one command (`content_id`) and is filed
|
|
324
|
+
with that row.
|
|
325
|
+
3. **The product is more honest in the full answer than in the reduced one, and the reduced
|
|
326
|
+
one is what an agent reads.** `risk`'s default view drops the model's own output and its
|
|
327
|
+
declared blind axes (`AUD-596-B06`); `sibling_view` over-declares (`AUD-596-D02`); a cut
|
|
328
|
+
run's `timings` describes 55 ms of a 7.9 s process (`AUD-596-B10`).
|
|
329
|
+
4. **A generated surface can be as wrong as a hand-written one when it reads the wrong
|
|
330
|
+
authority.** `AUD-594-X01` was the literal lying while the generator was right. Here the
|
|
331
|
+
generator recommends *"Runs now"* commands it could have known exceed the ceiling
|
|
332
|
+
(`AUD-596-X02`), and a shared basis constant names flags the emitting command rejects
|
|
333
|
+
(`AUD-596-X01`). Generating from a registry stops textual drift, not semantic drift: the
|
|
334
|
+
missing assertion is per-member conformance.
|
|
335
|
+
|
|
112
336
|
### Ninth Audit Pass: `5.8.18` findings, closed in `5.8.19` / MSAS + 12-repository bank / 2026-08-21
|
|
113
337
|
|
|
114
338
|
**Closed queue — all twelve rows are `closed 5.8.19`, one atomic commit each.** Kept in full because the mechanisms are the evidence and the direction column is what the fixes were written against.
|
|
@@ -210,7 +434,9 @@ where this one completes and publishes `scope` and `scan_truncated`; the canonic
|
|
|
210
434
|
### Eighth Audit Pass: `5.8.17` findings, closed in `5.8.18` / MSAS + 12-repository bank / 2026-08-21
|
|
211
435
|
|
|
212
436
|
**Closed queue — every row below is `closed 5.8.18` and re-verified from outside by the
|
|
213
|
-
ninth pass, except `AUD-592-D02` (half) and `AUD-592-D04`, which carry forward
|
|
437
|
+
ninth pass, except `AUD-592-D02` (half) and `AUD-592-D04`, which carry forward — and
|
|
438
|
+
`AUD-592-D04` is `reopened` by the tenth pass, which found the stamp missing from the
|
|
439
|
+
shipped wheel rather than missing from the source.** Kept in
|
|
214
440
|
full because the mechanisms are the evidence, and because the direction column is what the
|
|
215
441
|
fixes were written against. Ordered as the correction order was: **regressions first, then the rows a
|
|
216
442
|
previous pass marked closed that the field does not reproduce as closed, then new contract
|
|
@@ -232,7 +458,7 @@ in this ledger are listed after the table as mappings, not as new defects.
|
|
|
232
458
|
| `AUD-592-D01` / `D-1` | **P3 — residual of `AUD-591-A08`** | **closed `5.8.18`** (`0f8e03f`) — the root inventory keys on the triggering flag: `<repo>/.ask/readiness-history/` — only with `--snapshot` (optionally relocated by `--history-dir`). Mechanism as found, kept for traceability: open, confirmed in source. The write inventory in the root help says `migrate-check` writes *"only with `--history-dir`"* (`cli.py:296`). `--history-dir` only relocates the destination; the flag that writes is `--snapshot`, as `migrate-check --help` states correctly and as the code comment at `cli.py:12980-12986` states explicitly. Verified in the field: `migrate-check --history-dir <path>` exits 0 with zero writes and no guard message, while `--snapshot` fires the guard correctly. `AUD-591-A08`'s assertion checks that each cited flag **exists** in that command's parser — `--history-dir` does — not that it is the flag that triggers the write. | Key the inventory on `(command, triggering flag, path)` taken from the guard site itself, as `AUD-591-A08`'s own acceptance text required. An auditor tests what the inventory lists: this is the same mechanism by which `AUD-591-A01` went unfound. |
|
|
233
459
|
| `AUD-592-D02` / `D-2` | **P3 — residual of `AUD-591-A02`** | **closed `5.8.19`** (`1d3db4d`) **for both halves, from one module.** `--min-band` is a floor applied while the answer is composed, not a post-filter over rows, which is why it was left out of `_apply_selection` and why the exception cost what the rule was written to prevent. `selection_contract.min_band_filter_block()` now owns the disclosure: it merges rather than replaces (so `--band` and `--min-band` can both be in play), it returns nothing at the permissive floor (a payload that excluded nothing must not claim a selection), and it carries a basis saying the floor is not a row filter. `risk` publishes it from `_assemble_payload`, the one construction both its callers use. Field-verified on halo: `--min-band high` → `{min_band: high, total_before_filter: 0, matched: 0, shown: 0}`; default → `null`. Regression sweeps the commands offering the flag and fails any module that hand-builds the block. Mechanism as found: open in half, and the half that closed shows the fix the other needs. `f78522a` gave `enrich --min-band` its `_filter` block (`{min_band, total_before_filter, matched, shown}`, `cli.py:11666-11673`); `risk --min-band` still publishes `_filter: null` while `risk --band` publishes one — measured on `mall`: `--band high` → `_filter` with `total_before_filter: 50`, `--min-band medium` → `null`. The two call the same helper 250 lines apart and only one adds the block afterwards (`cli.py:11406-11413` against `:11658-11673`). Mechanism as found, kept for traceability: `--min-band` filters and validates (`cli.py:11294-11308` in `risk`, `:11563-11568` in `enrich`) but publishes no `_filter` block, while its sibling `--band` publishes one through `_apply_selection`. A consumer cannot tell a filtered answer from a small one. | Route `--min-band` through `_apply_selection` like every other selection flag, or publish the same `_filter` shape from wherever it is applied. |
|
|
234
460
|
| `AUD-592-D03` / `D-3` | **P3 — residual of `AUD-591-A02`** | **closed `5.8.18`** (`f28fe40`) — `enrich --rule NOPE-999` exits 1 against the same catalogue `spring-audit` validates against. Mechanism as found, kept for traceability: open. `enrich --rule NOPE-999` exits 0 with an empty result; `spring-audit --rule NOPE-999` exits 1 against the rule catalogue. One flag name, two contracts. Measured with a synthetic SARIF 2.1.0 (2 results): `--rule`, `--band` and an invalid `--band` all behave correctly in `enrich`; only unknown-rule validation is missing. | Validate against the same catalogue in both commands. `enrich` is experimental-tier, which sets the severity, not the contract. |
|
|
235
|
-
| `AUD-592-D04` / `build_commit` | **P3 — process** | **closed `5.8.19`** (`6eba05a`) **on the writer side, which is the side that was missing.** A `hatchling` build hook now writes `src/sourcecode/_build_commit.py` inside the build, where the checkout still exists, and the reader consults three sources narrowest first: `ASK_BUILD_COMMIT`, the stamped module, then the checkout — with `unrecorded` in the stamp treated as an absence that falls through rather than as an answer. The logic lives in `sourcecode.build_stamp`, importable without the build backend, because **a build-time behaviour the suite cannot exercise is how this row survived five reports**: the previous fix was asserted through a monkeypatched environment variable and nothing else. The hook never fails a build — an exported tarball has no git, and a release that cannot be built is worse than one that cannot name its commit. Verified end to end: stamp written, `ask version` publishes the commit, generated file gitignored so no checked-in copy can drift. Mechanism as found: open, fifth consecutive report, and the fix landed on the wrong side of the build boundary. `6d8eb1d` closed the *reader*: `product_info._build_commit()` takes `ASK_BUILD_COMMIT` and falls back to `git rev-parse HEAD` of the package checkout. **Nothing stamps it.** No `hatchling` hook, no release script and no CI step sets `ASK_BUILD_COMMIT` (`grep -rn ASK_BUILD_COMMIT` finds the reader, and a test that monkeypatches the variable — nothing that writes it), and an installed wheel has no git checkout, so the fallback returns `unrecorded` on every field install. The battery passed because it asserted the reader. Field verdict on 5.8.18 is unchanged: `build_commit: unrecorded`. Mechanism as found, kept for traceability: open, fourth consecutive report. `ask version` publishes `build_commit: unrecorded`, so no measurement in these ~150 invocations is attributable to a commit and *"fixed in 5.8.17"* is not falsifiable from outside. | Stamp the commit at build time. It is the cheapest row in this table and it is the precondition for every other row's verification. |
|
|
461
|
+
| `AUD-592-D04` / `build_commit` | **P3 — process** | **reopened by the tenth pass, sixth consecutive witness — the fix landed and the shipped wheel does not carry it.** `5.8.20` still answers `provenance.build_commit: "unrecorded"`, and `<site-packages>/sourcecode/_build_commit.py` **is absent from the installed wheel**: reader and writer both ship, the intermediate file — the only link that can work in an installed wheel — does not, so the chain falls through to `git -C <site-packages> rev-parse HEAD`, which a `pipx` install has no checkout for. Candidate mechanisms, all cheap to distinguish: the hook is not registered for the `wheel` target, or it writes after `hatchling` has collected the files (no `force_include`), or the release was built from an sdist with no `.git`, where `head_commit()` correctly returns `UNRECORDED`. **The row may not read `closed` again on a suite assertion**: the only admissible evidence is the built artefact (`S-06`). Consequence, recorded twice more this pass: not one of ≈355 field measurements is attributable to a commit. Previously **closed `5.8.19`** (`6eba05a`) **on the writer side, which is the side that was missing.** A `hatchling` build hook now writes `src/sourcecode/_build_commit.py` inside the build, where the checkout still exists, and the reader consults three sources narrowest first: `ASK_BUILD_COMMIT`, the stamped module, then the checkout — with `unrecorded` in the stamp treated as an absence that falls through rather than as an answer. The logic lives in `sourcecode.build_stamp`, importable without the build backend, because **a build-time behaviour the suite cannot exercise is how this row survived five reports**: the previous fix was asserted through a monkeypatched environment variable and nothing else. The hook never fails a build — an exported tarball has no git, and a release that cannot be built is worse than one that cannot name its commit. Verified end to end: stamp written, `ask version` publishes the commit, generated file gitignored so no checked-in copy can drift. Mechanism as found: open, fifth consecutive report, and the fix landed on the wrong side of the build boundary. `6d8eb1d` closed the *reader*: `product_info._build_commit()` takes `ASK_BUILD_COMMIT` and falls back to `git rev-parse HEAD` of the package checkout. **Nothing stamps it.** No `hatchling` hook, no release script and no CI step sets `ASK_BUILD_COMMIT` (`grep -rn ASK_BUILD_COMMIT` finds the reader, and a test that monkeypatches the variable — nothing that writes it), and an installed wheel has no git checkout, so the fallback returns `unrecorded` on every field install. The battery passed because it asserted the reader. Field verdict on 5.8.18 is unchanged: `build_commit: unrecorded`. Mechanism as found, kept for traceability: open, fourth consecutive report. `ask version` publishes `build_commit: unrecorded`, so no measurement in these ~150 invocations is attributable to a commit and *"fixed in 5.8.17"* is not falsifiable from outside. | Stamp the commit at build time. It is the cheapest row in this table and it is the precondition for every other row's verification. |
|
|
236
462
|
|
|
237
463
|
**Mapped, not new — inherited rows this pass re-witnesses.** `B-6` (partial answers exit 0)
|
|
238
464
|
keeps its row and gains its best measurement yet: overrun ×5.4 → ×4.2 → **×1.39** (median
|
|
@@ -651,7 +877,7 @@ retained only as evidence of what the auditor observed before the fix.
|
|
|
651
877
|
| B21 | **Six of the thirteen shapes being renamed name no command that emits them.** `schemas-v1` publishes `emitted_by` per shape; for `migration-blast-v1`, `spring-impact-v1`, `event-topology-v1`, `test-gap-ranking-v1`, `canonical-ir-v1` and `hibernate-strategy-v2` it is `[]`, beside seven shapes where it is populated. Read the way a consumer reads a list, an empty `emitted_by` says *nothing emits this shape* — which tells the one consumer that **is** affected by `BC-002` that it is not. | found here while declaring `BC-002` (5.8.5), not reported by the field | Low — a disclosure gap in a declaration whose whole purpose is to let a consumer decide whether it is affected | **closed 5.8.6 — derived, not typed out.** `schema_producers` reads the two authorities the CLI already holds — the registered command tree and the import graph of the module that defines each callback — and answers with the evidence it derived on: `command_import` (the command's own body imports the producing module), `module_import` (one hop, through an intermediate narrow enough to attribute), `substrate` (reached from more commands than a producer would be) or `unresolved` (nothing found, said in a sentence). Five of the six now name a command — `migration-blast-v1` → `migrate-check`, `spring-impact-v1` and `event-topology-v1` → `impact-chain`, `test-gap-ranking-v1` → `impact`, `hibernate-strategy-v2` → `migrate-check`/`migrate-recipe` — and the sixth, `canonical-ir-v1`, is reached from **28 modules**: naming every command would be less true than naming none, so it publishes `substrate` with the count and the instruction to match on `subject`. A declared producer still wins, because some producers are not one registered command (`ask (root)`, `baseline capture`). Regression `tests/test_schema_producers_derived_b21.py`, 33 assertions, parametrised by the registry so a fourteenth unnamed shape fails the battery. **History:** open, and named rather than shipped as a fact: every `BC-002` row with an empty producer list carries `emitted_by_basis` — *«not declared in the registry: this shape's producing command is not named yet, so match on `subject`»* — so the gap is visible instead of being read as a negative claim. What remains is to fill the six, and the honest way is derivation rather than six more hand-written strings: the producer is discoverable from the command that constructs each payload, which is the same authority every other population in this repository comes from. Until then the assertion in `tests/test_schema_rename_window_b20.py` holds the weaker invariant a reader can rely on: a row either names its emitters or says why it cannot. |
|
|
652
878
|
| C3-129 | **`timeline`'s absolute cost, now fully attributed and still 27 % above its own record.** `C3-128` closed by instrumenting rather than tuning, which was the right order and left this behind: the clock is split, nothing is unaccounted, and the number has not moved. Round 10 on 5.8.5 measures `timeline --since HEAD~5` at **~198 000 ms against the 5.7.2 record of 155 950 ms (+27 %)** — the only gate-shaped command over 120 s that is not there structurally (`delta` 191 s and `contract-diff` 115 s walk two whole trees, `compare` 46 s walks N candidates; `timeline` walks **five** trees and costs more than `delta` does with two). | 5.8.2 → 5.8.5 re-audits (residual of `C3-128`, which closed the attribution half in 5.8.5) | Low-Medium — an investigation command rather than a CI gate, and the last performance figure of the round sitting outside its own best | **closed 5.8.6 — by not paying twice for what does not change.** A metric at a commit is a pure function of that tree, and the tree at a sha never changes: the only cache in this product whose key can be exact rather than heuristic (analyzer fingerprint + metric + commit sha). `timeline_cache.SampleCache` stores measured values under the per-repository core store, and when every watched metric for a sample is a hit the tree is **never materialised**, which is the 13,8 % the split attributes to git. Measured on BroadleafCommerce over the same range: **26 845 ms → 12 ms**, samples and transitions byte-identical. What makes it admissible rather than merely fast, all asserted: a failed sample is never stored (one bad run cannot become permanent), the fingerprint is in the key (corrected analysis is never served from a previous release), the hit is published (`sample_cache`, `from_cache` per sample, and `cost` separates the cached population from the measured one instead of reporting a median over both), and `--no-cache` / `ASK_TIMELINE_NO_SAMPLE_CACHE=1` measure everything again so the command stays measurable without emptying a store (E-37). `cache_model` carries the layer, so `cache model` and `docs/CACHE.md` state it from one authority. Regression `tests/test_timeline_sample_cache_c3_129.py`, 11 assertions. **History:** open — and it opened with the answer already published, which is why it is cheap to attack and expensive to leave: `timeline`'s own `timings` say **82,9 % `measure:posture`, 13,8 % `materialise_tree`, 3,3 % `release_tree`, 0,0 % unattributed**. So this is not a `timeline` defect at all: it is `posture` paid five times over five materialised trees, and it makes the same substrate question `C3-127` asks — with the difference that here the five payers are one command, so the cache is intra-invocation and needs no cross-command contract. ⚠ **Do not reopen `B7`**: cross-tree cache reuse is healthy, the v3 assertion passes at 6,8×. Acceptance: `timeline --since HEAD~5` back inside +10 % of 155 950 ms on the audited corpus with `timings` still summing to 100 %, and `measure:posture` falling as a share rather than the total falling for an unnamed reason. |
|
|
653
879
|
| B23 | **The self-diagnostic's `ASK-09` row reads the refusal at the wrong nesting level, so it can only fail for reasons that are not the row.** Field: `ask selftest .` publishes `{"row":"ASK-09","status":"fail","seconds":4.42,"observed":"the refusal carries no `hint_basis`"}` while the same build, the same repository and the same session produce a refusal that carries `hint_basis` with `derived_from`, `command`, `declared_flags`, `flags_already_passed` and `context_found`. | 5.8.7 audit (round 11, `BUG-01`) | High — a ledger that runs itself is `F-BV`'s whole premise, and one false positive costs the five rows that pass | **open** — mechanism read at HEAD, not inferred: `_check_ask09` (`selftest.py:119-151`) runs `endpoints <repo>`, treats **any** non-zero exit as the refusal, and reads `hint` from the envelope root while the emitter nests it under `error` (measured here with `ASK_MAX_OUTPUT_TOKENS=1` on `tests/fixtures/test_gap_ranking`: `error.hint` present, root `hint` absent, root `hint_basis` present). Two consequences, and the second is worse than the report's: the first branch — *the refusal denies a smaller view while declaring flags*, the half that would catch the defect the row exists for — **can never fire**, because the string it matches is in a key it never reads; and any non-zero exit that is **not** `OUTPUT_TOO_LARGE` (a `--no-write` refusal, an invalid input, a crash) falls to the `not basis` branch and is published as `ASK-09 fail`. The field's 4,42 s against an `endpoints` that costs 6,5 s warm on that repository is consistent with the second path, and the probe cannot tell the reader which it took. Acceptance: the probe asserts the observed `error.code` **is** `OUTPUT_TOO_LARGE` before evaluating anything else, answers `not_applicable` **naming the code it saw** when it is not, reads `hint`/`hint_basis` from the document the emit seam actually writes, and carries one battery case per branch — fits under the ceiling · refuses correctly · refuses badly · failed for another reason. The last case is the regression: a row that cannot distinguish *the assertion is false* from *the run did not happen* is not a ledger. |
|
|
654
|
-
| B24 | **`ASK_MAX_ANALYSIS_SECONDS` is published as the CI budget control and bounds three commands.** Field, all at cap 5 s: `risk` honours it exactly (8,5 s, `PARTIAL, budget exhausted`, and 180 s → 180,7 s), while `timeline --since` runs 58,2 s, `verify-edit` 63,3 s, `contract-diff` 87,9 s at cap 10, and `enrich --sarif` and `cache warm` were killed above 200 s — every one `rc=0` and complete. | 5.8.7 audit (round 11, `BUG-02`) | High — the round's stated conclusion is *«si querías gatear CI con un budget, no puedes»*, and that is the use the surface advertises | **open** — ⚠ **correction to the reported mechanism, read at HEAD: nothing ignores a check, because on those paths there is no check.** `_analysis_budget` (`cli.py:1566`) resolves the variable and `_enforce_analysis_budget` (`cli.py:1622`) **warns and never bounds** — that is C3-55, decided and correct: below the class floor was once *rejected* and above it admitted a run that killed the session, so the floor is advice. Bounding lives in `phased_run`, and only three commands hand it a deadline: `spring-audit` (`cli.py:9699`), `risk` (`cli.py:10630`) and `audit-report` (`cli.py:11008`). The budget is therefore a **phase-runner** contract published as a process-wide one — `cache_model.py:657` prints it as *«bound and narrate a run where CI wants an explicit budget»* with no scope beside it. Acceptance, either half: the CIR build itself honours a deadline, so a budget bounds every repository-wide command and a truncated build declares itself the way `risk` does; **or** the surface names the population it bounds, derived from the registry rather than a hand list, and every command outside it publishes `analysis_budget.applies: false` with the reason. Until one of the two ships, no gate can be timed by this variable, and the help must not imply it can. |
|
|
880
|
+
| B24 | **`ASK_MAX_ANALYSIS_SECONDS` is published as the CI budget control and bounds three commands.** Field, all at cap 5 s: `risk` honours it exactly (8,5 s, `PARTIAL, budget exhausted`, and 180 s → 180,7 s), while `timeline --since` runs 58,2 s, `verify-edit` 63,3 s, `contract-diff` 87,9 s at cap 10, and `enrich --sarif` and `cache warm` were killed above 200 s — every one `rc=0` and complete. | 5.8.7 audit (round 11, `BUG-02`) | High — the round's stated conclusion is *«si querías gatear CI con un budget, no puedes»*, and that is the use the surface advertises | **closed 5.8.21 (`c909d18`) — second acceptance half taken: the scope reaches a payload.** The row's own words were that *the remaining cost is a call site, not a design*, and that is what shipped. `phased_run.budget_scope_reason()` gives each scope the sentence that has to travel with it; `cli._budget_disclosure()` is the payload block; `migrate-check` publishes it in `metadata.analysis_budget` as `{scope: "advisory_only", applies: false, reason, configured_max_seconds, configured_source}`. A caller who set `ASK_MAX_ANALYSIS_SECONDS` and got a complete answer 11 s later now learns from the ANSWER that the variable is advice on this command, and which three commands do stop at the deadline — instead of inferring a bug from a deadline that never fired. `applies` had been computed in `cli.py` and read by nothing, and `budget_scope()` had answered correctly since `b62d840` with no payload calling it. Not `false` alone: `false` beside a variable the help calls the CI budget reads as a defect unless the payload says why. The FIRST acceptance half — the CIR build itself honouring a deadline so a budget bounds every repository-wide command — is **not** taken and remains the open option. *Original note:* **open** — ⚠ **correction to the reported mechanism, read at HEAD: nothing ignores a check, because on those paths there is no check.** `_analysis_budget` (`cli.py:1566`) resolves the variable and `_enforce_analysis_budget` (`cli.py:1622`) **warns and never bounds** — that is C3-55, decided and correct: below the class floor was once *rejected* and above it admitted a run that killed the session, so the floor is advice. Bounding lives in `phased_run`, and only three commands hand it a deadline: `spring-audit` (`cli.py:9699`), `risk` (`cli.py:10630`) and `audit-report` (`cli.py:11008`). The budget is therefore a **phase-runner** contract published as a process-wide one — `cache_model.py:657` prints it as *«bound and narrate a run where CI wants an explicit budget»* with no scope beside it. Acceptance, either half: the CIR build itself honours a deadline, so a budget bounds every repository-wide command and a truncated build declares itself the way `risk` does; **or** the surface names the population it bounds, derived from the registry rather than a hand list, and every command outside it publishes `analysis_budget.applies: false` with the reason. Until one of the two ships, no gate can be timed by this variable, and the help must not imply it can. **Re-witnessed by the tenth pass, and the acceptance criterion is now half-built in source.** Round B measured `migrate-check` at budget 1 running 11 384 ms with `_partial: None` and filed it as *"named in the authority and implementing nothing"*; that framing is this row, already open, and the mechanism it corrects to is the one written above. What is new is that the authority the second acceptance half asks for **exists**: `phased_run.budget_scope()` (`phased_run.py:68-70`) answers `phase_runner` against `advisory_only` from `BUDGET_RUNNER_COMMANDS`, and nothing publishes it — not the help line rendered from `analysis_budget.BUDGETED_ANALYSIS_COMMANDS` (8 commands), not `analysis_budget.applies`, which is computed in `cli.py:1717-1744` and never reaches a payload. The row's remaining cost is a call site, not a design. Gating half split out as `AUD-596-X01`. |
|
|
655
881
|
| B25 | **`data-exposure` cannot tell *no labels declared* from *labels declared and unusable*.** The same malformed declaration (`dataLabels` as a dict, `customSecurityAnnotations` as a list of strings) is diagnosed exactly by `ask config . --config X.json` — *«`dataLabels`: not_a_list — expected a list of label declarations, found dict»*, *«declared and unusable»* — and reported by `ask data-exposure . --config X.json` as *«no labels declared — nothing measured»*. | 5.8.7 audit (round 11, `BUG-03`) | Medium — the user declares labels, is told nothing was declared, and concludes the differential feature of the product does not work | **open** — the validator exists and already produces the right sentences; what is missing is that its **result**, not its count, reaches the payload. `data-exposure` evaluates an emptied list after the parser has dropped the invalid entries, and loses the reason with them. Acceptance: a declaration that parses to zero usable labels answers `zero_result_reason: "declaration_invalid"` and carries the same warnings `config` prints, keyed to the same declaration; a declaration that is genuinely absent keeps today's sentence. One authority for *is this declaration usable*, two readers. |
|
|
656
882
|
| B26 | **The first number a new user reads is labelled `all languages` and is below the single-language count of the same tree.** `ask onboard .` and `ask prepare-context onboard .` say *«3 337 source files (all languages)»* on a repository where four independent methods count **3 342** `.java` (`find`, `git ls-files`, `find ./src`) and where `cache warm`, `selftest` and `spring-audit` all publish 3 342 — with 3 317 `.ts` in a sibling module besides. | 5.8.7 audit (round 11, `BUG-04`) | Medium — low functionally, high in trust: it is the first figure a newcomer sees and it is contradictable in five seconds | **open** — ⚠ **the reported cause is already decided and is not the defect.** C1-19 ruled that these are three legitimate populations, and `migrate-check.metadata.files_scanned_basis` names all three by their figures (3 337 / 3 336 / 3 342) precisely so they stop reading as drift. What survives is narrower and is real on both halves: the **label** is false — a count that excludes anything is not *all languages*, and one arithmetically below a single language's count in the same tree cannot be it (`summarizer.py:154-156`) — and the **basis lives in another command's payload**, so the two commands a newcomer runs first are the two that publish the figure without it. Acceptance: the sentence names the population it counted (or counts every language and says so), and the basis travels beside the number in the command that prints it, not only in `migrate-check`. |
|
|
657
883
|
| B27 | **The route into an external baseline history exists and no surface names it.** Under `ASK_READONLY=1` the round concluded that *«la única función de series temporales exige contaminar el repositorio»*: `ask baseline capture . --dir /external` writes, `ask baseline diff . --baseline /external/<sha>.json` compares, and `ask baseline trend . --dir /external` / `--baselines /external` answer `INVALID_INPUT: No such option`. | 5.8.7 audit (round 11, `BUG-05`) | Low — reported High; downgraded on measurement, with the disclosure half intact | **open (discoverability), premise refuted.** `trend` and `baseline trend` take the history directory **as their positional argument**: `_resolve_history_dir` (`cli.py:8508-8523`) uses a directory that already holds baselines as it is, so `ask trend /external` is exactly the read the round wanted and the time series is reachable without writing a byte into the repository. `--dir` is a spelling those two commands do not have, not a capability they lack. What is defective is the surface on both ends: the root help publishes the location as *«`<repo>/.ask/baselines/` (or `--dir`)»*, which reads as a flag every baseline command takes; and `trend`'s own `INVALID_INPUT` for an empty history names `ask baseline capture` and never the directory route it accepts. Acceptance: the two commands accept `--dir` as an alias of the positional (C4-25's rule — an alias, never a rename), and the refusal names the external-directory route it already supports. This is also the residual of **F-AX**: the auditor-on-somebody-else's-repository case is served, and reads as unserved. |
|
sourcecode/_docs/USER_GUIDE.md
CHANGED
|
@@ -46,7 +46,7 @@ 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.8.
|
|
49
|
+
5.8.21, `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
|
|
|
@@ -142,7 +142,7 @@ pipx install sourcecode # isolated install, no venv needed
|
|
|
142
142
|
|
|
143
143
|
# Verify
|
|
144
144
|
ask version
|
|
145
|
-
# ask 5.8.
|
|
145
|
+
# ask 5.8.21
|
|
146
146
|
```
|
|
147
147
|
|
|
148
148
|
Requires Python 3.9+.
|
|
@@ -1555,6 +1555,13 @@ inventories `rules_run` / `rules_not_run` with `rules_run_count` and
|
|
|
1555
1555
|
`rules_total`. Measured on BroadleafCommerce (2 766 Java files): a 4.5s budget
|
|
1556
1556
|
stops at 1 408 files inside the `SEC-004` group and ends at 5.1s.
|
|
1557
1557
|
|
|
1558
|
+
**Memory budget.** Repository-scoped JSON answers include `_meta.memory` with
|
|
1559
|
+
`unit: "MB"`, the observed `peak_rss_mb`, and `configured_max_rss_mb`. To bound
|
|
1560
|
+
long analyses on a CI or supervised runner, set `ASK_MAX_RSS_MB=<megabytes>`.
|
|
1561
|
+
Exceeding the ceiling produces exit code `1` and a structured
|
|
1562
|
+
`MEMORY_TOO_LARGE` error with the peak and configured limit; no partial answer is
|
|
1563
|
+
published. Unset the variable, or set it to `0`, to disable this ceiling.
|
|
1564
|
+
|
|
1558
1565
|
Every count in such an answer is a floor over what ran — a family or phase that
|
|
1559
1566
|
never ran is named, never reported as an absence of findings. A command outside
|
|
1560
1567
|
the set `ask --help` publishes does not consume this budget; bound it with
|
sourcecode/audit_report.py
CHANGED
|
@@ -45,6 +45,8 @@ def build_audit_report(
|
|
|
45
45
|
progress: "Optional[Callable[..., Any]]" = None,
|
|
46
46
|
checkpoint: "Optional[Callable[[str, dict], None]]" = None,
|
|
47
47
|
partial_status: "Optional[Callable[..., dict]]" = None,
|
|
48
|
+
ci: bool = False,
|
|
49
|
+
allow_partial: bool = False,
|
|
48
50
|
) -> dict[str, Any]:
|
|
49
51
|
"""Build the buyer-readable audit bundle from existing command payloads.
|
|
50
52
|
|
|
@@ -65,6 +67,7 @@ def build_audit_report(
|
|
|
65
67
|
risk = build_risk(
|
|
66
68
|
root, limit=risk_limit, min_band="low", profiles=profiles,
|
|
67
69
|
progress=progress, checkpoint=checkpoint, partial_status=partial_status,
|
|
70
|
+
ci=ci, allow_partial=allow_partial,
|
|
68
71
|
)
|
|
69
72
|
risk_source = "computed"
|
|
70
73
|
else:
|
|
@@ -133,6 +136,21 @@ def build_audit_report(
|
|
|
133
136
|
unsigned["_partial"]["cut_in"] = "risk"
|
|
134
137
|
if risk_source == "from-risk":
|
|
135
138
|
unsigned["_partial"]["cut_in"] = "the `--from-risk` payload"
|
|
139
|
+
# AUD-596-X01: the block was copied whole from `risk`, so a bundle
|
|
140
|
+
# built from a cut `--from-risk` file inherited that run's exit
|
|
141
|
+
# decision — a fact about a different process. This command's gate
|
|
142
|
+
# is this command's, and it is stamped before the signature.
|
|
143
|
+
from sourcecode.partial_contract import (
|
|
144
|
+
annotate_gate_exit,
|
|
145
|
+
gate_process_exit_code,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
annotate_gate_exit(
|
|
149
|
+
unsigned["_partial"],
|
|
150
|
+
process_exit_code=gate_process_exit_code(
|
|
151
|
+
ci=ci, was_cut=True, allow_partial=allow_partial
|
|
152
|
+
),
|
|
153
|
+
)
|
|
136
154
|
from sourcecode.provenance import build_evidence_manifest
|
|
137
155
|
unsigned["evidence_manifest"] = build_evidence_manifest(
|
|
138
156
|
unsigned,
|
|
@@ -210,6 +210,14 @@ EXCLUSION_REASONS: "dict[str, str]" = {
|
|
|
210
210
|
"that no analyser opens"
|
|
211
211
|
),
|
|
212
212
|
"own-footprint": "this tool's own output, which the run wrote itself",
|
|
213
|
+
"inert-artefact": (
|
|
214
|
+
"documentation or a build artefact by construction (.md, .txt, .png, "
|
|
215
|
+
".jar, …) that no analyser parses"
|
|
216
|
+
),
|
|
217
|
+
"pruned-directory": (
|
|
218
|
+
"inside a directory the source walk never descends into (build output, "
|
|
219
|
+
"vendored trees, VCS metadata)"
|
|
220
|
+
),
|
|
213
221
|
}
|
|
214
222
|
|
|
215
223
|
|
|
@@ -224,13 +232,22 @@ def dirty_exclusion_reason(
|
|
|
224
232
|
`has_uncommitted_changes: false` sat beside a `?? .claude/settings.json` that
|
|
225
233
|
`git status` had just printed and the payload could not explain the pair.
|
|
226
234
|
"""
|
|
227
|
-
from sourcecode.path_filters import analysis_reads
|
|
235
|
+
from sourcecode.path_filters import analysis_reads, changes_an_answer
|
|
228
236
|
|
|
229
237
|
if not analysis_reads(path):
|
|
230
238
|
return "tool-state-directory"
|
|
231
239
|
probe = str(path).replace("\\", "/").rstrip("/") + "/"
|
|
232
240
|
if any(probe.startswith(prefix) for prefix in own_prefixes):
|
|
233
241
|
return "own-footprint"
|
|
242
|
+
# AUD-596-A03: the rest of the admission rule, which lived only in
|
|
243
|
+
# `verify-edit` and made the two commands contradict each other on the same
|
|
244
|
+
# tree. A report checked out beside the source is not a modification of the
|
|
245
|
+
# source, and it was buying a repository-wide rebuild.
|
|
246
|
+
if not changes_an_answer(path):
|
|
247
|
+
from sourcecode.path_filters import _INERT_SUFFIXES
|
|
248
|
+
|
|
249
|
+
name = str(path).replace("\\", "/").rstrip("/").rsplit("/", 1)[-1].lower()
|
|
250
|
+
return "inert-artefact" if name.endswith(_INERT_SUFFIXES) else "pruned-directory"
|
|
234
251
|
return None
|
|
235
252
|
|
|
236
253
|
|