sourcecode 5.8.16__py3-none-any.whl → 5.8.17__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 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.16"
7
+ __version__ = "5.8.17"
@@ -16,12 +16,38 @@ 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 audit of release `5.8.15`:** score **90/100**, coverage **54/62
20
- invocables (87%)**, byte-identical CIR across six versions, and no longitudinal functional
21
- regression. A new P1 affects dirty-tree `verify-edit`; BUG-1 is reproduced on the
22
- Windows/NTFS audit host. The historical `risk` P0 remains open because nine successful
23
- runs do not disprove two earlier exit-0/no-output failures. Only the sixth-pass section
24
- below declares present state; older audit rounds remain for traceability.
19
+ **Latest attached audits — release `5.8.16`, 2026-08-21, two independent rounds.**
20
+ Round A (`saint-server`, 3 342 Java files, 17 commands, 25 invocations) scores **8.21/10**
21
+ with **zero false statements about the repository** and two contract defects. Round B (a
22
+ 16-repository bank, 49 to 24 073 Java files, **240 invocations, 0 crashes, 0 tracebacks**)
23
+ confirms **30 of 30 claims about source** — 19 exact to file and line, two of them where
24
+ ASK is right and a naive `grep` is wrong — and **refutes 8 of 8 claims ASK makes about its
25
+ own execution**. It scores **6.7/10** because one unauthorised write triggers its rubric's
26
+ elimination rule; its own counterfactual without that rule is 7.6, and it closes **8 of 18**
27
+ previously open defects by correction with **zero regressions among the inherited ones**.
28
+
29
+ Both rounds agree on where the product now fails, and it is not the Java/Spring analysis:
30
+ it is the contract an agent consumes programmatically — the write guard, repository
31
+ identity, list units, flag scope, and published budgets. The seventh-pass section below is
32
+ the only current queue; older rounds remain for traceability.
33
+
34
+ **Mechanisms confirmed in source during intake** (not taken on the reports' word):
35
+ `verify --init` writes with no `readonly.guard` (`cli.py:10706-10708`) while five sibling
36
+ writers guard; `spring-audit` derives `repo_id` from the CIR content hash
37
+ (`spring_security_audit.py:1416`, `spring_tx_analyzer.py:1081`) while every other surface
38
+ derives it from the resolved path (`cache.repo_id`); `_resolve_target`'s last resort is an
39
+ unbounded case-insensitive substring match (`repository_ir.py:10205-10214`) under a constant
40
+ `matched_fqns_basis` string (`repository_ir.py:9788`); no payload publishes
41
+ `direct_callers_unit`; and the two `--compact` token figures differ in the same help page
42
+ (`cli.py:587` versus `cli.py:3613`).
43
+
44
+ **Two reported mechanisms are corrected here, and neither correction dismisses the defect.**
45
+ `--rule`/`--band` are not unwired: they are declared `--table`-only render filters and are
46
+ applied inside the table path alone, so the defect is that they are accepted and silently
47
+ ignored outside it and that `--rule` validates nothing (`--band` does, in `risk`).
48
+ `validation --path-prefix` is *worse* than reported: it filters `endpoints` and `gaps`
49
+ (`cli.py:8172-8180`) and leaves `summary` at its pre-filter values, so the answer contradicts
50
+ itself rather than merely under-declaring.
25
51
 
26
52
  **Release status:** entries marked “pending `5.8.14`” in their historical wording are
27
53
  shipped in `5.8.14`; the correction battery fixes listed above are included in `5.8.15`.
@@ -37,9 +63,80 @@ cache and schema authorities (`1d00ba2`); endpoint census output distinguishes e
37
63
  test modules from retained test-fixture routes (`f65336d`, `014dec7`, `1c6cd0b`);
38
64
  and compact posture output retains counts while omitting unresolved bodies (`dad5dad`).
39
65
 
40
- The sixth audit supersedes the previous queue where it has stronger evidence: BUG-3 is
41
- closed in stable-cache regime, BUG-1 is reproduced on the audit host, and BUG-4a is
42
- partial because `onboard` still has no machine-readable population block.
66
+ **Historical, sixth pass:** it superseded the queue before it where its evidence was
67
+ stronger — BUG-3 closed in stable-cache regime, BUG-1 reproduced on the audit host, BUG-4a
68
+ partial because `onboard` still had no machine-readable population block.
69
+
70
+ **What the seventh pass does not touch.** Neither round exercised `BUG-2` (`risk` exiting 0
71
+ with neither parseable stdout nor its requested artifact) or `BUG-6`
72
+ (`verify-edit.security_delta` missing access opening): both rows keep their status and their
73
+ queue position. `BUG-1` and `BUG-5` gain witnesses on public OSS repositories, recorded
74
+ under their own rows rather than as new ones.
75
+
76
+ ### Seventh Audit Pass: `5.8.16` / 16-repository bank + `saint-server` / 2026-08-21
77
+
78
+ Ordered as the correction queue, regressions and permissive-direction failures first. Each
79
+ row carries the canonical ledger ID; the reporters' own IDs are given for traceability
80
+ (`A-*` and `B-*`/`N-*` from the bank round, `F-*`/`B-0*`/`R-01` from the `saint-server`
81
+ round). Rows already held elsewhere in this ledger are listed at the end as mappings, not
82
+ as new defects.
83
+
84
+ | ID | Severity | Current status | Required direction |
85
+ |---|---|---|---|
86
+ | `AUD-591-A01` / `A-1` | P0 | **closed 5.8.17**: Guarded at the emitter. The sweep the row asked for found **two more** unguarded paths a per-module audit could not see: `migrate-recipe --write` (writes `<repo>/rewrite.yml` from the command body) and `verify-edit --install-hook`, whose guard sat on the *hooks directory* — and `guard()` never refuses a path that already exists, so `git init` made that guard unfireable on every repository; it is `guard_mutation` on the hook file now. The proposed criterion `git status --porcelain == ""` **cannot see this defect**: the first thing written into `.ask/` is a `.gitignore` containing `*`, so every artefact after it is invisible to git. The sweep compares a filesystem footprint of the whole tree including `.git/hooks/`, both halves, over all ten write combinations, `--dir`/`--history-dir` pointed inside the repository. Previously — **open, reproduced in source**: `verify <repo> --init` writes `<repo>/.ask/contracts.yml` and exits 0 under `ASK_READONLY=1` and under `--no-write`. Six of seven write paths guard; this one calls `target.write_text` directly (`cli.py:10706-10708`). Second instance in two releases of the class `AUD-513-N01` closed. | Put the guard on the emitter, not the call site: `written_to != null` and an active guard are mutually exclusive states. Then sweep — for every command and flag combination that can produce a non-null `written_to`, a subprocess test with a clean git tree asserting `git status --porcelain == ""`. |
87
+ | `AUD-590-R01` / `R-01` | P0 (triage) | **closed 5.8.17**: Triaged and both halves answered. `no_security_signal` and `undocumented` are **one number twice** — the same variable assigned to both keys, in both endpoint authorities — and the payload now says the second is an alias and never an independent measure. `total` is decomposed by `route_census`: `distinct_routes` (distinct `(effective_path, method)`, every verb in `methods`) and `expansion_rows`, from one helper both authorities call. Measured on shenyu: 359 mappings, 235 distinct routes, 6 expansion rows. The triage also found the census **under**-counting: `_parse_route_path` returned the first literal only, so `@GetMapping({"/a","/b"})` published `/a` and dropped `/b` while the class-level array had always expanded. shenyu 359 → 365 published routes. Previously — **open, unexplained movement**: the endpoint census on `saint-server` moved 2 635 → 3 742 (+42%) between rounds at the same HEAD, published as `total_unit: "handler mappings declared … the whole route population"`. The reporter did not record the earlier version, so this is not yet attributable to `5.8.16`. | Decide between annotation expansion counted as population (multi-path or multi-verb mappings, class-level mappings counted beside method-level) and a real widening of detection. Compare `total` against distinct `(effective_path, method)` on the same tree. If it is expansion, it contaminates `data-exposure` and `pr-impact` gating; if it is detection, it needs a change note. Also verify that `no_security_signal` and `undocumented` — both 718 — are computed separately and are not one number under two names. |
88
+ | `AUD-591-A03` / `A-3` | P1 | **closed 5.8.17**: `partial` publishes what it is: a basis naming the substring match, `risk_score`/`risk_level` null, `risk_reason` and `candidates_total`, and prose that no longer opens with a verdict the payload withheld. The `not_found` half was the same defect with the opposite sign — `risk_score: 0.0` over a symbol never measured — and is null with its basis on both `not_found` and `ambiguous_path`. Resolution policy published in `impact --help`. Release assertion over every `*_basis`, derived from source: each must explain a field some payload publishes, a basis describing a null must say null, and the two paths producing a null score must not share one string. **The report is wrong on one point:** `rename-class --from` does not share this resolver — it requires a file declaring the class and refuses `--from O` outright, so the one path that rewrites source was never reachable from the substring match. Previously — **open, reproduced in source**: `_resolve_target`'s last resort is an unbounded case-insensitive substring match (`repository_ir.py:10205-10214`), so `ask impact O` answers over 129 symbols with `exit 0`, `risk_level: critical` and `matched_fqns_basis: "symbols resolved directly from the requested target"` — a constant string (`repository_ir.py:9788`) emitted on a path that resolved nothing. `PetControler` and `OwnerRepositor` are the same class of input and get opposite verdicts. Same class as `E-17` and as `B-5`, both closed. | Either `not_found` with the `candidates` list that already works, or a basis that names the substring match with `risk_level: null` and a `risk_reason`. Publish the resolution policy in `impact --help` with its minimum length and specificity. Add the release assertion that every `*_basis` describes the path actually taken. The shared resolver reaches `impact-chain`, `explain`, `plan`, `compare`, `fix-bug` and `rename-class --from`; `rename-class --from O` must be exercised on a disposable fixture before anything else. |
89
+ | `AUD-591-A10` / `A-10` | P1 | **closed 5.8.17**: Route-derived summary counts (`endpoints_with_body`, `validated_fields`, `gaps`, `endpoints_with_declared_constraints`, `declared_constraint_routes`) move with the filtered lists; `_filter` carries `total_before_filter` and `summary_before_filter`. `source_derived_routes`, `body_endpoints_in_code` and the validator catalogue stay whole and the block says why — recomputing them from the filtered list would replace an unscoped number with a wrong one. Both commands reject a prefix that is not a route path, echo the value that arrived, and name the MSYS rewrite with its two workarounds. The prefix matches `effective_path` as well as `path`, so a deployment prefix no longer empties every selection. Previously — **open, worse than reported**: `validation --path-prefix` filters `endpoints` and `gaps` and leaves `summary` at its whole-repository values (`cli.py:8172-8180`), and publishes no `_filter` block at all. `endpoints --path-prefix` filters and echoes correctly but validates nothing, so a Git Bash/MSYS path rewrite turns `/owners` into `C:/Program Files/Git/owners` and the answer is `total: 0` with `exit 0` — a silent false negative on a core-tier command in the audit host's own shell. | Reject a prefix that is not a route path, echoing the value received and naming the MSYS rewrite only when the value looks like an absolute Windows path. Recompute or scope `summary` under a filter, and reuse the `endpoints._filter` block (`path_prefix`, `total_before_filter`, `note`) rather than reimplementing it. The regression must run in Git Bash: a Linux-only test cannot see this. |
90
+ | `AUD-590-B01` / `B-01`, `F-01` | P1 | **closed 5.8.17**: One `repo_id` per tree, equal to the cache directory key, with `repo_id_basis`. The CIR fingerprint keeps its own name (`content_id`). `TransactionBoundaryIndex.repo_id` was a third site holding the content hash and is `content_id` too. Regression asserts cardinality 1 across the emitters **and** equality with the cache key — not merely that they agree with each other, but that they agree with the thing they name. Previously — **open, mechanism identified in source**: one field name, two identity functions. `spring-audit` sets `repo_id` from the CIR content hash (`spring_security_audit.py:1416`, `spring_tx_analyzer.py:1081`), while `migrate-check`, the cache directories and every other surface use the path hash (`cache.repo_id`). Same tree, same HEAD, same argv: `0123aa26a26297da` versus `7fced5c877cacfc5`. | One identity per tree, equal to the key of `~/.sourcecode/cache/<hash>` and `~/.sourcecode/context-cache/<hash>`; if a content hash is wanted it needs its own field name. Regression: invoke every command that emits `repo_id` on one path and assert cardinality 1. `audit-report` and `regress` correlate on this field and drop evidence silently when it diverges. |
91
+ | `AUD-591-A02` / `A-2`, `A-4` | P1 | **closed 5.8.17**: One `_apply_selection` helper on both sides of the format branch in `spring-audit`, `risk`, `enrich` and `migrate-check`, publishing `_filter` with `total_before_filter`/`matched`/`shown`; no selection, no block. `--rule` is rejected against the catalogue the analysis iterates (`rule_catalog.ids()`, and a new `migration_rule_ids()` read from `_ALL_RULES`), before the analysis runs. `enrich` is the declared exception: its rule ids come from the scanner's SARIF, so there is nothing to validate against and its help says so. One parametric battery, three commands × valid-with-matches / valid-without / invalid. Previously — **open, reported mechanism corrected**: `--rule` and `--band` are declared `--table`-only render filters and are applied only inside the table path (`cli.py:10992`, `11204`, `12584`, `10343`). In JSON they are accepted, ignored and never echoed, so `spring-audit --rule SEC-008`, `--rule NOPE-999` and no flag produce byte-identical payloads with `total_findings: 86`. `--rule` validates nothing anywhere; `--band` does validate in `risk` (`cli.py:10932-10943`); `--min-band` applies to the payload while its sibling `--band` does not. | A selection flag either selects or refuses. Outside `--table`, apply it or reject it with `flag`/`value`/`valid_values` taken from the rule catalogue, and echo the selection in a `_filter` block with `total_before_filter`. One parametric test over the selection-flag catalogue, three cases each — valid with matches, valid without matches, invalid — rather than two patches. Barrido: `--rule` also exists on `risk` and `enrich`. |
92
+ | `AUD-590-B02` / `B-02`, `F-02` | P2 | **closed 5.8.17**: Both lists publish `direct_callers_unit`, and each unit names where the other command keeps the same figure. `impact-chain` gained `indirect_callers_unit`. `resolution` is **not** merged — the commands resolve different things — so each publishes `resolution_vocabulary` with its values and a note mapping them onto the other's. Assertion: `len(impact.direct_callers) == chain.metadata.direct_caller_count`, so the declared units account for the cardinality difference. Previously — **open**: `direct_callers` names caller *classes* in `impact` and caller *methods* in `impact-chain` — two core-tier commands, one field name, aggregable by a consumer, and no payload publishes `direct_callers_unit`. `impact` publishes `indirect_callers_unit` (`repository_ir.py:9842`) and `impact-chain` publishes `direct_caller_count_unit` for the count but not for the list. Adjacent: the same symbol resolves `exact` in one and `class_expanded` in the other. | Declare the unit on both lists, reconcile the `resolution` vocabulary across the two resolvers, and assert in one test that the declared units explain the cardinality difference. `impact.implementation_fqns: []` beside an explanation naming 23 interface-reached callers needs either a `_basis` or a fix. |
93
+ | `AUD-590-B04` / `B-04`, `F-04` | P2 | **closed 5.8.17**: The root analysis was the single row `_build_analysis_classes` skipped, so its budget fell to an unnamed default and `ASK_MAX_ANALYSIS_SECONDS` was invisible on it; keyed now under the name the envelope gives it (B14) and calling the same preamble emitter `spring-audit` uses. `--output` is created before the analysis with a `partial-analysis-v1` stub (`partial: true`, `status: running`, and a reason saying it is a trace and not an answer), replaced atomically on success, guarded by `--no-write` and best-effort throughout. Previously — **open**: the one command `cache model` classifies as *"not a foreground run here"* is the one that emits nothing while it runs. `ask . --agent -o file` was killed at 140 s having written zero bytes to stdout, stderr and the output file, while `spring-audit` prints a budget preamble before starting and `ASK_MAX_ANALYSIS_SECONDS` explicitly does not bound the root command. | Reuse the `spring-audit` preamble emitter for every command whose `cache model` classification is not-a-foreground-run — the classification is already a product fact. Flush it to stderr so a killed run leaves a trace, and create the `-o` file at start or write a `partial: true` stub on deadline. |
94
+ | `AUD-590-B03` / `B-03`, `F-03` | P2 | **closed 5.8.17**: One phrase from `output_budget`, the module that enforces the limit, converted by the `chars/4` model `token_estimate` declares — a ceiling, not a typical size. Help field names equal payload keys, checked against the payload plus the serializer's section registry. **The predicate question is answered and the answer is the feared one:** `security_surface` needs a custom annotation that *names a resource*, while `GATE-001..004` fire on any custom authorization mechanism, so a gate taking no resource argument produces audit findings and no section. The section carries a `basis` naming the wider predicate. Previously — **open**: `--compact` publishes two different budgets in one help page — `~2,500–4,000 tokens` (`cli.py:587`) and `typically 1000–3000 tokens` (`cli.py:3613`) — and measures 22 390 B ≈ 5 597 tokens under the product's own `UTF-8 bytes ÷ 4` estimator, over both. The same help promises `confidence` and `gaps`; the payload has `confidence_summary` and `analysis_gaps`, and no `repo_id` at all. `security_surface` is absent on a repository where `spring-audit` reports 368 security findings including the GATE-* family. | One figure, derived from the same estimator or expressed as a function of repository size, and help field names equal to payload keys. Check whether the predicate that emits `security_surface` in `--compact` is the one that fires the `GATE-*` rules; if they diverge, the agent channel is hiding security surface. |
95
+ | `AUD-591-A06` / `A-6`, `A-7` | P2 | **closed 5.8.17**: `build_repo_ir(...)` was an argument expression, evaluated to be passed into a function whose first act is a syntactic parse: the repository was analysed so a regex could reject a string. `parse_query` runs before the repository is touched — **50.3 s → 0.31 s measured** — and a syntactic failure builds no `near_matches`. `find_java_files` records its own truncation (thread-local, because `--jobs` runs walks concurrently) and `last_scan_truncation()` is read *at the call site*, beside the walk that produced the list: read anywhere else it is a previous walk's state, and `extract_java_endpoints` does its own uncapped walk — a test holds that its census declares nothing. Release ceiling: four malformed routes rejected under 10 s each. `compare` also publishes the tree its candidates came from. Previously — **open**: the error path costs more than the success path. `explain-endpoint ./repo` — a purely syntactic rejection, *"a route path starts with `/`"* — takes 50.3 s against 0.67 s for a valid route, on the 49-file repository, and returns `near_matches: []` after building them. `compare` without `--path` spends 62–111 s scanning the CWD, offers closest candidates from a *different* repository under it, declares `not_found` for a symbol that is in the analysed tree, and never publishes that the scan stopped at 25 000 directory entries — a truncation the root `--help` already reports. | Validate the shape of a typed positional before touching the repository, and do not build suggestions for a syntactic failure. Publish `scan_truncated` with its limit and unit wherever a `not_found`, a `candidates` list or a census is derived from a truncated scan; warn on stderr before `compare` scans a CWD holding several repositories. Release assertion: no `INVALID_INPUT` rejection costs more than a small fixed ceiling on the reference repository. |
96
+ | `AUD-591-A09` / `A-9` | P3 | **closed 5.8.17**: `exit_code` is what the process returns; the gate verdict keeps `gate_exit_code` with its basis. Asserted against the observed return code on both `--ci` and `--no-ci`. Previously — **open**: `verify --no-ci` publishes `exit_code: 2` in a payload while the process exits 0. A pipeline reading the field rather than `$?` inverts the decision `--no-ci` just took. | Make the field describe the process exit, or rename it `would_exit_code` and say that `--no-ci` suppresses it. Adjacent, not a defect: `--fail-on never` exiting 2 comes from the `unverified` axis, not the violations axis, and the payload names `--allow-unverified` correctly — only the help example's comment oversells it. |
97
+ | `AUD-591-A08` / `A-8` | P3 | **closed 5.8.17**: Rows corrected (`--history-dir`, `--capture-baseline`) and three writing commands added, `verify --init` among them. Every backticked flag in the inventory is resolved against that command's own click parameters, so the table cannot cite an option the parser rejects. Previously — **open, and the reason `AUD-591-A01` was not found earlier**: the root help's *"commands that modify files inside the repository"* table cites `verify --update-baseline` (the real flags are `--baseline` and `--capture-baseline`) and `migrate-check --history` (the real flag is `--history-dir`), and omits `verify --init`, which is the one that writes without a guard. An auditor probes what that inventory lists. | Correct both lines and add `--init`. Then generate the inventory from the same registry the write guard consults — while they are two hand-kept sources they will diverge again — and extend the existing backtick assertion (closed for payload `message`/`hint` under `N-7`) to the help text. |
98
+ | `AUD-591-A05` / `A-5` | P3 | **closed 5.8.17**: The probe answers per layer (`_cache_probe`); `Scope` carries `snapshot_warm` and `ris_warm`; the advisory names each with the state actually probed for it. `warm` survives as their OR, for the coarse question it answers. Finishes `N-3`'s parity criterion. Previously — **open, `N-3` half-corrected**: the advisory no longer collapses four cache layers into one boolean and honestly says context/parse are unknown before analysis — but it asserts *"snapshot/RIS present"* while `metadata.cache_layers` in the same run publishes `snapshot: "cold"`, and `cache freshness` reports `RIS HEAD: (none), STALE` while the payload calls RIS `warm`. Three authorities, one fact. | The advisory prints what the payload will publish or says `unknown before analysis` for every layer it cannot know, `snapshot` included. Parity test across stderr, `metadata.cache_layers` and `cache freshness` in a single invocation. |
99
+ | `AUD-591-Q01` / `Q-1` | Question | **answered 5.8.17**: Reachable, and only from the root analysis: `ask <repo>` and `ask cache warm` write `core-*.json.gz`; no other command does. All 240 field invocations were of other commands, so `cold` was correct on every one of them and said nothing about the cache. `Layer` gained `written_by`, `cache model` prints it, and a test runs `spring-audit` twice from a cleared cache and asserts the core files stay absent — the answer expires if the mechanism changes. Previously — **open, not classified as a defect**: `metadata.cache_layers.snapshot` was `"cold"` in all 240 invocations, on 16 repositories, including after `cache clear --all -y` followed by two runs of the same command. From outside, an unreachable state published as a layer cannot be distinguished from a broken cache write or from a layer only `cache warm` fills. | Answer it internally: if the state is reachable, `cache model` should say which command reaches it; if it is not, it should not be published as a layer. |
100
+
101
+ **Mapped, not new — inherited rows this pass re-witnesses.** `N-6` (cold `spring-audit`
102
+ on `openmrs-core` 8.86 s against the 5.8.7 anchor of 5.6 s, ×1.58, criterion ≤6.5 s) and
103
+ the Windows cost family belong to `BUG-1` / `AUD-511-R02` / `AUD-589-B02`. `D-09` (cost
104
+ anchors still stamped *"on 5.1.0"* in a 5.8.16 build, fourth witness) and `B-5` belong to
105
+ `BUG-5` / `AUD-588-B12` / `AUD-588-F03`. `N-5` (the advisory says *"136 Java files"* with
106
+ no population label while the progress line correctly says `82 [population=production_java_sources]`)
107
+ is the unclosed half of `AUD-588-B11`. `B-6` (budget overrun ×4.2 with `exit 0` on a
108
+ partial answer, and an `overrun_bound` whose basis measures this run rather than bounding
109
+ the next), `B-7` (`total_ms` covers 17.5–87% of wall clock, third consecutive version, and
110
+ its published summands exceed the total by 23% and 40%), `B-3` (`--agent` omits 15 blocks
111
+ `--compact` publishes, `mybatis` among them, at ×2.17 bytes and ×2.25 time, and
112
+ `sibling_view` exists only on one side and names no block) and `B-8` (`posture --compact`
113
+ caps `unresolved` at `limit: 0`, so `total: 3, shown: 0`) remain open exactly as recorded;
114
+ this pass adds witnesses, not rows.
115
+
116
+ **Closed by correction and re-verified from outside in this pass** — do not re-derive:
117
+ `N-1` (`rename-class` write guard, and it is the model for `AUD-591-A01`), `N-2` (posture counts
118
+ Spring IoC beans, verified against source on three repositories: struts 5, mall 155 of 161
119
+ countable, petclinic 12 exactly), `B-1` (`mybatis.mapper_interfaces` 76, matching the tree),
120
+ `B-4` (posture security population), `B-5` (`imports_found` renamed to `evidence`), `N-4`
121
+ (`summary_basis: "no descriptive section found in README"`, the literal acceptance text),
122
+ `N-7` (`impact` emits `candidates`), `N-8` (numeric bounds validated, and the only criterion
123
+ whose sweep was executed as a sweep — seven numeric flags), `N-9` (cache hints out of
124
+ `limitations`). `B-2`/`D-05` remains closed by refutation, re-verified independently: four
125
+ `<servlet-mapping>` elements in the struts `web.xml`, two of them inside the comment at
126
+ lines 143-153, and ASK reports 2.
127
+
128
+ ⚠ **`B-1` is not verifiable from outside.** Its criterion 3 required a negative fixture — an
129
+ XML whose namespace resolves to no interface must stay in `orphan_xml`. The field cannot
130
+ distinguish *"resolved correctly"* from *"closed by emptying the list"* without writing a
131
+ file into the audited tree. Verify it here, with a mapper XML naming a non-existent
132
+ interface under `@MapperScan`, before treating the row as closed.
133
+
134
+ **Process finding, and the highest-value item of this pass.** In four defect classes out of
135
+ four, the reported instance was closed and the class was not: `N-1` → `AUD-591-A01`, `B-5` →
136
+ `AUD-591-A03`, `N-7` → `AUD-591-A08`, `N-8` → `AUD-591-A02`/`AUD-591-A10`. The single exception is the one whose
137
+ acceptance criterion contained a sweep clause *and* was executed as a sweep (`N-8`, seven
138
+ numeric flags), and it did not recur. Every acceptance criterion carrying a sweep clause is
139
+ to be closed with a parametric test over the catalogue, not with the reported case.
43
140
 
44
141
  ### Sixth Audit Pass: `5.8.15` / `saint-server` / 2026-08-20
45
142
 
@@ -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.16, `validation` enters through that shared CIR and `data-exposure` reuses one semantic
49
+ 5.8.17, `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.16
145
+ # ask 5.8.17
146
146
  ```
147
147
 
148
148
  Requires Python 3.9+.
sourcecode/cache_model.py CHANGED
@@ -39,6 +39,11 @@ class Layer:
39
39
  location: str
40
40
  invalidated_by: str
41
41
  warmed: str # what `ask cache warm` does to this layer
42
+ #: Which commands populate it. Empty means every command that reads it also
43
+ #: fills it — the ordinary case. Named where it is not (AUD-591-Q01): a layer
44
+ #: only one command writes reads `cold` on every other command's payload, and
45
+ #: a reader with no way to know that reads it as a cache that is not working.
46
+ written_by: str = ""
42
47
 
43
48
 
44
49
  @dataclass(frozen=True)
@@ -191,6 +196,15 @@ LAYERS: tuple[Layer, ...] = (
191
196
  "presentation flags (--compact, --agent, --full, --format, …) for the view"
192
197
  ),
193
198
  warmed="built for the compact view (`--agent` also builds the agent view)",
199
+ written_by=(
200
+ "the root analysis alone — `ask <repo>` and `ask cache warm`. "
201
+ "AUD-591-Q01: `cache_layers.snapshot` read `cold` in all 240 field "
202
+ "invocations across 16 repositories, including after `cache clear "
203
+ "--all -y` plus two runs, because every one of those runs was of "
204
+ "another command. No other command writes `core-*.json.gz`, so on "
205
+ "any run but the root analysis this layer is `cold` by construction "
206
+ "and its being cold says nothing about the cache."
207
+ ),
194
208
  ),
