sourcecode 5.8.22__py3-none-any.whl → 5.8.23__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "5.8.22"
7
+ __version__ = "5.8.23"
@@ -1,3 +1,4 @@
1
1
  """Generated at build time by hatch_build.py. Do not edit (AUD-592-D04)."""
2
2
 
3
- BUILD_COMMIT = '583c07c405358d3e76dd20968960f5ab928608fd'
3
+ BUILD_COMMIT = '775b6cd689fe2700d130c2fe68476eeff5bb893b'
4
+ CLOSURE_PROVENANCE = {'`AUD-592-A02` / `A-1` second half, `§12.5`': {'commit': '63f3d61', 'status': 'present'}, '`AUD-592-B01` / `B-8`': {'commit': '8329614', 'status': 'present'}, '`AUD-592-D01` / `D-1`': {'commit': '0f8e03f', 'status': 'present'}, '`AUD-592-D02` / `D-2`': {'commit': '1d3db4d', 'status': 'present'}, '`AUD-592-D03` / `D-3`': {'commit': 'f28fe40', 'status': 'present'}, '`AUD-592-R01` / `A-1`': {'commit': '216025f', 'status': 'present'}, '`AUD-592-R02` / `N-4`, reopens `AUD-513-N04`': {'commit': 'ec3050d', 'status': 'present'}, '`AUD-593-N01` / `N-01`': {'commit': 'dd6a706', 'status': 'present'}, '`AUD-593-N02` / `N-02`': {'commit': 'f539787', 'status': 'present'}, '`AUD-593-N03` / `N-03`': {'commit': '1dd0b1c', 'status': 'present'}, '`AUD-593-N05` / `N-05`': {'commit': 'aa89388', 'status': 'present'}, '`AUD-593-N06` / `N-06`': {'commit': 'aa584a6', 'status': 'present'}, '`AUD-594-N04` / `D-5`, disclosure half of `B-3`': {'commit': 'a0060b7', 'status': 'present'}, '`AUD-594-X01` / `A-1`, residual of `AUD-593-N03`': {'commit': 'dd222b9', 'status': 'present'}, '`AUD-594-X02` / `A-1` second half': {'commit': '1193937', 'status': 'present'}, '`AUD-594-X03` / `X-03`, residual of `AUD-593-N01`': {'commit': 'e89e8f0', 'status': 'present'}, '`AUD-594-X04`': {'commit': '05b8cc9', 'status': 'present'}, '`AUD-595-A02` / `N-5`, advisory half of `AUD-588-B11`': {'commit': '3e15227', 'status': 'present'}, '`AUD-595-A03` / `B-6` exit-code half': {'commit': 'ab7285f', 'status': 'present'}, '`AUD-595-A05`': {'commit': 'e0b3d10', 'status': 'present'}, '`AUD-595-A07`, narrow half of `AUD-592-A02`': {'commit': 'cc15177', 'status': 'present'}, '`AUD-595-B01` / §B': {'commit': 'b77a1ad', 'status': 'present'}, '`AUD-595-Q01`': {'commit': '91a6566', 'status': 'present'}, '`AUD-596-A01` / `ASK-DET-001`': {'commit': 'fa508b4', 'status': 'present'}, '`AUD-596-A02` / `ASK-SELF-001`': {'commit': '4dfadc8', 'status': 'present'}, '`AUD-596-A03` / `ASK-C1-001`': {'commit': 'bffa3a6', 'status': 'present'}, '`AUD-596-A04` / `ASK-UX-003` + `ASK-UX-004`': {'commit': 'f0aac3c', 'status': 'present'}, '`AUD-596-B01` / `B-01`, harder witness for `BUG-6`': {'commit': '567d09d', 'status': 'present'}, '`AUD-596-B03` / `B-03`': {'commit': 'a8cb814', 'status': 'present'}, '`AUD-596-B06` / `B-06`': {'commit': '8107927', 'status': 'present'}, '`AUD-596-B07` / `B-07`, class residual of `AUD-592-R02`': {'commit': 'e060485', 'status': 'present'}, '`AUD-596-B09` / `B-09`': {'commit': '5b8146c', 'status': 'present'}, '`AUD-596-B10` / `B-10`': {'commit': '5cb994d', 'status': 'present'}, '`AUD-596-B16` / `B-16`': {'commit': '1507f69', 'status': 'present'}, '`AUD-596-D02` / `B-08`, residual of `AUD-594-N04`': {'commit': 'e7f73c3', 'status': 'present'}, '`AUD-596-D03` / `ASK-AGT-001`': {'commit': '8c46967', 'status': 'present'}, '`AUD-596-D04` / `B-18`': {'commit': '040f027', 'status': 'present'}, '`AUD-596-D05` / `B-19`': {'commit': 'fd2bf68', 'status': 'present'}, '`AUD-596-D06` / `B-20`': {'commit': '784cd83', 'status': 'present'}, '`AUD-596-D07` / `B-17`, half refuted': {'commit': 'c3ff822', 'status': 'present'}, '`AUD-596-D08` / `B-22`, class residual of `AUD-591-A06`': {'commit': 'b396c6a', 'status': 'present'}, '`AUD-596-D09` / `B-23`, surviving half of `B26`': {'commit': 'fd3d63f', 'status': 'present'}, '`AUD-596-D11` / `ASK-UX-005`': {'commit': 'f4b4f77', 'status': 'present'}, '`AUD-596-D13` / `B-28`': {'commit': '1014c72', 'status': 'present'}, '`AUD-596-D14` / `B-15`, narrow half of `B20`': {'commit': '10f2827', 'status': 'present'}, '`AUD-596-X01` / `ASK-GATE-001`, gating half of `B24`': {'commit': 'e59c655', 'status': 'present'}, '`AUD-596-X02` / `ASK-UX-001` + `ASK-UX-002` + `B-21`': {'commit': 'f6ebf50', 'status': 'present'}, '`AUD-597-A02` / `ASK-UX-008` + `ASK-DOC-002`': {'commit': '9d47a4c', 'status': 'present'}, '`AUD-597-A03` / `ASK-DET-001`': {'commit': '4c7622a', 'status': 'present'}, '`AUD-597-A04` / `ASK-LEDGER-001`': {'commit': '46fb870', 'status': 'present'}, '`AUD-597-D01` / `B-04`': {'commit': '1094e6a', 'status': 'present'}, '`AUD-597-D02` / `ASK-DOC-001`': {'commit': 'ecb1bb7', 'status': 'present'}, '`AUD-597-D04` / `N-05`': {'commit': 'e51c087', 'status': 'present'}, '`AUD-597-D05` / `N-04`, fourth appearance of `C4-27`': {'commit': '9cccc15', 'status': 'present'}, '`AUD-597-D06` / `ASK-CLI-001`': {'commit': '178aa6d', 'status': 'present'}, '`AUD-597-D07` / `N-03`': {'commit': '9245e35', 'status': 'present'}, '`AUD-597-R01` / `ASK-PERF-001`': {'commit': '6910054', 'status': 'present'}, '`AUD-597-R02` / `ASK-ENC-001`': {'commit': 'ce6dac1', 'status': 'present'}, '`AUD-597-R03` / `ASK-UX-002`': {'commit': 'c57e50b', 'status': 'present'}, '`AUD-597-X01` / `ASK-BUILD-001` + `ASK-META-001` / `B-12`': {'commit': '354338b', 'status': 'present'}, '`B-3`': {'commit': '773268d', 'status': 'present'}, '`B-6`': {'commit': '17c6e06', 'status': 'present'}, '`BUG-4a` / `AUD-588-B11` residual': {'commit': '24b4185', 'status': 'present'}, '`BUG-4b` / `AUD-588-B11` residual': {'commit': '0376350', 'status': 'present'}, '`E-39`, class residual of `AUD-593-N06`': {'commit': '25fc11f', 'status': 'present'}, '`N-5`': {'commit': 'c68083f', 'status': 'present'}, '`N-7`': {'commit': '082e9de', 'status': 'present'}, '`R-1`': {'commit': '424fb1c', 'status': 'present'}, '`R-2`': {'commit': '5158b92', 'status': 'present'}, '`R-3`': {'commit': '88aec04', 'status': 'present'}, '`R-4`': {'commit': '04fd04f', 'status': 'present'}}
@@ -16,7 +16,88 @@ 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.21`, 2026-08-22, two independent rounds,
19
+ **Latest attached audits — audit of release `5.8.22`, 2026-08-22, two independent rounds, and
20
+ the first pass in which every headline finding is falsifiable from outside.** Round A (MSAS,
21
+ `banyan-v2` @ `3dde0376`, the same subject bit for bit for the sixth consecutive round — 57
22
+ invocations, `ASK_READONLY=1` and an explicit budget on every one, 0 tracebacks, 0 writes)
23
+ scores **8.58/10** against 7.77 — the largest rise of the series — and its verdict is **buy and
24
+ upgrade**. Round B (the 16-repository bank, ~95 invocations, Windows 11 + pipx) scores
25
+ **8.8/10** against 8.6, verdict **adopt without reservations**. Both rounds verify the eleventh
26
+ pass's two P1 regressions closed from outside: `ask --help` **177.4 s → 0.8 s** on the monolith,
27
+ **>400 s → 1.67 s** in a 16-repository working directory and **0.48 s** in a tree with no Java in
28
+ it; `selftest` 12 rows, **0 errors, 0 B of stderr** where three rows had been dying with
29
+ tracebacks under an exit code of 0. `AUD-597-A03` is verified by repetition: `population_total`
30
+ 1908 and `unconditional` 1903, five runs out of five.
31
+
32
+ **The precondition is paid, and it changed what an audit can say.** `build_commit` is stamped
33
+ (`583c07c405358d3e76dd20968960f5ab928608fd`, with `_build_commit.py` travelling inside the
34
+ wheel), so for the first time in eight reports every measurement below is attributable to a
35
+ named artefact. The consequence arrived in the same round: **five closures this ledger declares
36
+ are not present in the binary that carries it.** `AUD-597-A01` (the front-page block still
37
+ recommends four of five lines unbounded, measured on four repositories from 49 to 4 772 Java
38
+ files, so it is not conditional on size), `AUD-596-D01` twice over (`validation` and
39
+ `data-exposure` carry no run identity at any depth, and one `md5` is identical for two different
40
+ repositories for the third consecutive round), `AUD-596-D10` (five complete payloads searched,
41
+ zero memory keys) and `AUD-597-D03` (`route_surface_changed.items[0].source_file` is the empty
42
+ string). The honest reading is the one Round B gives: the packaged ledger runs ahead of the
43
+ binary that transports it, and the product warns only about the opposite direction (*"rows closed
44
+ after it shipped are not in this copy"*). That is `AUD-597-A04` reopened with five witnesses and,
45
+ now, with a commit to settle it against — filed as `AUD-598-X01`.
46
+
47
+ **One regression, and it is performance.** `ask . --compact` goes 170.8 s (5.8.20) → 154.0 s
48
+ (5.8.21) → **214.6 s** (5.8.22): +39 % against the release it follows and worse than the one
49
+ before it, on a **byte-identical 23 458 B payload**. `ask . --agent` does not move on the same
50
+ tree in the same session (185.5 → 180.8 s), so the shared model build is not what grew; it is
51
+ specific to the `--compact` view. Round B's bank sees none of it — seven of seven anchors flat or
52
+ better, intra-series dispersion 3–7 % — so the row is scale- or view-conditioned rather than a
53
+ host effect. It is `AUD-598-R01`, and it leads the queue.
54
+
55
+ **Two adverse readings are recorded as not regressions, on purpose.** `selftest` 153.4 → 188.2 s
56
+ (+23 %) carries **one more row doing real work**, and Round A counts it as coverage cost rather
57
+ than drift; and the `×2`–`×3.7` block the tenth pass attributed to the retired parse cache is
58
+ back at baseline (`compare` 106.6 → 59.2 s, `endpoints --servlets` 27.6 → 6.5 s, `impact` at its
59
+ anchor), which confirms that retraction instead of adding a fix. **Zero content drift for the
60
+ sixth consecutive round**: 3 763 endpoints, 725 `no_security_signal`, census 3763/3872/1120,
61
+ 373 findings / 94 defects / 43 003 symbols, `posture` 143 271 B five times, `spring-audit
62
+ --min-severity high` 153 299 B twice byte-identical — where 5.8.21 still varied by ±3 B on
63
+ clocks. Round B's 16-of-16 non-regression sweep over what 5.8.21 closed is intact, and 16 of 16
64
+ trees are unchanged after ~95 invocations.
65
+
66
+ The standalone audit record is [`AUDIT-2026-08-22-5.8.22.md`](AUDIT-2026-08-22-5.8.22.md).
67
+ It preserves the methodology, release identity, open queue and the five artefact/ledger
68
+ discrepancies without replacing this ledger's row-level status authority.
69
+
70
+ **External corpus feature battery — 2026-08-22.** A separate read-only battery
71
+ against 37 repositories under `/Users/user/Documents/workspace/testing` exercised
72
+ the new assessment/gate packs, manifests, output artifacts, MCP projection, CI
73
+ renderers, deployment evidence and content determinism. Thirty repositories
74
+ completed an `ask-pack-assessment-v1` pack with six components, `MEASURED` and
75
+ exit code 0. Seven (`alfresco-community-repo`, `cas`, `keycloak`, `nacos`,
76
+ `neo4j`, `shenyu`, `tutorials`) exceeded an external 120-second test budget and
77
+ did not produce a final aggregate. This is recorded as an operational
78
+ validation gap, not as a regression, because no comparable baseline exists.
79
+
80
+ The battery did produce one reproducible contract gap: `AUD-599-C01` (P2).
81
+ With `--output-dir` but no `--output`, component artifacts were persisted but
82
+ the aggregate remained an inline response; four large repositories therefore
83
+ ended with `OUTPUT_TOO_LARGE`. It is now corrected in the checkout: the
84
+ aggregate is written as a deterministic `pack.json` (or format-specific
85
+ extension) and an explicit `--output FILE` still wins. Regression tests cover
86
+ both routes. `AUD-599-P01` is closed as a scale limitation after a same-protocol
87
+ baseline and repeat: the same seven repositories timed out, but no comparable
88
+ release baseline or internal phase timings exists to justify an optimization.
89
+ `AUD-599-T01` is covered by a controlled `PASS`/`BLOCK`/`UNVERIFIED` fixture
90
+ with distinct exit codes. `AUD-599-Q01` is closed: all 37 before/after Git
91
+ snapshots were identical.
92
+
93
+ The same battery found no confirmed analysis drift, no MCP field loss, no CI
94
+ renderer defect, and stable component `content_id` values across repeated
95
+ assessment runs. Deployment artifacts were preserved in manifests while
96
+ posture correctly kept profile selection as an assumption when the artifact did
97
+ not decide it. The full evidence and exact measurements are in
98
+ [`AUDIT-TESTING-CORPUS-2026-08-22.md`](AUDIT-TESTING-CORPUS-2026-08-22.md).
99
+
100
+ **Previous attached audits — audit of release `5.8.21`, 2026-08-22, two independent rounds,
20
101
  and the first pass whose queue is led by regressions the previous queue's own fixes
21
102
  introduced.** Round A (MSAS, `banyan-v2` @ `3dde0376`, the same subject bit for bit for the
22
103
  fifth consecutive round — 107 invocations, 43 commands) scores **7.77/10** against 8.17 on
@@ -275,6 +356,71 @@ with neither parseable stdout nor its requested artifact) or `BUG-6`
275
356
  queue position. `BUG-1` and `BUG-5` gain witnesses on public OSS repositories, recorded
276
357
  under their own rows rather than as new ones.
277
358
 
359
+ ### Twelfth Audit Pass: `5.8.22` findings / MSAS (round A) + 16-repository bank (round B) / 2026-08-22
360
+
361
+ **Open queue.** Ordered as the correction order is: the performance regression first, then the
362
+ class that five separate witnesses point at — a closure this ledger declares that the shipped
363
+ binary does not carry — then the row that is that class's sharpest instance, then the residues.
364
+ Round A's ids are `ASK-*`, Round B's are `N-*` and `B-*`; a finding that is a new witness for a
365
+ row this ledger already holds is named in *Mapped, not new* below rather than filed twice.
366
+
367
+ **Scores.** Round A **8.58/10** against 7.77, same seven-axis rubric, same subject bit for bit
368
+ for the sixth consecutive round (57 invocations); verdict **buy and upgrade**, and it is the
369
+ version this round would deploy of the six it has audited. Round B **8.8/10** against 8.6, same
370
+ rubric and same 16-repository bank (~95 invocations); verdict **adopt without reservations**, and
371
+ it calls this the lowest-risk round of the five it has measured: two of its own P1 regressions
372
+ closed inside 24 hours, zero new regressions, performance uniformly better. **Neither rise comes
373
+ from a lowered bar.** Round A raises determinism 6.0 → 8.5 on repetition it performed itself, and
374
+ Round B *lowers* precision 8.6 → 8.0 on the five unconfirmed closures — its own words: in a
375
+ product whose central argument is the accounting of what it knows, a `closed` that does not
376
+ survive a field check weighs more than its severity.
377
+
378
+ **What the queue does not contain.** No content regression on either subject; no traceback in
379
+ ~150 invocations across both rounds; no unauthorised write over 17 trees, with
380
+ `git status --ignored --short` identical before and after, `.ask/` never created and 0 hooks
381
+ installed; `--dry-run` and `--no-write` correct on all three commands that write.
382
+
383
+ | ID | Severity | Current status | Required direction |
384
+ | --- | --- | --- | --- |
385
+ **Implementation note for `AUD-598-X01`:** in progress in `16ce5d0`. The build
386
+ hook now generates `CLOSURE_PROVENANCE` beside `BUILD_COMMIT` and rejects a
387
+ build when an explicit closed-row commit is not an ancestor of the stamped
388
+ build. The checkout contains 71/71 tracked closure commits; a real wheel
389
+ inspection is still required before changing the defect row to `closed`. See
390
+ [`DESIGN-build-closure-provenance.md`](architecture/DESIGN-build-closure-provenance.md).
391
+
392
+ | `AUD-598-R01` / `ASK-PERF-002` | **P1 — regression, on the view the front page recommends for agent context** | **open.** `ask . --compact` 170.8 s → 154.0 s → **214.6 s**, +39 % against the release it follows and worse than the one before it, on a **byte-identical 23 458 B payload**. `ask . --agent` does not move on the same tree in the same session (185.5 → 180.8 s), so the shared model build is not what grew. Round B's bank sees none of it: 7 of 7 anchors flat or better with 3–7 % dispersion, so it is view- or scale-conditioned, not a host. The cache was warm on both sides of the measurement | Attribute it from the payload rather than from a stopwatch: the root command already publishes per-phase `timings`, so run `--compact` against no flag on one tree and read where the 60 s lands. Both views project the same model, so a delta of that size is in the projection or in what the bounded view recomputes to build it. The battery assertion is a **ratio** (`--compact` against `--agent` on one repository), never an absolute second count on a subject nobody else can run |
393
+ | `AUD-598-X01` / `N-01` + `B-13` + `B-14` + `B-29` + `N-06` | **P1 — the ledger is this product's strongest commercial argument, and five of its cells are wrong in the shipped artefact** | **open, and it is `AUD-597-A04` reopened with five witnesses and a commit to settle it against.** Measured on the installed wheel at `583c07c`: the front-page block still recommends four of five lines unbounded on four repositories from 49 to 4 772 Java files, so it is not conditional on size (`AUD-597-A01`, third round, second declared closure); `validation` and `data-exposure` carry no run identity at any depth of a recursive search and both still lack `schema_version` (`AUD-596-D01`); one `md5` is identical for two different repositories, third round (`AUD-596-D01`); five complete payloads searched for a memory key, zero hits, `timings` still seven keys and none of memory (`AUD-596-D10`); `route_surface_changed.items[0].source_file` is the empty string and there is no `declaration_line` (`AUD-597-D03`). Round B's reading is the fair one and this ledger adopts it: no bad faith, the packaged ledger runs **ahead of** the binary that transports it, and `ask version` warns only about the opposite direction | Make it mechanical, now that there is a commit to check against. Each `closed` cell carries its fix commit already; resolve those at **build** time and stamp, beside `_build_commit.py`, whether each cited commit is an ancestor of `build_commit` — a generated table, not a hand-kept one. Then a `selftest` row fails the build when a row claims a closure this artefact cannot contain, and the five witnesses above are answered by construction rather than one at a time. This closes the class; it does not close the five underlying defects, which stay filed where they are |
394
+ | `AUD-598-B01` / `ASK-META-001` | **P1 — it blocks the only published non-drift assertion this product has** | **open, and it is the sharpest instance of `AUD-598-X01`.** Round A finds `_meta` absent from **9 of 9** payloads on the installed wheel — `posture`, `endpoints`, `spring-audit`, `validation`, `migrate-check`, `impact-chain`, `selftest`, `data-exposure`, and the root, whose `_meta` carries only `timing_ms` and `token_economy`. Zero `content_id`, zero `memory`, with `--output` as well as on stdout. Second round for this reading, and the eleventh pass measured the **opposite** at HEAD on three of those same payloads and closed the row on it. Both measurements are honest and they cannot both describe one artefact. The irony is measured and belongs in the row: `AUD-597-A03` was closed without the tool built to detect it, whose own basis says *"payload size cannot either — 1908 and 1904 weigh the same"* | Measure **inside a built wheel**, not in the checkout — that is the whole lesson of `AUD-598-X01`, and this row is where it is cheapest to apply. Then find the seam: `_stamp_envelope` returns the content untouched when the payload is not a JSON object and when Click cannot name the running command, and a root `_meta` carrying exactly two keys says something other than the envelope wrote it. The assertion belongs in the release battery, over a wheel, one payload per emit path |
395
+ | `AUD-598-B02` / `ASK-UX-002` | **P2 — an agent that follows the advice enters a cycle** | **open. `AUD-597-R03` closed the size regression and left the no-op underneath it, which has now become a loop.** `posture . --diff dev:prod` estimates 71 116 tokens / 284 464 B and answers *"Use --compact…, --limit N…"*; with `--compact`, the same estimate to the byte and *"Use --limit N…"*; with `--limit 20`, the same estimate again and *"Use --compact…"*. Three invocations, one estimate, and the third hint returns to the first. The regression half is confirmed fixed: `--compact` no longer inflates the payload (287 454 → 284 464 B, equal to no flag) | A refusal must not offer a remedy it has not measured. Either the bounded view actually bounds this answer — `--diff` composes two profile sets and the rows that make it large are outside what `--compact` reaches, which is `AUD-596-X02`'s shape on a second surface — or the hint stops naming the flag for this command. Cheapest correct form: compute the bounded projection's size before recommending it, and when no offered flag moves the estimate, say that instead of listing them |
396
+ | `AUD-598-B03` / `B-11` | **P2 — ninth report, and the precondition it was blocked on is gone** | **open.** 17 cost anchors read *"on 5.1.0"* inside a `5.8.22` build. Beside it, the published anchor for `risk` is **77.2 s** where the command exhausts every budget it is given on the audited monolith — 301 s under a 300 s budget in 5.8.20, **401 s under 400 s** now — so the number a buyer reads is not reachable on the subject the same report measures. That `--ci` now exits 75 there turns it into an honest failure instead of a false green, which is why the round does not lower the score further | Regenerate the anchors attached to `build_commit`, and publish each anchor's **subject** beside its number: an anchor without the repository it was measured on is not falsifiable, and this is the ninth report to say so. Where a command cannot finish on a class of repository, the anchor's honest form is the refusal and its budget, not a smaller number measured elsewhere |
397
+ | `AUD-598-B04` / `N-02` | **P3 — refused on its literals, with a real half underneath** | **open on the disclosure half; the parse half is refused, third re-derivation.** `1e9` is a valid budget: `_parse_analysis_budget_env()` parses with `float()`, refuses non-numbers, non-finite and `<= 0`, and the correction is already in that function's own docstring from `AUD-596-B03`. `1_000` is the same fact — Python's `float()` accepts the underscore literal, so it is 1 000 seconds. Neither is invalid input and neither will start being rejected. What survives is the round's observation stated correctly: a budget that **cannot bind** is accepted in silence, and a caller who typed one believes a deadline is in force. The four genuinely new classes the round checked (`inf`, `nan`, `+60`, `60.0`) behave as declared | Say it once, where the budget is read: when the configured value exceeds the command's own published anchor by a wide margin, publish an advisory that this budget will not bind. That is a disclosure over a fact the product already holds, not a new rejection — rejecting a valid float is the confident-refusal shape this ledger has already refused twice |
398
+
399
+ **Mapped, not new.** `ASK-PERF-001`, `ASK-ENC-001`, `ASK-DET-001`, `ASK-BUILD-001`,
400
+ `ASK-UX-008`, `ASK-LEDGER-001`, `ASK-DOC-001` and `ASK-DOC-002` are the eleventh pass's rows
401
+ verified closed from outside and are recorded there, not re-filed here. `B-26` is `ASK-17`
402
+ (parse cache 539.04 MB over a 512 MB budget with five warm repositories — open and declared by
403
+ the product itself), `B-27` is `C3-128` (`timeline` with no ceiling possible), `B-21` is
404
+ `AUD-596-X02` (`posture` pays the full analysis before refusing on the ceiling), `B-30` is the
405
+ surface-contract row (20 contracts, all `forbid_finding`), and `B-22`'s residue — `compare`
406
+ declares `resolved_count` and `unresolved_count` and still exits 0 — carries forward on
407
+ `AUD-596-D08` rather than opening a row. `N-04`'s residue is the `message` half of
408
+ `AUD-597-D05`: the hint no longer returns to the error, and *"Possible options: --depth"*
409
+ survives in the message beside it. `ASK-CLI-001` is closed on the cost that carried it and its
410
+ design half — `plan` and `fix-bug` do not accept `--path` — is a grammar decision, not a defect.
411
+
412
+ **Refused, with its price, and not re-litigated.** `ASK-GATE-001`'s residue is
413
+ `migrate-check --ci` and `migrate-apply --ci` exiting 2 with *"No such option: --ci"*. That is
414
+ the refusal `AUD-597-D01` published with its reason: both commands are `advisory_only`, the run
415
+ always completes, so a completeness gate on them could only ever exit 0 — a gate that cannot
416
+ fail, published as a gate. Round B reads the corrected help and closes the row on the authority
417
+ being fixed. It stays refused, and the price stays on the record.
418
+
419
+ **Round B's own methodological note, kept because it constrains what this ledger may publish.**
420
+ No absolute figure from that bank is publishable — only ratios between versions measured back to
421
+ back in one session on one host. It is the constraint that retracted the `×1.93` of the tenth
422
+ pass, and it is the reason `AUD-598-R01` above is specified as a ratio.
423
+
278
424
  ### Eleventh Audit Pass: `5.8.21` findings / MSAS (round A) + 16-repository bank (round B) / 2026-08-22
279
425
 
280
426
  **Open queue.** Ordered as the correction order is: the two regressions first, then the
@@ -323,13 +469,13 @@ invocations is `AUD-597-R02`, and it is the first in five rounds.
323
469
  | `AUD-597-R02` / `ASK-ENC-001` | **P1 — regression, Windows, and it makes a CI gate green over three unrun rows** | **closed 2026-08-22 (`ce6dac1`).** All three parts. (1) `encoding="utf-8", errors="replace"` at the `subprocess.run` the row names. (2) A stream that is not a string reads as no document, never as a `TypeError`. (3) `summary.error > 0` exits **75** — the code the product already reserves for *"the gate did not finish"* — while a `fail` stays an answer and stays 0. The self-referential row ships as `AUD-597-R02` in `selftest.ROWS`: it drives `selftest --rows B6` and reads both halves, no row in `error` and an exit code that agrees with the summary; one named child row is what stops the recursion. Not re-measured on Windows — closed against the mechanism and a fixture that reproduces the decode end to end. *Original note:* **open.** `selftest .` returns **rc=0** with **3 of 11 rows in `error`**, 2 458 B of stderr and **three Python tracebacks** — the first tracebacks in five rounds, against *"0 tracebacks"* as a measured property of the four before it. `UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 148984`. **Confirmed in source**: `selftest.py:96-97` runs `subprocess.run(argv, capture_output=True, text=True, timeout=…)` with **no `encoding=`**, so the child's UTF-8 output is decoded with `locale.getpreferredencoding()` — `cp1252` on the audit host. The reader thread dies, `capture_output` leaves the stream `None`, and `json.loads(None)` at `:103` raises the `TypeError` the row publishes as `observed`. Exactly the three repository-wide rows fail, because only their payloads reach the failing offset — and they are the identity and contract rows (`AUD-590-B01`, `B6`, `ASK-16`). Every ASK note carries an em dash, so the mechanism is deterministic by construction. | Three parts, and the first is one line. (1) `encoding="utf-8", errors="replace"` at `selftest.py:96-97`. (2) `stream is None` produces a `fail` naming the cause, never an `error` carrying a `TypeError` — a row that could not be evaluated is not a row that passed. (3) **`summary.error > 0` must not exit 0**; the product already reserves `75` for *"could not run"* and this is that case. Add the self-referential row the report asks for: a `selftest` row asserting `summary.error == 0`. |
324
470
  | `AUD-597-X01` / `ASK-BUILD-001` + `ASK-META-001` / `B-12` | **P1 — the precondition, seventh consecutive report, and this pass is what it costs** | **closed 2026-08-22 (`354338b`).** The reader, the writer and the hook were all already in the tree; the wheel shipped without the file. Hatchling selects a wheel's contents through the VCS ignore rules and `src/sourcecode/_build_commit.py` is in `.gitignore` — correctly, it is generated — and nothing had reconciled the two facts. `artifacts` in the wheel target is the declaration for exactly that. `reproduced_on`: a wheel built from this checkout — **stamp ABSENT → `sourcecode/_build_commit.py`, `BUILD_COMMIT = 'c57e50b…'`**. The assertion is made against the artefact, which is the lesson of seven reports: the previous battery asserted the writer and the reader's precedence, both were true, and the wheel was still empty. It now builds the wheel and reads inside it — the module is present, the value is a 40-character object name, it is the HEAD the build ran against, and an install of that wheel reports it with `ASK_BUILD_COMMIT` cleared. **Still outstanding and not a code change: the release carries no tag and is not merged to `master`.** *Original note:* **open.** `build_commit: "unrecorded"`. The reader ships (`product_info.py:26-48`), the writer ships as importable code (`build_stamp.py`), and `<pkg>/_build_commit.py` — the only one of the three sources that can work inside an installed wheel — is not in the package. **This round is the bill.** Four rows were declared `closed 5.8.21` with a commit hash and two of them reproduce in the field; `_meta` is absent from 18 of 18 field payloads and **present at HEAD on 3 of 3 measured**; the release commit `bcb9e4d` is on `origin/fix/p01-no-confident-zero` and there is **no `v5.8.21` tag** (the newest tag is `v5.8.2`) and no merge to `master`. Nobody outside — and nobody inside — can say which tree the audited artefact was built from, so *"the fix does not work"* and *"the binary does not carry the fix"* are indistinguishable, in both directions. | Stamp the commit at build time and prove it from the built wheel, not from the checkout: a post-packaging smoke test that installs the wheel and imports `sourcecode._build_commit`, asserting `BUILD_COMMIT != "unrecorded"`. Until it exists, **no row may be filed `closed` with a commit hash** — the hash is a claim about an artefact nobody can identify. Tag the release, and publish the tag with it. |
325
471
  | `AUD-597-R03` / `ASK-UX-002` | **P2 — regression, and it is monotone in the wrong direction for an agent** | **closed 2026-08-22 (`c57e50b`).** **One report claim corrected against source**: the proposed remedy — emit a `*_cap` block only when `omitted > 0` — was already in force, `declare_cap` returns early on an uncut list, so that was not the mechanism. The mechanism is the disclosure: each cut declares itself with the registered `cap_effect` prose, ~640 B of it, so five cuts over five short lists bought back less than the five disclosures cost. `reproduced_on`: smallest reproduction here, **195 B → 2 103 B**. The trade is now measured at each cut — a list is emptied when the rows removed weigh more than the disclosure that replaces them — and `--compact` asks for the smallest honest answer, so where cutting cannot make the answer smaller, not cutting it *is* the smaller answer. Where a cut is declined the payload says so, paid out of what the accepted cuts saved, so the sentence can never take the answer back over the unbounded one. `--limit N` never takes this trade: a caller who names a number asked for a shape. The invariant ships over the whole bounded-flag population read off the registry — every shape × both vocabularies × four bounds, plus `compact_document`. *Original note:* **open.** On the repository where the row was reported, `--compact` now makes the answer **larger**: `posture . --diff dev:prod` estimates 284 464 B, and the same command with `--compact` estimates **287 454 B** (+2 990 B, +0.9 %), both refusing with `OUTPUT_TOO_LARGE`, 2/2 byte-identical estimates per form. In 5.8.20 the flag was a no-op there (284 464 B both ways), so the fix that closed `AUD-596-X02` face (2) turned a no-op into a cost. The mechanism is the disclosure: widening `POSTURE_BOUNDED_LISTS` to `undecided`, `no_rule_matched` and three `chains/*_files` lists brought their `*_cap` blocks with them, and on a repository whose large population is **not** in those lists the disclosure is paid and the trim is never earned. The closing measurement was taken on thingsboard (38 775 → 31 701 B) and the reporting repository was not re-measured — the same failure as `AUD-597-A04`. | Two parts, both already doctrine here. (1) Emit a `*_cap` block only when `omitted > 0`; `interface_mediated_callers_cap` already returns `null` below its threshold, so the pattern exists and is not being applied uniformly. (2) Add the monotonicity invariant to the `ASK-10` battery over the **whole** bounded-flag population, not over `posture`: *a bounded payload is never larger than the unbounded one it replaces*. It is one assertion and it would have caught this before release. |
326
- | `AUD-597-A01` / `ASK-UX-001` + `N-01` | **P2 — a declared closure the field contradicts, and it is a new class** | **closed 2026-08-22 (`f8cf061`).** **Two thirds of the field reading does not reproduce at HEAD**: `endpoints .` and `spring-audit .` both carry `--compact` here, measured — that they did not in the field is `AUD-597-X01`, not this row. What does reproduce is `risk .`, and the cause is the narrowing: `_bounded_invocation` appended one literal. The value is typed rather than the flag skipped — `output_ceiling` publishes each flag *as typed* beside the vocabulary it already owns, so a count flag arrives with a first-look count and the line stays pasteable. `reproduced_on`: `risk .` → `risk . --limit 20`, and the comment column is padded from the longest line actually rendered, so `--compact# effective access` is gone. A command declaring nothing bounding no longer sits under *"Runs now"* at all. Asserted as a population: every line carries its own command's flag, every line parses through the parser it is recommended to, no line under *"Runs now"* is unable to bound itself. *Original note:* **open, and half of it landed.** `AUD-596-X02` face (1) is filed `closed 5.8.21 (f6ebf50)` naming two lines it would fix. **Measured at HEAD**: `posture . --diff dev:prod --compact` and `migrate-check . --compact` carry their flag, so the mechanism is real — and **`endpoints .`, `spring-audit .` and `risk .` are still recommended unbounded** under *"Runs now"*, on a page whose own next sentence claims *"each line above carries the flag its command declares"*. `endpoints .` refuses at ~612 K estimated tokens on MSAS. In the field neither line carried a flag, which is `AUD-597-X01` again. **A second defect is visible in the same block at HEAD**: the comment column is not aligned on the line the fix lengthened — `posture . --diff dev:prod --compact# effective access, two profile sets`, with no space before the `#`. | `_bounded_invocation` returns the invocation unchanged when the only bounding flag it finds is not `--compact`, and `endpoints`/`spring-audit`/`risk` are exactly that case. Take the flag each command **declares** — the same intersection the refusal offers — rather than the one literal the function special-cases, and if a command declares none, the line does not belong under *"Runs now"*: move it to the budgeted block with its ceiling. Pad the comment column after the substitution, not before. Assert over the front page as a population: **every line under "Runs now" runs**, on the largest fixture available. |
472
+ | `AUD-597-A01` / `ASK-UX-001` + `N-01` | **P2 — a declared closure the field contradicts, and it is a new class** | **reopened 2026-08-22 — the shipped wheel (`583c07c`) does not carry it: four of the five front-page lines are still unbounded, measured on four repositories from 49 to 4 772 Java files, so it is not conditional on size. Third round for this reading, second declared closure. A witness of `AUD-598-X01`, not re-filed as a new row.** *Previous verdict:* **closed 2026-08-22 (`f8cf061`).** **Two thirds of the field reading does not reproduce at HEAD**: `endpoints .` and `spring-audit .` both carry `--compact` here, measured — that they did not in the field is `AUD-597-X01`, not this row. What does reproduce is `risk .`, and the cause is the narrowing: `_bounded_invocation` appended one literal. The value is typed rather than the flag skipped — `output_ceiling` publishes each flag *as typed* beside the vocabulary it already owns, so a count flag arrives with a first-look count and the line stays pasteable. `reproduced_on`: `risk .` → `risk . --limit 20`, and the comment column is padded from the longest line actually rendered, so `--compact# effective access` is gone. A command declaring nothing bounding no longer sits under *"Runs now"* at all. Asserted as a population: every line carries its own command's flag, every line parses through the parser it is recommended to, no line under *"Runs now"* is unable to bound itself. *Original note:* **open, and half of it landed.** `AUD-596-X02` face (1) is filed `closed 5.8.21 (f6ebf50)` naming two lines it would fix. **Measured at HEAD**: `posture . --diff dev:prod --compact` and `migrate-check . --compact` carry their flag, so the mechanism is real — and **`endpoints .`, `spring-audit .` and `risk .` are still recommended unbounded** under *"Runs now"*, on a page whose own next sentence claims *"each line above carries the flag its command declares"*. `endpoints .` refuses at ~612 K estimated tokens on MSAS. In the field neither line carried a flag, which is `AUD-597-X01` again. **A second defect is visible in the same block at HEAD**: the comment column is not aligned on the line the fix lengthened — `posture . --diff dev:prod --compact# effective access, two profile sets`, with no space before the `#`. | `_bounded_invocation` returns the invocation unchanged when the only bounding flag it finds is not `--compact`, and `endpoints`/`spring-audit`/`risk` are exactly that case. Take the flag each command **declares** — the same intersection the refusal offers — rather than the one literal the function special-cases, and if a command declares none, the line does not belong under *"Runs now"*: move it to the budgeted block with its ceiling. Pad the comment column after the substitution, not before. Assert over the front page as a population: **every line under "Runs now" runs**, on the largest fixture available. |
327
473
  | `AUD-597-A02` / `ASK-UX-008` + `ASK-DOC-002` | **P2 — the most destructive command in the product cannot be previewed** | **closed 2026-08-22 (`9d47a4c`).** One authority, two consumers, as the row asks. `_BOUNDING_FLAGS` was a vocabulary of *flags*; `_BOUNDING_MODES` sits beside it and is intersected with the command's declared options the same way, so nothing branches on a command name. `reproduced_on`: `export . --c4` now offers `--by-directory`, `--module-graph`, `--integrations` instead of the sentence reserved for a command that declares nothing smaller — which is still printed where it is true. `rename-class` gets the `--summary-only` its sibling `repo-ir` already has: the weight of a dry run is entirely in the per-file diffs, the plan is the file list, so a dry run prints the plan and declares the diffs omitted, `--full-plan` puts them back inline, `--output` takes the whole audit. The smaller view names exactly the files the full one does — asserted, because a preview that is not the plan is worse than no preview. *Original note:* **open.** `rename-class … --dry-run` on MSAS refuses with `OUTPUT_TOO_LARGE`: **2 427 925 estimated tokens, 9 711 702 B**, `hint_basis: {"declared_flags": [], "context_found": false}`, and a hint that reads *"No smaller inline variant is available with the current flags"*. 2/2, both argument forms. The root help lists `rename-class` as the command that rewrites sources without `--dry-run`, so the only safe way to inspect it is the one that cannot print. `export . --c4` is the same defect in the same authority: 385 794 tokens, `declared_flags: []`, the same *"no smaller variant"* sentence — **while `export .` itself advertises `--by-directory` / `--module-graph` / `--integrations`**. Confirmed in source: `output_ceiling._BOUNDING_FLAGS` is a seven-entry **flag** vocabulary (`--compact`, `--summary-only`, `--limit`, `--top-n`, `--min-severity`, `--max-nodes`, `--max-edges`), and a command whose smaller answer is a **mode** rather than a flag is invisible to it. | One authority, two consumers. Teach the ceiling that a command can declare a smaller *mode*, so `export`'s three sections reach `declared_flags` and *"no smaller inline variant"* is only ever printed when the basis says `none`. Then give `rename-class` the `--summary-only` its sibling `repo-ir` already has (25 386 K tokens → 73 783 B): file count, occurrence count, file list without bodies — and make `--dry-run` use it by default, with the full plan behind `--output`. |
328
474
  | `AUD-597-A03` / `ASK-DET-001` | **P2 — a `P1` filed closed that reproduces 5/5 on the repository that reported it** | **closed 2026-08-22 (`4c7622a`).** **The row's hypothesis is refuted with a measurement: it is not the pool.** `BeanGraph.build` chose a class's stereotype with `next(iter(ann_set & _BEAN_ANNOTATIONS))` — a pick out of a set of *strings*, whose iteration order changes with the per-process hash seed. A class annotated `@Entity` and `@Component` resolved to `entity` in some processes and `component` in others, and `spring_beans()` excludes the JPA stereotypes, so the bean left the population on the run where the seed went the other way. `reproduced_on`: `PYTHONHASHSEED=0` → `['com.x.Plain']`, seeds 1-7 → `['com.x.Mixed', 'com.x.Plain']`. Membership, not order, which is why `AUD-596-A01` ordering the population at close could not reach it — and it explains what the row could not: `endpoints` is byte-identical including under `--no-cache --jobs 2` because nothing on that path picks out of a set, and the worker count never entered into it. The three picks take a declared rank now: every Spring stereotype outranks every JPA one, and within Spring the more specific wins. Asserted in new interpreters — eight seeds over the container, six over the whole `posture` payload. MSAS not re-measured; closed against the mechanism, which is seed-dependent rather than repository-dependent. *Original note:* **open.** `posture . summary.population_total` alternates between **1908 and 1904** over five sequential runs on an unchanged tree, warm cache, same `tree_state` — 3/5 give 1908, 2/5 give 1904, with `conditional_beans` fixed at 5 and active/inactive/unresolved fixed at 3/2/0, and **all five payloads weigh exactly 143 271 B**. `AUD-596-A01` is filed `closed 5.8.21 (fa508b4)` on a real and correctly diagnosed defect — node duplication, 683 published where 668 beans exist — measured after on shenyu and halo. **Inflation and membership are two defects.** Ordering the population at close removes duplicates; it does not make the same set arrive. That `endpoints` is byte-identical across runs, including under `--no-cache --jobs 2`, places it in the Spring container build rather than in the route index or in the pool at large. | Make the **set** deterministic, not only its order: collect bean definitions into a map keyed by `(fqn, file, line)` fed by every worker, and do not close the population until the pool is drained. Assert that the worker count does not change the set (`--jobs 1 2 4 8 19`), and assert it **on MSAS-shaped input** — six runs, one value. The row was reported there; per `AUD-597-A04` it cannot close anywhere else. |
329
475
  | `AUD-597-A04` / `ASK-LEDGER-001` | **P2 — the ledger is the product's strongest commercial argument and it is currently wrong in both directions** | **closed 2026-08-22 (`46fb870`).** The seven cells were corrected in place with the entry. The durable half is the widened assertion, and it needed widening twice over: `AUD-594-X04` reads only the preamble of a section *headed* "closed in X" while the declaration lives in *Current Synchronization*, and it matches ids literally while the prose writes `D01` where the cell carries `AUD-596-D01`. Two assertions now: a row named in a closure-declaring sentence anywhere in this document does not read `open`; and a closure declaration names its rows unambiguously, because `D01` matches three passes and `A03` four. `reproduced_on`: both fired on the live ledger. Four declarations corrected — three short ids spelled pass-qualified, and one claim **withdrawn as an overclaim**: `AUD-596-D12` was listed as verified closed from outside beside `D11` and `D03` and is not, its cell reading `open` was right, and its residue is `AUD-597-D02`. *Original note:* **open, and the shipped copy is corrected in this repository as part of this entry.** The 5.8.21 ledger marks `open` two rows that its own binary closed (`D03`, `D11`) and `closed` two that reproduce (`A01`, `X02`). Verified here and worse than reported: **six** rows — `D01`, `D03`, `D08`, `D11`, `D12`, `D14` — still read `open` while their fix commits (`f6ae10e`, `8c46967`, `b396c6a`, `f4b4f77`, `e132458`, `10f2827`) are ancestors of the release commit, and the section preamble already claimed them closed; `D10` cited `10f2827`, which is `D14`'s commit, and its own are `dabcc95` + `493e796`. All seven cells are corrected in place. `AUD-594-X04` is the same class in the pessimistic direction and its assertion only polices sections headed *"closed in X"*, which this one is not. | The durable half is the **closure criterion**, and it is the root cause shared by `AUD-597-R03`, `AUD-597-A01`, `AUD-597-A03` and `ASK-META-001`: four fixes correct in their own frame, verified against a fixture or another repository, none re-measured where the defect was reported. A row carries `reproduced_on` — the original repro command and its post-fix output — or it closes as *"closed against fixture / another repository; original witness not re-measured"*, which is honest and sufficient. Widen the `X04` assertion so a row whose preamble declares it closed cannot ship reading `open`, whatever the section heading says. |
330
476
  | `AUD-597-D01` / `B-04` | **P3 — the asymmetry `AUD-596-X01` closed for two of three** | **closed 2026-08-22 (`1094e6a`), and half of it is refused with its price.** The missing flag is the second step: the help told the reader `migrate-check` has a deadline. Every surface rendered the eight-command population as *"deadline for …"* and three of them have one. `migrate-check` and `migrate-apply` are `advisory_only` — the run always completes — so a completeness gate on them can only ever exit 0 and `--allow-partial` would name a state that cannot occur: shipping them is `AUD-596-X01`'s own shape turned around, a gate that cannot fail published as a gate. Making them gate for real means making them phase runners, which is `B24`'s first acceptance half. What ships: `analysis_budget.budget_tiers()` splits the population with `phased_run.budget_scope()` — the function that has answered this since `b62d840` — so the help reads *"deadline for spring-audit, risk, audit-report — advisory only (the run completes) for …"*, and neither tier can be named without the other. The gating flags are asserted over `BUDGET_RUNNER_COMMANDS`: every member declares both, nothing outside it declares `--allow-partial`, and the flag is spelled once in `cli.py`. *Original note:* **open.** `ASK_MAX_ANALYSIS_SECONDS=1 migrate-check . --ci` → **rc=2**, `INVALID_USAGE: No such option: --ci (Possible options: --copy, --dir)`; without the flag, 20 770 ms against a 1 s budget, rc=0, no `_partial`. `spring-audit`, `risk` and `audit-report` all return 75 now, so the pattern is proven and shared — and the root help enumerates **eight** commands bounded by the variable, of which `migrate-check` and `migrate-apply` still cannot fail a pipeline over an incomplete inventory. | The flags come from the shared factory (`AUD-596-X01` already built it) applied over the canonical tuple, not command by command — three facts, three decorators is how this row was born. Parameterise the assertion over `analysis_budget.BUDGETED_ANALYSIS_COMMANDS`, never over a list kept by hand. |
331
477
  | `AUD-597-D02` / `ASK-DOC-001` | **P3 — one call site, and it is confirmed live at HEAD** | **closed 2026-08-22 (`ecb1bb7`).** Two rejections are two messages. The one that says *"pass both halves"* offers the ref form whole; the one that says *"not both"* names one, and the one it names is the one the caller is closer to having typed — both refs are present by construction at that branch, so the checkouts decide: two of them is a complete checkout invocation with refs added, one is a ref invocation with a stray positional. Each form is rendered from the command's own usage line, so a command added to `_two_states` later inherits both and the two cannot drift. `reproduced_on`: `ask delta <base> <head> --base-ref <ref> --head-ref <ref> [--repo <path>]` → `ask delta <base> <head>` for the duplicate-checkout form, `ask delta --base-ref <ref> --head-ref <ref> [--repo <path>]` for the stray-positional one. The property is asserted as the property rather than as three cases: no hint this call site can produce carries a token from both columns. *Original note:* **open.** `delta` rejects refs and checkouts together with *"Refs and checkouts name the same two states two different ways. Pass two checkouts, or two refs with `--repo`, not both."* and then prints, verbatim at HEAD: `"hint": "ask delta <base> <head> --base-ref <ref> --head-ref <ref> [--repo <path>]"` — **the exact combination it just refused**, concatenated. `AUD-596-D12` face (1) was closed by `e132458` and this face survived it: the no-argument hint was fixed, the both-arguments hint was not. `contract-diff` shares the call site. | Two rejections are two messages. The hint after *"not both"* names **one** of the two forms, and the one it names is the one the caller is closer to having typed. The regression assertion goes over both error paths of both commands, and the property is checkable in one line: **no hint may contain the token pair the message just declared mutually exclusive**. |
332
- | `AUD-597-D03` / `N-06` | **P3 — a confident empty string in the axis this product just built to refuse them** | **closed 2026-08-22 (`e132458`).** Every item of the new `route_surface_changed` axis publishes `method`, `path`, `handler_symbol`, `access_policy` and **`"source_file": ""`** — an empty string, not an absence. Confirmed in source at `verify_edit.py:738`: `str(getattr(ep, "source_file", "") or "")`. In the same release `endpoints[]` publishes `source_file` and `declaration_line` for every route (`AUD-596-D06`, closed), so the anchor exists and the axis that declares `cir.endpoints` as its authority does not read it. | Fill it from `cir.endpoints`, which the axis already names as its authority; where the model genuinely has no anchor, **omit the key** or publish `null` with a basis. `""` is this ledger's own *"never a confident falsehood"* rule broken in the newest code in the product. |
478
+ | `AUD-597-D03` / `N-06` | **P3 — a confident empty string in the axis this product just built to refuse them** | **reopened 2026-08-22 — the field reads `route_surface_changed.items[0].source_file` as the empty string in `583c07c`, with no `declaration_line` beside it: the confident empty string this axis was built to refuse. A witness of `AUD-598-X01`.** *Previous verdict:* **closed 2026-08-22 (`e132458`).** Every item of the new `route_surface_changed` axis publishes `method`, `path`, `handler_symbol`, `access_policy` and **`"source_file": ""`** — an empty string, not an absence. Confirmed in source at `verify_edit.py:738`: `str(getattr(ep, "source_file", "") or "")`. In the same release `endpoints[]` publishes `source_file` and `declaration_line` for every route (`AUD-596-D06`, closed), so the anchor exists and the axis that declares `cir.endpoints` as its authority does not read it. | Fill it from `cir.endpoints`, which the axis already names as its authority; where the model genuinely has no anchor, **omit the key** or publish `null` with a basis. `""` is this ledger's own *"never a confident falsehood"* rule broken in the newest code in the product. |
333
479
  | `AUD-597-D04` / `N-05` | **P3 — a declaration gap, in the block that exists to be honest about the clock** | **closed 2026-08-22 (`e51c087`).** `risk` times fine-grained walks inside its two coarse phases, so a run cut before any walk starts times nothing at all. A phase in `phases_completed` has a duration by construction: `PhaseTimings.close_phase(name)` records it at the boundary where the phase ends, and records only the part the walks did not already claim, so `measured_ms` can neither double-count nor exceed the wall clock. `audit` closes whether or not a checkpoint is being written; `compose` closes at every checkpoint and accumulates, since it is one phase. `reproduced_on`: `ASK_MAX_ANALYSIS_SECONDS=5 risk ./spring-security` — **`phases {}` / measured 0 / unaccounted 100.0 % → `phases {audit: 9019.88, compose: 16.34}` / measured 9036.22 / unaccounted 0.0 %**, and a complete run keeps its walk-level split (365.26 measured against 365.29 wall). *Original note:* **open.** `ASK_MAX_ANALYSIS_SECONDS=5 risk ./spring-security` publishes `_partial.phases_completed: ["audit"]` beside `timings.phases: {}`, `measured_ms: 0`, `unaccounted_pct: 100.0`. `wall_ms` is honest now (`AUD-596-B10`, closed and verified from outside: 40 798 ms external against `wall_ms` 39 929), so what remains is that a phase declared complete is attributed nowhere, which makes 100 % unaccounted **structural in every partial** rather than a measurement. Round B credits the new `how_to_read` naming the five walks a cut run skipped. | A phase in `phases_completed` has a duration by construction — record it where the phase ends, not where the run does. `unaccounted_pct` on a partial then means what it means on a full run, which is the only reason to publish it. |
334
480
  | `AUD-597-D05` / `N-04`, fourth appearance of `C4-27` | **P3 — the fix exists in the sibling command and did not travel** | **closed 2026-08-22 (`9cccc15`), and there were two mechanisms, wrong in opposite directions.** The suggestion in the *message* is **Click's**, computed inside `NoSuchOption` with its own cutoff, so this product's guard never got to decide; the message carries the refusal now and the hint carries the candidate, asserted over every command that declares a long option. And the guard measured `sum(a != b for a, b in zip(...)) + abs(len diff)` — a Hamming count, not an edit distance — so a single deletion shifted every later character and `--compct` against `--compact` scored 3: the one-character typo the guard exists for was being thrown away. Levenshtein now. The semantic guard is stated as the property that makes a suggestion plausible: at most two edits **and** at most half the name — a correction that rewrites half of a short word has proposed a different one. `reproduced_on`: `plan … --path .` → no correction and *"`ask plan` takes: --copy, --depth, --format, --output, --progress"*; `--compct` → `--compact`; `--lmit` → `--limit`. `scope: "root"` stays attached to the misplaced-global-flag hint alone. *Original note:* **open.** `plan OwnerController ./spring-petclinic --path .` → `INVALID_USAGE: No such option: --path (Possible options: --depth)`, under the generic hint *"Check the command syntax, option name, and argument shape, then try again"*. Edit distance 3 over 6 characters and no semantic relation: the suggester is proposing a flag that answers a different question. `explain-endpoint --method` received the hint carrying the command's real signature in the same release, so the mechanism shipped and reached one of the two. | Suggest from the command's own declared parameters with a **semantic** guard, not from edit distance alone — a suggestion that is not a plausible answer to what the caller asked is worse than no suggestion. Fourth appearance; close it over the population of commands that emit `Possible options`, not over `plan`. |
335
481
  | `AUD-597-D06` / `ASK-CLI-001` | **P3 — the only finding of its round with no row at all, in either direction** | **closed 2026-08-22 (`178aa6d`).** All three spellings are accepted everywhere and nothing acquires three new options: an alias a command does not declare is read before the parser as the repository it names and handed to the positional slot that was going to receive it. No payload changes, and the root help declares the grammar. Two guards, both the row's own counterexamples: an alias a command *does* declare keeps its meaning (`delta --repo`), and an alias never fills a slot already named (`plan Thing . --path .` is a duplicate to refuse). **One defect found while building it**: option arity was read from `_OPTIONS_WITH_VALUE`, a partial hand list, so `--limit 2`'s value counted as a supplied positional and made an empty slot look full — `risk --dir <repo> --limit 2` was refused while `risk --dir <repo>` worked. Arity comes off the command's own parameters now. Asserted over the registry population: 40+ commands × 3 aliases. *Original note:* **open.** Three conventions name one concept across commands of one tier: `--path` (`compare`, `fix-bug`), a positional (`delta`, `contract-diff`), `--repo` (the ref forms). `plan … --path .` and `fix-bug … --path .` both exit 2. The messages improved to `INVALID_USAGE` with `Possible options`; the grammar did not. `grep ASK-CLI-001` over the shipped ledger returns nothing — it is neither open nor closed, it was never read, which is its own process defect. | Accept all three spellings as aliases everywhere (backward compatible, no payload changes) and declare the canonical one in `--help`. The assertion is derivable from the registry: **every command that takes a repository declares the same option name for it**. |
@@ -435,7 +581,7 @@ round A. `posture` is the exception, and it is `AUD-596-A01`.
435
581
  | `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. |
436
582
  | `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). |
437
583
  | `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. |
438
- | `AUD-596-D01` / `B-13` + `B-14` | **P3 — highest leverage of the P3s** | **closed 2026-08-22 (`f6ae10e`).** 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. |
584
+ | `AUD-596-D01` / `B-13` + `B-14` | **P3 — highest leverage of the P3s** | **reopened 2026-08-22 — two witnesses in `583c07c`: `validation` and `data-exposure` publish no run identity at any depth of a recursive search and still carry no `schema_version`, and one `md5` is identical for two different repositories for the third consecutive round. `risk` and `onboard` do gain identity, so the closure is partial rather than absent. A witness of `AUD-598-X01`.** *Previous verdict:* **closed 2026-08-22 (`f6ae10e`).** 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. |
439
585
  | `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. |
440
586
  | `AUD-596-D03` / `ASK-AGT-001` | **P3** | **closed 2026-08-22 (`8c46967`).** `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. |
441
587
  | `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. |
@@ -444,7 +590,7 @@ round A. `posture` is the exception, and it is `AUD-596-A01`.
444
590
  | `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. |
445
591
  | `AUD-596-D08` / `B-22`, class residual of `AUD-591-A06` | **P3** | **closed 2026-08-22 (`b396c6a`).** `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. |
446
592
  | `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`. |
447
- | `AUD-596-D10` / `B-29` | **P3 — scale** | **closed 2026-08-22 (`dabcc95`, `493e796`).** 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. |
593
+ | `AUD-596-D10` / `B-29` | **P3 — scale** | **reopened 2026-08-22 — five complete payloads searched in `583c07c` for a memory key, zero hits; `timings` still carries seven keys and none of them is memory. A witness of `AUD-598-X01`.** *Previous verdict:* **closed 2026-08-22 (`dabcc95`, `493e796`).** 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. |
448
594
  | `AUD-596-D11` / `ASK-UX-005` | **P3 — exit-code contract** | **closed 2026-08-22 (`f4b4f77`).** `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`. |
449
595
  | `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. |
450
596
  | `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`. |
@@ -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.22, `validation` enters through that shared CIR and `data-exposure` reuses one semantic
49
+ 5.8.23, `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
 
@@ -109,8 +109,31 @@ packs compose them into consumable results:
109
109
  ask pack list
110
110
  ask pack assessment . --profile prod --output-dir .ask/packs/assessment
111
111
  ask pack gate . --since origin/main --compact --output-dir .ask/packs/gate
112
+ ask pack assessment . --format markdown -o assessment.md
112
113
  ```
113
114
 
115
+ To include deployment evidence, pass the environment artefacts that describe
116
+ the deployment being assessed rather than an inferred profile:
117
+
118
+ ```bash
119
+ ask pack assessment . --deployment k8s/deployment-prod.yaml --output-dir .ask/packs/assessment
120
+ ```
121
+
122
+ The gate can emit native annotations for CI logs:
123
+
124
+ ```bash
125
+ ask pack gate . --since origin/main --format github-actions
126
+ ask pack gate . --since origin/main --format gitlab
127
+ ```
128
+
129
+ The pack JSON preserves `manifest`, identity, fingerprints, evidence and each
130
+ component's partial status. `PASS`, `BLOCK` and `UNVERIFIED` are data
131
+ decisions; the process exit code is the pipeline signal. When `--output-dir` is
132
+ used without `--output`, the aggregate is written there as `pack.json` and each
133
+ component remains a separate JSON artifact. An explicit `--output FILE` takes
134
+ precedence. See the
135
+ [Product Route contract](PRODUCT-ROUTE.md) for the boundaries.
136
+
114
137
  See [Product Route](PRODUCT-ROUTE.md) for the evidence contract and scope.
115
138
 
116
139
  A distinct set of commands exists to feed **AI coding agents** rather than to be read by a
@@ -155,7 +178,7 @@ pipx install sourcecode # isolated install, no venv needed
155
178
 
156
179
  # Verify
157
180
  ask version
158
- # ask 5.8.22
181
+ # ask 5.8.23
159
182
  ```
160
183
 
161
184
  Requires Python 3.9+.
@@ -200,7 +223,7 @@ estimate scaled by file count would be wrong in the direction that costs you a s
200
223
  and an anchor printed without its release goes on recommending a nightly job for a command
201
224
  that has come to finish in seconds (C3-97).
202
225
 
203
- 41 commands and six command groups exist. Four of them carry most of the measured
226
+ 41 commands and seven command groups exist. Four of them carry most of the measured
204
227
  value in field use, and they are the ones to learn first:
205
228
 
206
229
  | Start with | Because |
sourcecode/build_stamp.py CHANGED
@@ -7,6 +7,7 @@ open for five reports.
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
+ import re
10
11
  import subprocess
11
12
  from pathlib import Path
12
13
 
@@ -20,8 +21,53 @@ STAMP_PATH = Path("src") / "sourcecode" / "_build_commit.py"
20
21
  _TEMPLATE = '''"""Generated at build time by hatch_build.py. Do not edit (AUD-592-D04)."""
21
22
 
22
23
  BUILD_COMMIT = {commit!r}
24
+ CLOSURE_PROVENANCE = {closures!r}
23
25
  '''
24
26
 
27
+ _CLOSED_WITH_COMMIT = re.compile(
28
+ r"closed[^:|()]{0,100}\(`?([0-9a-f]{7,40})`?\)", re.IGNORECASE
29
+ )
30
+ _CLOSED_BY_COMMIT = re.compile(
31
+ r"closed\s+by\s+`([0-9a-f]{7,40})`", re.IGNORECASE
32
+ )
33
+
34
+
35
+ def closed_row_commits(ledger: str) -> dict[str, str]:
36
+ """Extract explicit fix commits from currently closed table rows."""
37
+ rows: dict[str, str] = {}
38
+ for line in ledger.splitlines():
39
+ if not line.startswith("|"):
40
+ continue
41
+ cells = line.split("|")
42
+ if len(cells) < 5:
43
+ continue
44
+ row_id, status = cells[1].strip(), cells[3].strip()
45
+ if not row_id or "closed" not in status.lower() or "reopened" in status.lower():
46
+ continue
47
+ match = _CLOSED_WITH_COMMIT.search(status) or _CLOSED_BY_COMMIT.search(status)
48
+ if match:
49
+ rows[row_id] = match.group(1)
50
+ return rows
51
+
52
+
53
+ def closure_provenance(root: "str | Path", commit: str, ledger: str) -> dict[str, dict[str, str]]:
54
+ """Return whether each ledger closure is contained in *commit*."""
55
+ closures = closed_row_commits(ledger)
56
+ if not closures or commit == UNRECORDED:
57
+ return {row: {"commit": fix, "status": "unknown"}
58
+ for row, fix in sorted(closures.items())}
59
+ result: dict[str, dict[str, str]] = {}
60
+ for row, fix in sorted(closures.items()):
61
+ try:
62
+ present = subprocess.run(
63
+ ["git", "-C", str(root), "merge-base", "--is-ancestor", fix, commit],
64
+ capture_output=True, check=False, timeout=5,
65
+ ).returncode == 0
66
+ except (OSError, subprocess.SubprocessError):
67
+ present = False
68
+ result[row] = {"commit": fix, "status": "present" if present else "absent"}
69
+ return result
70
+
25
71
 
26
72
  def head_commit(root: "str | Path") -> str:
27
73
  """The HEAD of the checkout at *root*, or `unrecorded`. Never raises.
@@ -40,10 +86,20 @@ def head_commit(root: "str | Path") -> str:
40
86
 
41
87
 
42
88
  def write_stamp(root: "str | Path", commit: "str | None" = None) -> Path:
43
- """Write the generated stamp module under *root* and return its path."""
44
- target = Path(root) / STAMP_PATH
89
+ """Write the generated stamp and closure provenance under *root*."""
90
+ root = Path(root)
91
+ build_commit = commit or head_commit(root)
92
+ ledger_path = root / "docs" / "DEFECT-LEDGER.md"
93
+ ledger = ledger_path.read_text(encoding="utf-8") if ledger_path.is_file() else ""
94
+ closures = closure_provenance(root, build_commit, ledger)
95
+ absent = [row for row, item in closures.items() if item["status"] == "absent"]
96
+ if absent:
97
+ raise RuntimeError(
98
+ "closed ledger rows cite commits outside this build: " + ", ".join(absent)
99
+ )
100
+ target = root / STAMP_PATH
45
101
  target.parent.mkdir(parents=True, exist_ok=True)
46
102
  target.write_text(
47
- _TEMPLATE.format(commit=commit or head_commit(root)), encoding="utf-8"
103
+ _TEMPLATE.format(commit=build_commit, closures=closures), encoding="utf-8"
48
104
  )
49
105
  return target
sourcecode/ci_output.py CHANGED
@@ -71,3 +71,51 @@ def render_ci_result(
71
71
  annotation_line = f"::{annotation} title=ASK {command}::{_escape(message)}"
72
72
  summary = f"ASK_CI command={_escape(command)} status={_escape(normalized_status)} severity={_escape(level)} findings={int(count)}"
73
73
  return f"{annotation_line}\n{summary}"
74
+
75
+
76
+ def _pack_component_lines(data: dict) -> list[tuple[str, str, int]]:
77
+ """Return the bounded component facts needed by CI log renderers."""
78
+ manifest = data.get("manifest") or {}
79
+ rows: list[tuple[str, str, int]] = []
80
+ for component in manifest.get("components") or []:
81
+ if not isinstance(component, dict):
82
+ continue
83
+ rows.append((
84
+ str(component.get("command") or "unknown"),
85
+ str(component.get("status") or "unknown").upper(),
86
+ int(component.get("exit_code") or 0),
87
+ ))
88
+ return rows
89
+
90
+
91
+ def render_pack_gate_github_actions(data: dict) -> str:
92
+ """Render a gate pack as GitHub Actions annotations and a stable summary."""
93
+ verdict = str(data.get("verdict") or "UNVERIFIED").upper()
94
+ lines: list[str] = []
95
+ for command, status, exit_code in _pack_component_lines(data):
96
+ if status in {"BLOCK", "FAILED", "FINDINGS"}:
97
+ level = "error"
98
+ elif status in {"UNVERIFIED", "PARTIAL", "UNKNOWN"} or exit_code == 75:
99
+ level = "warning"
100
+ else:
101
+ level = "notice"
102
+ message = f"component={command}; status={status}; exit_code={exit_code}"
103
+ lines.append(f"::{level} title=ASK pack gate::{_escape(message)}")
104
+ lines.append(f"ASK_GATE verdict={_escape(verdict)} exit_code={int(data.get('exit_code') or 0)}")
105
+ return "\n".join(lines)
106
+
107
+
108
+ def render_pack_gate_gitlab(data: dict) -> str:
109
+ """Render a gate pack as portable GitLab job output.
110
+
111
+ GitLab has no equivalent to GitHub's workflow-command annotations. The
112
+ exit code remains the gate, while these bounded lines make the result
113
+ searchable and groupable in GitLab job logs.
114
+ """
115
+ verdict = str(data.get("verdict") or "UNVERIFIED").upper()
116
+ lines = ["section_start:0:ask_pack_gate[collapsed=true]ASK pack gate"]
117
+ for command, status, exit_code in _pack_component_lines(data):
118
+ lines.append(f"ASK_GATE_COMPONENT command={command} status={status} exit_code={exit_code}")
119
+ lines.append(f"ASK_GATE verdict={verdict} exit_code={int(data.get('exit_code') or 0)}")
120
+ lines.append("section_end:0:ask_pack_gate")
121
+ return "\n".join(lines)