195
209
  Layer(
196
210
  id="ris",
@@ -941,10 +955,13 @@ def as_dict(here: "Optional[Conditioning]" = None) -> dict:
941
955
  def render_markdown() -> str:
942
956
  """The model as the tables published in the user guide."""
943
957
  out: list[str] = []
944
- out.append("| Layer | What it stores | What invalidates it | `cache warm` |")
945
- out.append("|---|---|---|---|")
958
+ out.append("| Layer | What it stores | What invalidates it | `cache warm` | Written by |")
959
+ out.append("|---|---|---|---|---|")
946
960
  for lyr in LAYERS:
947
- out.append(f"| `{lyr.id}` | {lyr.stores} | {lyr.invalidated_by} | {lyr.warmed} |")
961
+ out.append(
962
+ f"| `{lyr.id}` | {lyr.stores} | {lyr.invalidated_by} | {lyr.warmed} | "
963
+ f"{lyr.written_by or 'every command that reads it'} |"
964
+ )
948
965
  out.append("")
949
966
  out.append(f"Measured on {REFERENCE_REPOSITORY}, each command in isolation.")
950
967
  out.append("")
@@ -1038,6 +1055,10 @@ def render_text(here: "Optional[Conditioning]" = None) -> str:
1038
1055
  lines.append(f" location {lyr.location}")
1039
1056
  lines.append(f" invalidated {lyr.invalidated_by}")
1040
1057
  lines.append(f" cache warm {lyr.warmed}")
1058
+ if lyr.written_by:
1059
+ # AUD-591-Q01: `cache model` is where a reader goes to find out why a
1060
+ # layer reads `cold`, so the answer belongs on the layer.
1061
+ lines.append(f" written by {lyr.written_by}")
1041
1062
  lines.append("")
1042
1063
  lines.append("What a warm gives each command")
1043
1064
  lines.append(f" Timings: {REFERENCE_REPOSITORY}, each command measured in isolation.")
@@ -695,13 +695,24 @@ def project_endpoint_surface(cir: CanonicalRepositoryIR) -> dict:
695
695
  1 for e in endpoints
696
696
  if e.get("security", {}).get("policy") == "none_detected"
697
697
  )
698
+ from sourcecode.repository_ir import route_census as _route_census
699
+
698
700
  return {
699
701
  "endpoints": endpoints,
700
702
  "total": len(endpoints),
703
+ # AUD-590-R01: the same decomposition the other endpoint authority
704
+ # publishes, from the same helper, so a census read through the CIR
705
+ # projection and one read through `extract_java_endpoints` cannot
706
+ # disagree about what `total` counts.
707
+ "route_census": _route_census(endpoints),
701
708
  "no_security_signal": no_security_signal,
702
709
  "security_model": cir.metadata.get("security_model", "unknown"),
703
710
  # Legacy field alias — same count, kept for backward compat
704
711
  "undocumented": no_security_signal,
712
+ "undocumented_note": (
713
+ "alias of `no_security_signal`, kept for compatibility — the same "
714
+ "count published under a second name, never an independent measure"
715
+ ),
705
716
  }
706
717
 
707
718