sourcecode 5.8.4__py3-none-any.whl → 5.8.6__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 +1 -1
- sourcecode/_docs/DEFECT-LEDGER.md +15 -9
- sourcecode/_docs/USER_GUIDE.md +58 -10
- sourcecode/answer_bounds.py +230 -0
- sourcecode/audit_recipe.py +289 -0
- sourcecode/breaking_changes.py +138 -13
- sourcecode/cache.py +33 -1
- sourcecode/cache_model.py +19 -1
- sourcecode/cli.py +334 -32
- sourcecode/envelope.py +15 -0
- sourcecode/format_contract.py +2 -0
- sourcecode/functional_routes.py +425 -0
- sourcecode/output_ceiling.py +267 -15
- sourcecode/parse_cache.py +147 -0
- sourcecode/perf.py +258 -0
- sourcecode/regress.py +264 -0
- sourcecode/release_info.py +1 -1
- sourcecode/repository_ir.py +208 -18
- sourcecode/risk.py +163 -32
- sourcecode/schema_producers.py +247 -0
- sourcecode/schema_registry.py +62 -2
- sourcecode/selftest.py +445 -0
- sourcecode/timeline.py +91 -15
- sourcecode/timeline_cache.py +174 -0
- {sourcecode-5.8.4.dist-info → sourcecode-5.8.6.dist-info}/METADATA +11 -9
- {sourcecode-5.8.4.dist-info → sourcecode-5.8.6.dist-info}/RECORD +30 -23
- {sourcecode-5.8.4.dist-info → sourcecode-5.8.6.dist-info}/WHEEL +0 -0
- {sourcecode-5.8.4.dist-info → sourcecode-5.8.6.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.8.4.dist-info → sourcecode-5.8.6.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.8.4.dist-info → sourcecode-5.8.6.dist-info}/licenses/NOTICE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -41,7 +41,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
41
41
|
| B15 | **A write that could not finish left its temporary inside the caller's repository.** The payload goes to `.<name>.tmp-<pid>-<ns>` beside the destination and is renamed onto it — atomic, and correct — but when the rename failed nothing removed the temporary. Field matrix of five bad destinations on Windows: **13,9 MB of untracked files inside the repository** (2 757 334 B per attempt plus a 153 481 B `.partial`), as `??` in `git status`. Three edges came with it: a discard sink (`nul`, `NUL`, `CON`, `/dev/null`) was reported as an I/O error for successfully throwing output away; the OS `strerror` was interpolated into our English message, so the same matrix produced two `es-ES` messages and one English; and `-o ./missing/out.json` created the tree in silence while the hint told the caller to check that the directory exists. | 5.6.1 audit, re-reported and widened in the 5.7.2 audits | **High** — it is what breaks the read-only premise the product is sold on, and the auditor grades it blocking for any pre-CI security review | **closed 5.8.1** — one authority, `sourcecode.output_sink`. The temporary is removed in a **`finally`**, so the guarantee does not depend on enumerating the exit paths correctly (which is how five escape routes were missed). A discard sink answers `written: false`, `sink: "null-device"`, `exit 0`, is matched only on the platform that reserves the name (a POSIX file called `nul` is a file), and a run writing there keeps its checkpoint in the partials directory instead of `nul.partial`. The platform's message travels in `os_error: {raw, errno, errno_name, locale}`, never inside the contract sentence, and the error publishes `cleaned_up: [...]` — what the run took back with it, so "read-only" is checkable rather than trusted. Parent directories are created, deliberately, and the hint says so. `phased_run` writes through the same authority and registers its checkpoint as this run's, so a failed answer removes it. Regression `tests/test_output_sink_leaves_nothing_b15.py`, 12 assertions, including the end-to-end matrix leaving the tree byte-identical and a negative control that cleanup can only ever remove what this run wrote. |
|
|
42
42
|
| ASK-10 | **A command with an output ceiling and no flag that can get under it.** `posture spring-boot` returned 355 032 B / `exit 0` in 5.6.1; the unified ceiling made it `exit 1` — the right call — and left no in-band way out: not `--limit`, not `--top-n`, not `--min-severity`, not `--compact`. The only routes were `--output FILE` (the answer leaves the reader's context) and `ASK_MAX_OUTPUT_TOKENS=0` (the guard off). `exit 1` on **2 of 8** corpus repositories, and `thingsboard` returned `exit 0` in 5.7.2 — so the trap closes as the product improves: the growth was **better detection**, `unresolved` 64 120 → 184 864 B because conditional beans now name `@ConditionalOnExpression` instead of a generic reason. `validation` inherits it. | 5.7.2 audits, escalated to high and blocking by the 5.8.0 re-audit | **High** — structural rather than incidental: every future gain in exhaustiveness pushes another command at the ceiling | **closed 5.8.1** — `posture` and `validation` declare `--limit N` and `--compact`, both already in the ceiling's vocabulary, so the hint offers them without being taught (verified: with the ceiling forced, both now answer *"Use --compact …, --limit N …"*). Bounding is one authority, `sourcecode.output_bounds`: it walks the payload so a `--diff` answer is bounded on **both** sides, touches lists and never counts (`summary.unresolved` is an int under the same name), and every cut publishes the `*_cap` contract — `{total, shown, omitted}` plus the registered effect — because a flag that cuts without declaring the cut is B14 again. **Nothing is bounded by default**: a payload that shrinks silently between releases is what AS-16 was filed for. Measured: `posture keycloak-config-cli` 53 115 → 4 605 B under `--compact`, summary byte-identical. The invariant is now derived from the CLI registry (`tests/test_bounding_flag_registry_ask10.py`): a command that can exceed the ceiling and declares no bounding flag fails the battery until somebody decides, with `cli.COMMANDS_WITHOUT_BOUNDING` carrying the exemptions and their reasons — including, on the record, the repository-scale commands that have not yet been measured against the ceiling. Behaviour battery `tests/test_output_bounds_ask10.py`, 13 assertions. |
|
|
43
43
|
| B4 | **`verify-edit` reported two repository-wide builds beside two empty lists, and nothing in the payload could explain them.** Reported as the empty-diff short circuit failing: clean worktree, `changed_files: []`, `changed_build_files: []`, `head_model_cache_hit: true`, `model_built: true`, 23 468 ms with `ASK_PROGRESS` never leaving *diffing working tree vs HEAD*. | 5.6.1 audit, re-reported through the 5.7.2 and 5.8.0 audits | High | **closed 5.8.1 — and the reported cause is not the cause.** Measured on BroadleafCommerce (2 985 `.java`) with a clean tree: **0,47 s, `model_built: false`** — the short circuit (C3-32 / C3-102 / AS-13) fires, and the row's stated acceptance (`< 2 000 ms`, `model_built: false`, `verdict: pass`) already held. The reported shape reproduces exactly with **one untracked `zz.yml`**: both lists empty, `model_built: true`, **16,3 s**. `_can_move_a_verdict` admits any suffix an axis may read — deliberately, since C3-14 — so a `.yml` that is not `application*` is neither a changed `.java` nor a changed build file, and appeared in **no** published list. The change set was never empty; the payload could not say so. `analysis.model_forced_by: {count, paths, reason}` is that sentence, and `basis` no longer reads *"0 changed .java file(s) and 0 changed build file(s)"* beside two full builds. Regression `tests/test_verify_edit_names_what_forced_the_model_b4.py`, 5 assertions, including the negative control that a changed `.java` file is not explained twice. ⚠ Still open and now separable: the two-build cost itself when a forcing path exists (the parse cache serves the parse; the IR assembly is paid twice), tracked as the profiling half. |
|
|
44
|
-
| ASK-09 / B16-sv | **Three output-ceiling guards written outside the authority that owns them.** Reported as `endpoints`, `cold-start` and `posture` claiming *"No smaller inline variant is available with the current flags"* while `--compact` fits (10 194 B ≈ 2,5 K tokens). | 5.7.2 audits (corpus `ASK-09`, `saint-server` `B16-sv` — one row, two origin IDs) | High (corpus: medium) | **closed 5.8.4 — does not reproduce, measured on the reporter's own subject, and the residue it was hiding is fixed.** `tutorials` (24 073 `.java`), no environment variable, the real 50K ceiling: `endpoints` answers *"Use --compact …, --limit N …"* at `estimated_tokens: 241 041`, `cold-start` answers *"Use --compact …"* at 230 518, and `posture` does not arise — 134 431 B ≈ 33K tokens, `exit 0`, no hint at all. Three cycles of byte-identical text were measured against a build, not against this tree. **What the row's rewritten acceptance did buy is the battery**: `tests/test_ceiling_hint_at_real_scale_ask09.py` builds a repository that genuinely overflows (1 200 handler mappings, ~107K tokens), *discovers* the population by running every path-only command that declares a bounding flag rather than listing them, and asserts that no such command denies a flag it declares — parametrised by (command, repository), never by (command, artificial ceiling). It is verified red: forcing the *"nothing smaller"* prefix fails it. It also refuses to be vacuous — the fixture must overflow, the population must contain the two commands the field named, and the flag the hint offers must actually get the answer under the ceiling when run. ⚠ **The residue the old fixture could not see**: `repo-ir`'s stdout guard compared against a constant bound at import, so `ASK_MAX_OUTPUT_TOKENS=0` — which `breaking-changes-v1` declares as restoring pre-5.7.0 behaviour *exactly*, and which `repo-ir`'s own hint offers — left it refusing while every other command printed, and its refusal published no `estimated_tokens`, no `ceiling_tokens` and no `estimate_basis`, so the number it refused on existed only inside an English sentence. Both now come from `output_ceiling.exceeds`, the authority: one refusal, one shape, one ceiling that the caller can actually move. Measured: `=0` now exits 0, a lowered ceiling bites, and the payload carries its measurement. **History, kept because the row was closed twice on it:** REOPENED 5.8.2 — the closure was verified against the wrong path: against the unified emit seam, with the ceiling forced, `endpoints` answers *"Use --compact …, --limit N …"* and `cold-start` *"Use --compact …"* — the derivation reads `ctx.command.params` and works. What did reproduce is the half the row itself names: `repo-ir` carried **two** hand-written hints (stdout and the separate MB guard on a written file) and `migrate-check` a **third**, `_migrate_check_output_hint` — a hand-maintained copy of `build_hint`, same list, same prefix rule. A hand-written escape list cannot know which flags the caller already passed, which is the reported defect seen from the other side. `output_ceiling` now owns the whole vocabulary: `--force` as a **bypass** (offered only where the command declares it), `--gzip` as a **file-only** bound, `ways_out(destination="file")` for the MB guard — where `--output FILE` is not an escape and the token env var does not apply — and the *"nothing smaller"* prefix decided by counting the **bounding** escapes rather than the sentence, so a command whose only remaining escape is `--force` no longer opens with "Use". `_migrate_check_output_hint` is deleted. Regression `tests/test_ceiling_hint_one_authority_ask09.py`, 7 assertions, two of which are structural: `cli.py` may not contain the prefix sentence, and every `OUTPUT_TOO_LARGE` hint in it must come from `build_hint` (AST-checked). ⚠ **Reopened by the 5.8.2 re-audits, which report it as `B16` (saint-server) and `ASK-09` (corpus) — one defect, two origin IDs, like `B16-sv` before it.** The fix landed and the *hint* half did not: `ask endpoints tutorials` (24 073 `.java`) still answers *"No smaller inline variant is available with the current flags"* at `estimated_tokens: 240 908`, while `--compact` (45 902 B ≈ 11,5 K) and `--limit 50` (66 765 B ≈ 16,7 K) both fit with 4× of margin and are both declared on `endpoints`. Byte-identical text since 5.7.2, third cycle. **The diagnosis is the valuable part and it is about our method, not the code**: the regression assertion forces a low ceiling (`ASK_MAX_OUTPUT_TOKENS=1`) over a *small* fixture payload, and the path that fails is the *natural* rebase of a 240 K-token payload against the real 50 K ceiling — so the battery validates one route while the defect lives in another, and a green test certified a defect the field reproduces in three consecutive cycles. `posture` received `--limit`/`--compact` in the same release this row was closed in and *does* answer correctly, which is why the derivation looked proven. Suspected: a short-circuit that decides the hint before `ctx.command.params` is consulted, reachable only at a size the fixture never reaches (compare `ASK_MAX_OUTPUT_TOKENS=2000 ask endpoints <small>` against `ask endpoints <24k-file repo>`). **Acceptance, restated so the same closure cannot repeat**: the hint names `--limit` and `--compact` with an unforced ceiling on a repository that genuinely exceeds 50 K, and the regression is parametrised by `(command, repository)` over a fixture that really overflows — never by `(command, artificial ceiling)`. |
|
|
44
|
+
| ASK-09 / B16-sv | **Three output-ceiling guards written outside the authority that owns them.** Reported as `endpoints`, `cold-start` and `posture` claiming *"No smaller inline variant is available with the current flags"* while `--compact` fits (10 194 B ≈ 2,5 K tokens). | 5.7.2 audits (corpus `ASK-09`, `saint-server` `B16-sv` — one row, two origin IDs) | High (corpus: medium) | **closed 5.8.5 — the fifth cycle diagnosed by measurement instead of by hypothesis, and the derivation given a second authority that no thread boundary can take away.** The ledger asked for the difference between our 241 041 tokens and the reporter's 241 394 to be found in *the environment or the entry point*, and for no third fixture. It was found in the entry point, and it was measured here, on `HEAD` before the fix: `output_ceiling.build_hint(None)` — the guard reached with **no Click context** — returned *"No smaller inline variant is available with the current flags; --output FILE …, ASK_MAX_OUTPUT_TOKENS=0 …"*, the field's sentence byte for byte, about a command that declares two bounding flags. `_declared_and_used(None)` answered `([], set())` and the prefix rule spelled that **absence of a reading** as a **claim about the command**. Nothing about the repository, the size or the command was ever the subject; a `CliRunner` fixture always has a context, which is why nine batteries stayed green across three cycles. **Four things close it.** (1) **A second, context-free authority**: `output_ceiling.resolve()` reads Click's context first — the only source that can tell `--limit 50` from the 50 nobody typed — and the **command tree resolved from `sys.argv`** second, consulted only when the context produced no bounding flag at all, which is the signature of both failures the row suspects (no context; or a context pointing at a group or wrapper whose `params` are not the leaf's). The tree is registered by the entry point as a *provider*, so the Typer→Click conversion is paid only by a run already over the ceiling, and never on the answer path. (2) **Observability, which is the half the ledger said ends the cycle**: every `OUTPUT_TOO_LARGE` refusal — the emit seam, `repo-ir`'s stdout guard, `repo-ir`'s MB guard and `migrate-check`, all four — publishes `hint_basis: {derived_from, command, declared_flags, flags_already_passed, context_found}`, so **one** field run names which authority answered and what it read, instead of a fourth round of guessing. Enforced structurally: an `OUTPUT_TOO_LARGE` refusal in `cli.py` with no `hint_basis` fails the battery (AST-checked). (3) **A failed derivation fails loudly**: with nothing resolved, the hint opens *"The flags that would bound this answer could not be read from the running invocation, so this list may be missing a smaller inline view — check `--help` for the command"* and keeps the universal escapes, which hold whatever the command is. The *"no smaller inline variant"* sentence is now reserved for what it claims: a command that **was** read and declares nothing bounding, or whose bounding flags this run already passed. (4) **The collateral the row named**: `_flag_was_given` — which governs the `--force` bypass and the `--limit`-versus-`--output` reading — delegates to the same authority (`flag_was_passed`), so a run that typed `--force` and reached the guard off-context is no longer refused with its bypass silently removed. **Measured, before and after, through the real entry point** (`run_cli.py`, real ceiling, `click.get_current_context` degraded to `None` for the ceiling module only — the shape of the hypothesis, not a stub of the answer): before, the field's sentence; after, *"Use --compact …, --limit N …"* with `derived_from: "argv"`, `context_found: false`, and `--force` no longer refused on the same route. **And on the field's own subject**, `tutorials` @24 074 `.java`, no `ASK_MAX_*`, the real 50 K ceiling: `estimated_tokens: 241 527` — 0,06 % from the reporter's 241 394, so the same tree and the same payload — hint *"Use --compact …, --limit N …"*, `hint_basis.derived_from: "click"`, `context_found: true`. That is the reading this platform gives; the reporter's platform is where it degrades, and the refusal now carries the field that says which. Regression `tests/test_ceiling_hint_without_context_ask09.py`, 10 assertions, **verified red against `HEAD`** (6 failed, 4 errored) — the condition no `CliRunner` can produce, asserted end-to-end in a subprocess. ⚠ **What is still owed, and it is not a code change**: the acceptance run on Windows 11 / pipx against the real clone. It is no longer a blocking unknown — if the sentence reappears there, `hint_basis` names the cause in one run. **History of the reopening, kept because the row was closed twice on it:** **reopened 5.8.4 — fifth cycle, and the second closure declared against an artefact instead of against the reported subject.** Both 5.8.4 re-audits reproduce it, independently, on different machines. Corpus, Windows 11 / pipx / Git Bash, `tutorials` (24 073 `.java`, ~600 Maven modules), no `ASK_MAX_*` in the environment, no `tutorials/.ask`, the real 50K ceiling: `ask endpoints tutorials` exits 1 with the byte-identical *"No smaller inline variant is available with the current flags"* at `estimated_tokens: 241 394`, while `--compact` (44 973 B ≈ 11,2K) and `--limit 50` (65 038 B ≈ 16,3K) both exit 0 with 3–4× of margin and are both declared on `endpoints`. `saint-server` round 9 reports the same sentence from `cold-start` and `endpoints`, with `--compact` measured at 76 053 B and 10 230 B — **0,4 % of the payload the hint tells the caller to dump**. **The number is what makes this diagnosable at last**: our closure measured 241 041 tokens on the tree the reporter measures 241 394 on — 0,15 % apart, so it is the same repository and the same payload, and the difference lives in the environment or the entry point, not in the subject. That difference IS the finding. ⚠ **Do not build a third fixture.** Hypotheses for the agent that fixes this, cheapest discriminator first: (1) `exceeds()` calls `build_hint()` with no context, so `_declared_and_used` recovers it from `click.get_current_context(silent=True)`, which is **thread-local** — any emission that reaches the guard off the invoking thread, after the context is popped, or through a shim entry point sees `ctx is None`, gets `([], set())` back, and the hint degrades **silently** into the false sentence; a `CliRunner` fixture always has a context, which is exactly why every battery we own is green; (2) `ctx.command` resolving to a wrapper or group rather than the leaf command, so its `params` do not carry `--compact` — the reported environment is a pipx console script on Windows, which no run of ours covers; (3) a short-circuit deciding the hint before `ctx.command.params` is consulted, reachable only above a size no fixture reaches. **The fix that ends the cycle is not the sentence, it is the observability**: the `OUTPUT_TOO_LARGE` payload must publish what the hint was derived from — `declared_flags`, `flags_already_passed`, and whether a context was found at all — so one field run names the cause instead of a fourth round of guessing, and a context-less hint must fail loudly rather than claim *"nothing smaller"*, which is a confident falsehood in the exact sense this repository forbids. ⚠ The same context lookup governs `_flag_was_given("force")`, so the `--force` bypass is suspected to fail on the identical path and has never been exercised in the field. **Acceptance, unchanged and now carrying a platform**: `ask endpoints tutorials` with no environment variable names `--limit` and `--compact`, verified on Windows/pipx against a real clone (`git clone --depth 50 --single-branch https://github.com/eugenp/tutorials.git`), never against a synthetic repository. **What the 5.8.4 closure did buy, and it is real: the battery, and the `repo-ir` residue it uncovered.** History of both closures, kept because the row was closed twice on them: `tutorials` (24 073 `.java`), no environment variable, the real 50K ceiling: `endpoints` answers *"Use --compact …, --limit N …"* at `estimated_tokens: 241 041`, `cold-start` answers *"Use --compact …"* at 230 518, and `posture` does not arise — 134 431 B ≈ 33K tokens, `exit 0`, no hint at all. Three cycles of byte-identical text were measured against a build, not against this tree. **What the row's rewritten acceptance did buy is the battery**: `tests/test_ceiling_hint_at_real_scale_ask09.py` builds a repository that genuinely overflows (1 200 handler mappings, ~107K tokens), *discovers* the population by running every path-only command that declares a bounding flag rather than listing them, and asserts that no such command denies a flag it declares — parametrised by (command, repository), never by (command, artificial ceiling). It is verified red: forcing the *"nothing smaller"* prefix fails it. It also refuses to be vacuous — the fixture must overflow, the population must contain the two commands the field named, and the flag the hint offers must actually get the answer under the ceiling when run. ⚠ **The residue the old fixture could not see**: `repo-ir`'s stdout guard compared against a constant bound at import, so `ASK_MAX_OUTPUT_TOKENS=0` — which `breaking-changes-v1` declares as restoring pre-5.7.0 behaviour *exactly*, and which `repo-ir`'s own hint offers — left it refusing while every other command printed, and its refusal published no `estimated_tokens`, no `ceiling_tokens` and no `estimate_basis`, so the number it refused on existed only inside an English sentence. Both now come from `output_ceiling.exceeds`, the authority: one refusal, one shape, one ceiling that the caller can actually move. Measured: `=0` now exits 0, a lowered ceiling bites, and the payload carries its measurement. **History, kept because the row was closed twice on it:** REOPENED 5.8.2 — the closure was verified against the wrong path: against the unified emit seam, with the ceiling forced, `endpoints` answers *"Use --compact …, --limit N …"* and `cold-start` *"Use --compact …"* — the derivation reads `ctx.command.params` and works. What did reproduce is the half the row itself names: `repo-ir` carried **two** hand-written hints (stdout and the separate MB guard on a written file) and `migrate-check` a **third**, `_migrate_check_output_hint` — a hand-maintained copy of `build_hint`, same list, same prefix rule. A hand-written escape list cannot know which flags the caller already passed, which is the reported defect seen from the other side. `output_ceiling` now owns the whole vocabulary: `--force` as a **bypass** (offered only where the command declares it), `--gzip` as a **file-only** bound, `ways_out(destination="file")` for the MB guard — where `--output FILE` is not an escape and the token env var does not apply — and the *"nothing smaller"* prefix decided by counting the **bounding** escapes rather than the sentence, so a command whose only remaining escape is `--force` no longer opens with "Use". `_migrate_check_output_hint` is deleted. Regression `tests/test_ceiling_hint_one_authority_ask09.py`, 7 assertions, two of which are structural: `cli.py` may not contain the prefix sentence, and every `OUTPUT_TOO_LARGE` hint in it must come from `build_hint` (AST-checked). ⚠ **Reopened by the 5.8.2 re-audits, which report it as `B16` (saint-server) and `ASK-09` (corpus) — one defect, two origin IDs, like `B16-sv` before it.** The fix landed and the *hint* half did not: `ask endpoints tutorials` (24 073 `.java`) still answers *"No smaller inline variant is available with the current flags"* at `estimated_tokens: 240 908`, while `--compact` (45 902 B ≈ 11,5 K) and `--limit 50` (66 765 B ≈ 16,7 K) both fit with 4× of margin and are both declared on `endpoints`. Byte-identical text since 5.7.2, third cycle. **The diagnosis is the valuable part and it is about our method, not the code**: the regression assertion forces a low ceiling (`ASK_MAX_OUTPUT_TOKENS=1`) over a *small* fixture payload, and the path that fails is the *natural* rebase of a 240 K-token payload against the real 50 K ceiling — so the battery validates one route while the defect lives in another, and a green test certified a defect the field reproduces in three consecutive cycles. `posture` received `--limit`/`--compact` in the same release this row was closed in and *does* answer correctly, which is why the derivation looked proven. Suspected: a short-circuit that decides the hint before `ctx.command.params` is consulted, reachable only at a size the fixture never reaches (compare `ASK_MAX_OUTPUT_TOKENS=2000 ask endpoints <small>` against `ask endpoints <24k-file repo>`). **Acceptance, restated so the same closure cannot repeat**: the hint names `--limit` and `--compact` with an unforced ceiling on a repository that genuinely exceeds 50 K, and the regression is parametrised by `(command, repository)` over a fixture that really overflows — never by `(command, artificial ceiling)`. ⚠ **The owed acceptance run is no longer owed: both 5.8.5 re-audits certify the closure from the outside, one of them on the exact platform the row could never reach.** Corpus cycle 8, Windows 11 / pipx / Git Bash, `tutorials`, no `ASK_MAX_*`, real 50 K ceiling: `estimated_tokens: 241 394` — the same number as the four failing cycles, so the same tree and the same payload — now answering *"Use `--compact` …, `--limit N` …, `--output FILE` …, `ASK_MAX_OUTPUT_TOKENS=0` …"*, and the reporter records the root cause as ours: *"el hint no negaba los flags: negaba haber podido leerlos, y la regla de prefijo convertía «no leí nada» en «no hay nada»"*. `saint-server` round 10 certifies the **derivation** rather than the sentence, which is the half that was not asked for: `cold-start` names `--compact` and **not** `--limit` because it declares none, `endpoints` and `validation` name both, and each offered flag is measured actually getting under the ceiling (`cold-start --compact` 76 324 B; `endpoints` 10 230 / 13 534 B; `validation` 6 982 / 10 684 B against 961 077 B unbounded, −99,3 %) — *"la introspección de Click funcionando en las dos direcciones"*, with the inverse error (naming a flag the command does not have) explicitly checked for and absent. Both reporters mark the row closed; the corpus reporter's open inventory is now empty except `R2`. |
|
|
45
45
|
| ASK-12 | **A field describing an exclusion named three production trees as excluded.** `spring-audit spring-boot` published `patterns_matched: [… "core/", "module/", "cli/" …]` — 4 254 + 1 625 + 76 production files. Read literally, whole modules had left the audit population; measured, the exclusion was *conservative* (3 650 excluded against 4 214 files actually under a test root). The auditor filed an over-exclusion false positive on the strength of the field and then withdrew it. Cause: the constructor's fallback named `parts[0]`, the first path segment, when no source-root marker matched — and the same published field had **two constructors** with different shapes and different `basis` strings (`cli.py` for `spring-audit`, `repository_ir.py` for `endpoints`) over the one authority both already shared. | 5.7.2 audits | Low — a reporting defect, not an exclusion one, which is why it cost an auditor a round instead of costing a finding | **closed 5.8.1** — one producer, `repository_ir.excluded_test_source_detail`, beside `is_test_source_file`, which is the authority that decides the fact. `patterns_matched` can now only hold a **declared test root** — a directory whose files are excluded in their entirety — and everything else is published as a `rules_matched` census with a `rules_meaning` sentence per rule, so a per-file rule can no longer be read as a whole-directory claim. A surface counting something other than files says so (`unit`, `files_matched`), because `endpoints` counts route annotations. Regression `tests/test_test_source_exclusion_one_producer_ask12.py`, 14 assertions, two of them structural: `cli.py` may not compose the key at all, and exactly one function in `repository_ir.py` may (AST-checked). |
|
|
46
46
|
| B14 | **`--compact` made the answer smaller by making it unattributable.** `cold-start --compact` took 1 968 807 characters to 16 452 (−99,2 %) and removed **11 of 19 keys** on the way — `schema_version`, `repo_id`, `current_git_head` and the 331-character `hotspots` among them — in a document whose weight was **97,7 % `endpoints`**, and cut the endpoint list to 30 with no `*_cap`. The view an agent is told to inject could not say which repository, which commit or which schema it described. | 5.6.1 audit; AS-15 closed the root `--compact` half in 5.7.0, this is the `cold-start` half, re-reported against 5.8.0 | High | **closed 5.8.1** — three rules, in the shared bound rather than invented per command. `COMPACT_IMMUTABLE_KEYS` is the minimum no `--compact` may remove (provenance is contract, not content). Every collection that was cut declares `{total, shown, omitted}` with the registered effect — measured on BroadleafCommerce, `endpoints_cap` now publishes `total: 272, shown: 30, omitted: 242` where the cut was silent. And trimming goes by **weight**: a section already under 2 KB is kept rather than dropped by a fixed list, which is what recovers `hotspots`; everything actually removed is named with its size in `compact_omitted`. Regression `tests/test_compact_keeps_identity_b14.py`, 12 assertions, including that nothing removed claims nothing. |
|
|
47
47
|
| B2 | **A risk band and a rule severity share a vocabulary and measure different things.** They disagreed on **34 of 50 rows (68 %)** — 5 defects rated `medium` band as `critical`, 29 rated `high` band as `medium` — and both numbers were right: severity is what the rule says about the defect, the band is what the composition says about it in *this* repository (reach, access, write effect). One surface on, the collision became a contradiction a reader could not resolve: `audit-report` announced **14 critical** over bands while `--min-severity critical` filtered severities and returned **0**. | 5.6.1 audit, reproduced through 5.8.0 | Medium | **closed 5.8.1** — the band now also has a name that cannot be mistaken for a severity: `risk_tier: R1…R5`, published **beside** `band` and never instead of it, so no consumer loses a field it reads. One mapping (`risk_model.tier`), one sentence (`TIER_CROSSWALK_NOTE`) travelling with every count — it names both axes, states that they disagree by construction, gives the crosswalk, and points at `--min-band` as the flag that filters tiers rather than `--min-severity`. `risk` publishes `by_tier` beside `by_band`; `audit-report` leads with tiers, keeps bands under the legacy spelling, renders the note, and derives tiers from bands when it is handed an older bundle. Regression `tests/test_risk_tier_is_not_a_severity_b2.py`, 12 assertions, including that no tier shares a spelling with any severity and that a summary with no census still says *not measured* rather than zero. ⚠ The row's `reach`/`access`/`score_raw` per row was already shipped (`factors`, `endpoints_reached`, `reach_scope`); `--min-band` already existed on `risk` and `enrich`. |
|
|
@@ -53,14 +53,20 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
53
53
|
| P0-1 | **`impact` publishes a confident zero on a class the container wires by string.** `ask impact IdOverrideTableGenerator . --depth 6` @ `8645873661` answers `direct_callers: []`, `risk_level: low`, `confidence_score: 1.0`, `closure_complete: true`, no `analysis_warnings`, *"No callers or dependents found … Low-risk isolated change."* — on a Hibernate id generator that 135 entities bind by string in `@GenericGenerator`, while `ask explain` lists **119 referencing classes** and `ask impact-chain`, in the same run, publishes `confidence: low` with the CH-005 blind spot named. Three commands, one symbol, three verdicts, and the unhedged one is the sentence a migration scope is cut with | Broadleaf adversarial audit 2026-08-17 (§13) | **High** — the failure mode that destroys the offer in one line: it excludes from scope the exact component a Hibernate migration turns on | **closed 5.8.3** (`90f10b2`) — the reading of an empty result is now one authority, `sourcecode.empty_reach`: the five codes (CH-005, CH-003, G-2, P1-B, P1-E), the precedence between them, the words each is published in and the confidence they force. Only the detectors stay with their substrate, and CH-003's moved whole because it only ever read graph nodes, which both callers hold. `impact` publishes `blind_spots` with the chain's codes, `analysis_warnings` with byte-identical text, `confidence_score` capped below the `medium` band, `closure_complete: false` with `unmodeled_inbound_edges` as the third cause beside E-32's two, and an explanation that states the blind spot instead of asserting isolation. Measured on the audited commit: confidence 1.0 → 0.4, `closure_complete` true → false, `blind_spots: [framework_di]` naming the Hibernate supertype. Negative controls hold — a stereotype bean with no callers keeps its confident answer. Battery `tests/test_empty_reach_authority_p01.py`, 14 assertions, including a cross-command invariant that fails if the two payloads disagree on the codes or on the words |
|
|
54
54
|
| P0-2 | **Endpoint reach is not monotone: a callee reports less than its caller.** `ask impact BasicPersistenceModule . --depth 6` → 268 endpoints; `ask impact CriteriaTranslator . --depth {6,8}` → **0**, with `BasicPersistenceModule` a direct caller of `CriteriaTranslator` and the walk reporting `closure_complete` for itself. Breaks the property a tech lead assumes without asking — if A calls B, the reach of B contains the reach of A — and invites excluding the core of the change | Broadleaf adversarial audit 2026-08-17 (§13) | **High** | **closed 5.8.3** (`41c3150`, `a6cbe21`) — **the reported cause is not the cause**: the hub-frontier heuristic expands its nodes in the main walk and the closure was genuinely exhausted. Two expansions applied to the SEED and to nothing else — the subtype expansion (E-27's rule) and the interface bridge for `implements` edges carrying an unresolved short name, which `ImplementationGraph` cannot see. Reach depended on where the walk started. Both now live in `caller_reach.ReachExpansion`, applied at every node by the traversal both commands share. Measured A/B, depth 6, same cache: `CriteriaTranslator` **0 → 274** endpoints, `BasicPersistenceModule` 268 → 274, `Money` 60 → 277, and `AdminSecurityFilter`/`IdOverrideTableGenerator`/`SkuCustomPersistenceHandler` unchanged at 0. Cost flat (0.09–0.16 s/query vs 0.08–0.23 s). `impact-chain` takes the same object: 0 → 239 endpoints at the same depth. ⚠ The subtype half narrows to the corresponding member and the DI bridge does not — see `C1-25`, which the first cut of this fix reopened. Battery `tests/test_reach_monotonicity_p02.py`, 8 assertions |
|
|
55
55
|
| P0-3 | **The negative set derived from `pr-impact` is wrong in ≥54 % and does not exist as a state.** `ask pr-impact . --files -` over the Hibernate surface → 258 affected endpoints; the complement against `ask endpoints` is 13, of which **3 are reachable according to ASK itself** (`impact-chain 'AdminBasicEntityController#viewEntityForm'` returns `/user-management/{id}`) and 4 more through `ProductHandlerMapping`/`CategoryHandlerMapping`/`SkuHandlerMapping` → `blCatalogService` → `ProductDaoImpl`, which is in the changed set | Broadleaf adversarial audit 2026-08-17 (§6.2, §13) | **High** — it is literally the deliverable (*"what NOT to review"*), and it would fail on the routes a QA lead checks first | **closed 5.8.3** (`1803ebb`) — three states with one authority (`sourcecode.endpoint_disposition`): `affected`, `unknown` (with the reason), `unaffected` (with its basis), partitioning the declared route population so nothing is subtracted. Three blockers demote `unaffected`, each repository-wide because none is a statement about one route: a container-wired class inside the cone (the audit's handler-mapping chain, caught through the `container_wiring` authority, never by a name), a closure that was not exhausted, a changed file that mapped to no class. Closing it needed two missing edges, both found by reproducing §6.2: **`super.m()` produced no call edge when the overriding class declared `m` itself** — which is what an override IS — and **a request handler was not callable**, because handlers carry `symbol_kind: "endpoint"` and `_type_indices` indexed only `"method"`. Re-run on the audited commit: **affected 254 · unknown 14 · unaffected 0 of 268**, with every one of the 7 rows the audit proved reachable now `affected`. Batteries `tests/test_endpoint_disposition_p03.py` (9 assertions) |
|
|
56
|
-
| B15-res | **The `.partial` is not removed on the error path.** B15 closed the atomic-temporary half in 5.8.1 and it holds — the null device answers `exit 0` with no residue, the OS locale is isolated in `os_error`, the hint names the parent-directory decision — but a fourth artefact was outside the `finally`: `ask spring-audit . -o ./saint-server` against an existing directory leaves `?? saint-server.partial` in the caller's repository. The auditor has cleaned the user's repository by hand in four consecutive sessions. | 5.8.0 → 5.8.2 re-audits (residual of `B15`) | Medium — small in code, large in claim: **the pitch is read-only deterministic analysis**, and a pre-CI security review blocks a tool that writes into the client's tree and leaves it there | **closed 5.8.3** (`6fd8001`) — registration is about OWNERSHIP, not about which writer did the writing, and the `register_artifact` call sat inside the branch of `PhasedRun._write` that writes the file itself. The three commands that supply `_safe_write_file` therefore wrote a checkpoint nobody owned, so the failure path ran `cleanup_artifacts()` against an empty registry. Reproduced before and after on `spring-audit -o <existing dir>`: `cleaned_up: []` + residue → `cleaned_up: ["target-dir.partial"]` + a clean tree. Battery `tests/test_hygiene_rows_b15res_b18_ask16.py`, with the negative control that a delivered answer leaves no checkpoint and cleanup does not fire on a good run |
|
|
57
|
-
| B16 / ASK-09 | *(see the `ASK-09 / B16-sv` row above — same defect, reopened there.)* The 5.8.2 saint-server audit files it as `B16` and the corpus audit as `ASK-09`; both describe the ceiling hint denying a `--compact` that exists and fits. Recorded here only so a reader searching `B16` lands on the reopened row rather than concluding it is untracked. | 5.8.2 re-audits | High | **
|
|
58
|
-
| B18 | **`--min-band` rejects the tier vocabulary that our own report teaches.** `audit-report` prints *"Risk tiers: R1 14, R2 7, R3 74, R4 3"*, the crosswalk *"R1 = band critical …"*, and the instruction *"Filter tiers with --min-band"* — and then `ask risk . --min-band R1` answers `INVALID_INPUT` and lists the severity spellings (`critical`, `high`, `medium`, `low`) as the only ones it takes. Two surfaces of one product, one teaching a vocabulary the other refuses. | 5.8.1 → 5.8.2 re-audits | Medium — **a regression introduced by the `B2` fix**: B2 gave the band a tier spelling precisely so it could not be read as a severity, and the filter was never taught the new vocabulary, so the product documents a filter expression it refuses | **closed 5.8.3** (`6fd8001`) — `risk_model.band_of` resolves either spelling from the crosswalk the report is already rendered from, with `BAND_OF_TIER` derived rather than retyped so the two cannot drift. `--min-band` and `--band` on `risk`, and `--min-band` on `enrich`, all read it; a rejection names both spellings. Measured on BroadleafCommerce: `--min-band R1` and `--min-band critical` both return 0 rows, `--min-band R2` returns 18. The second thread is closed too — `TIER_CROSSWALK_NOTE` names the command the flag lives on (`ask risk --min-band`) and what the other two carry instead. ⚠ **This row read `open` for its whole first day closed**: the verdict was written into the *defect* cell because the quoted error message carried escaped pipes (`\|`), and a reader — or a `grep` — counting open rows counted this one. The quoted message no longer carries a pipe; `tests/test_ledger_rows_are_wellformed.py` now asserts that every row has exactly five cells and that no cell but the last opens with a verdict. |
|
|
59
|
-
| B19 | **The schema registry resolves 44 identifiers and one of them has a document.** Residual of `B6`, which the same audit calls excellent and better than what it asked for: every identifier resolves, `pending_renames` declares the 13 non-conforming ones with their canonical name, and `endpoints` emits `endpoints-v1`. But `with_published_document: 1` — only `envelope-v1` has a JSON Schema behind it, and the rest resolve to their *description*. | 5.8.2 re-audit (residual of `B6`) | Medium — resolution enables **discovery**; the use case that opened B6 was **validation in CI** (`--ci` / `--fail-on`), and that needs the document, not the description | **closed 5.8.4 for the five a gate consumes** — `verify-v1`, `verify-edit-v1`, `endpoints-v1`, `pr-impact-v1` and `core-analysis-v1`, the set the audit puts at 90 % of CI use, are published as JSON Schema documents and served by `ask schema <name>`; `with_published_document` goes **1 → 7** (six identifiers; `pr-impact` and the root analysis each carry two emitted values pointing at one document). **The documents are held to the payloads, not to prose**: every one is validated in the battery against output the suite produces, so a document that stops describing what the command emits fails — a schema written from a reading of the code drifts the first time the code moves. Cross-checking against a second repository is what made them true rather than fitted: `spring-petclinic` caught `verify-edit`'s verdict vocabulary, which is `pass` / **`break`** / `unverified` and not the `fail` the first draft assumed — `break` is a statement about the change, not about the repository. **The two canonically-named documents sit beside emitted values that are still bare numbers** (`1.3`/`1.0` for `pr-impact`, `1.0` for the root analysis), deliberately: renaming an emitted `schema_version` is the incompatible change `ASK-11` exists to stop us making in silence, so `ask schema 1.0` still resolves to all seven shapes and each offers its canonical name to pin. Asserted both ways — every claimed document exists, every document is claimed, and no emitted identifier was quietly renamed to match one. Battery `tests/test_published_schema_documents_b19.py`, 30 assertions. ⚠ **Still open**: the other ~37 identifiers, which continue to resolve to their description. Original note — the row is honest about this already (`documented_note` says exactly which of the two it gives), so this is a scope completion rather than a correction. Recommended order, from the audit: the five a gate would actually consume — `verify-v1`, `verify-edit-v1`, the root analysis (`1.0` → `core-analysis-v1`), `endpoints-v1`, `pr-impact` (`1.2` → `pr-impact-v1`) — which it estimates at 90 % of CI use. Generate from the code that already owns each shape rather than by hand, since `entries[].constant` already names the authority. Publish `with_published_document` as a progress metric. |
|
|
56
|
+
| B15-res | **The `.partial` is not removed on the error path.** B15 closed the atomic-temporary half in 5.8.1 and it holds — the null device answers `exit 0` with no residue, the OS locale is isolated in `os_error`, the hint names the parent-directory decision — but a fourth artefact was outside the `finally`: `ask spring-audit . -o ./saint-server` against an existing directory leaves `?? saint-server.partial` in the caller's repository. The auditor has cleaned the user's repository by hand in four consecutive sessions. | 5.8.0 → 5.8.2 re-audits (residual of `B15`) | Medium — small in code, large in claim: **the pitch is read-only deterministic analysis**, and a pre-CI security review blocks a tool that writes into the client's tree and leaves it there | **closed 5.8.3** (`6fd8001`) — registration is about OWNERSHIP, not about which writer did the writing, and the `register_artifact` call sat inside the branch of `PhasedRun._write` that writes the file itself. The three commands that supply `_safe_write_file` therefore wrote a checkpoint nobody owned, so the failure path ran `cleanup_artifacts()` against an empty registry. Reproduced before and after on `spring-audit -o <existing dir>`: `cleaned_up: []` + residue → `cleaned_up: ["target-dir.partial"]` + a clean tree. Battery `tests/test_hygiene_rows_b15res_b18_ask16.py`, with the negative control that a delivered answer leaves no checkpoint and cleanup does not fire on a good run. ⚠ **Field-confirmed 5.8.4**: both error paths (`-o ./<existing dir>` and `-o /dev/null`) leave `git status --porcelain` empty on the reporter's machine — the first session in nine with no ASK residue to remove by hand |
|
|
57
|
+
| B16 / ASK-09 | *(see the `ASK-09 / B16-sv` row above — same defect, reopened there.)* The 5.8.2 saint-server audit files it as `B16` and the corpus audit as `ASK-09`; both describe the ceiling hint denying a `--compact` that exists and fits. Recorded here only so a reader searching `B16` lands on the reopened row rather than concluding it is untracked. | 5.8.2 re-audits | High | **reopened 5.8.4 — tracked as `ASK-09 / B16-sv`**, where the fifth cycle and the second false closure are recorded. The 5.8.4 saint-server audit files it as `B16` (five rounds, byte-identical text, `cold-start` and `endpoints`) and the corpus audit as `ASK-09` (fourth cycle, on `tutorials` under Windows/pipx). One defect, two origin IDs. **Closed 5.8.5 and verified from both corpora in round 10 / cycle 8** — see the `ASK-09 / B16-sv` row. ⚠ **Mapping to keep, because the same audit files it twice**: what round 10 calls `B5-residual` (*"`validation` was the only ceiling-bound command with no reduced variant"*, carried seven rounds) is not a third defect — the **flag** half is `ASK-10`, shipped 5.8.1 (`validation` declares `--compact` and `--limit N`), and the **hint** half naming it is this row, shipped 5.8.5. One product defect, three reporter IDs |
|
|
58
|
+
| B18 | **`--min-band` rejects the tier vocabulary that our own report teaches.** `audit-report` prints *"Risk tiers: R1 14, R2 7, R3 74, R4 3"*, the crosswalk *"R1 = band critical …"*, and the instruction *"Filter tiers with --min-band"* — and then `ask risk . --min-band R1` answers `INVALID_INPUT` and lists the severity spellings (`critical`, `high`, `medium`, `low`) as the only ones it takes. Two surfaces of one product, one teaching a vocabulary the other refuses. | 5.8.1 → 5.8.2 re-audits | Medium — **a regression introduced by the `B2` fix**: B2 gave the band a tier spelling precisely so it could not be read as a severity, and the filter was never taught the new vocabulary, so the product documents a filter expression it refuses | **closed 5.8.3** (`6fd8001`) — `risk_model.band_of` resolves either spelling from the crosswalk the report is already rendered from, with `BAND_OF_TIER` derived rather than retyped so the two cannot drift. `--min-band` and `--band` on `risk`, and `--min-band` on `enrich`, all read it; a rejection names both spellings. Measured on BroadleafCommerce: `--min-band R1` and `--min-band critical` both return 0 rows, `--min-band R2` returns 18. The second thread is closed too — `TIER_CROSSWALK_NOTE` names the command the flag lives on (`ask risk --min-band`) and what the other two carry instead. ⚠ **This row read `open` for its whole first day closed**: the verdict was written into the *defect* cell because the quoted error message carried escaped pipes (`\|`), and a reader — or a `grep` — counting open rows counted this one. The quoted message no longer carries a pipe; `tests/test_ledger_rows_are_wellformed.py` now asserts that every row has exactly five cells and that no cell but the last opens with a verdict. ⚠ **Field-confirmed 5.8.4**: `ask risk . --min-band R1 -o out.json` exits 0 on the reporter's corpus |
|
|
59
|
+
| B19 | **The schema registry resolves 44 identifiers and one of them has a document.** Residual of `B6`, which the same audit calls excellent and better than what it asked for: every identifier resolves, `pending_renames` declares the 13 non-conforming ones with their canonical name, and `endpoints` emits `endpoints-v1`. But `with_published_document: 1` — only `envelope-v1` has a JSON Schema behind it, and the rest resolve to their *description*. | 5.8.2 re-audit (residual of `B6`) | Medium — resolution enables **discovery**; the use case that opened B6 was **validation in CI** (`--ci` / `--fail-on`), and that needs the document, not the description | **closed 5.8.4 for the five a gate consumes** — `verify-v1`, `verify-edit-v1`, `endpoints-v1`, `pr-impact-v1` and `core-analysis-v1`, the set the audit puts at 90 % of CI use, are published as JSON Schema documents and served by `ask schema <name>`; `with_published_document` goes **1 → 7** (six identifiers; `pr-impact` and the root analysis each carry two emitted values pointing at one document). **The documents are held to the payloads, not to prose**: every one is validated in the battery against output the suite produces, so a document that stops describing what the command emits fails — a schema written from a reading of the code drifts the first time the code moves. Cross-checking against a second repository is what made them true rather than fitted: `spring-petclinic` caught `verify-edit`'s verdict vocabulary, which is `pass` / **`break`** / `unverified` and not the `fail` the first draft assumed — `break` is a statement about the change, not about the repository. **The two canonically-named documents sit beside emitted values that are still bare numbers** (`1.3`/`1.0` for `pr-impact`, `1.0` for the root analysis), deliberately: renaming an emitted `schema_version` is the incompatible change `ASK-11` exists to stop us making in silence, so `ask schema 1.0` still resolves to all seven shapes and each offers its canonical name to pin. Asserted both ways — every claimed document exists, every document is claimed, and no emitted identifier was quietly renamed to match one. Battery `tests/test_published_schema_documents_b19.py`, 30 assertions. ⚠ **Field-verified in 5.8.4**: the audit reads the documents back as real JSON Schema draft 2020-12 with `$schema`, `$id` and `title`, confirms `with_published_document: 1 → 7` and the six identifiers, and records that contract validation in CI — the use case that opened `B6` eight rounds earlier — is executable at last. The 13 declared renames it leaves behind are tracked as `B20`. ⚠ **Still open**: the other ~37 identifiers, which continue to resolve to their description. Original note — the row is honest about this already (`documented_note` says exactly which of the two it gives), so this is a scope completion rather than a correction. Recommended order, from the audit: the five a gate would actually consume — `verify-v1`, `verify-edit-v1`, the root analysis (`1.0` → `core-analysis-v1`), `endpoints-v1`, `pr-impact` (`1.2` → `pr-impact-v1`) — which it estimates at 90 % of CI use. Generate from the code that already owns each shape rather than by hand, since `entries[].constant` already names the authority. Publish `with_published_document` as a progress metric. |
|
|
60
60
|
| ASK-16 | **74 % of `spring-audit`'s wall clock is outside the instrumented phase.** Clean CPU, all layers warm, writing to a file to bypass ceiling estimation: wall 9,4 s against `analysis_time_ms: 2 454` — 26 % of the clock. `symbols_analyzed` identical across 5.8.0/5.8.1/5.8.2 (30 053), output grew 8–10 % while time doubled, and stdout-vs-file is only ~1,6 s, so serialisation, population, cache state and ceiling estimation are all **excluded as causes by measurement**. ~5,4 s unattributed. The command recovered 7 of the 10,4 s it lost in 5.8.1 and sits **+45 % over its 5.7.2 best** (7,6 s → 11,0 s). | 5.8.1 → 5.8.2 corpus re-audits | High | **closed 5.8.3 by measurement** (`6fd8001`) — `perf.PhaseTimings` is always on and always published: `metadata.timings` gives the per-phase split, the measured total and `unaccounted_ms`, published rather than distributed over the phases. **The missing 74 % is the CIR build.** BroadleafCommerce, 2 985 files: cold wall 9 690 ms — `cir_build` 8 679 (90 %), `security_rules` 732, `semantic_model` 63, `tx_rules` 53, unaccounted 163 (1,7 %); warm wall 2 133 ms — `cir_build` 1 118, `security_rules` 735, unaccounted 164 (7,7 %). So `analysis_time_ms: 2 454` was never the analysis, it was the security rule pass alone, and the optimisation target is the parse, not the rules. The auditor's ordering — instrument before tuning — was right, and it changed the answer |
|
|
61
|
-
| R2 | **WebFlux functional routing is not modelled.**
|
|
62
|
-
| C3-127 | **`validation` is the only command with sustained monotonic degradation.** Steady-state, three consecutive versions worsening: 17 268 → 18 273 → 21 337 ms, **+23,6 % over the record**, while every other command returned to record level or better after the cache converged. `ASK_PROGRESS=1` attributes 100 % of it to one phase, `mapping validation surface`. | 5.8.2 re-audit | Medium | **open, premise corrected by measurement (5.8.3, `e57f3e7`)
|
|
63
|
-
| P1-proc | **A performance regression battery still does not exist, and the sixth request now arrives with a validated protocol.** `R6` (the parse cache never converging, so timings were irreproducible) was a real defect, was fixed well in 5.8.2 — dispersion between consecutive warm runs fell from 2,81× to a median of 1,04×, and `cache status` no longer prints its `over by N MB` line — and **was found only because an auditor happened to be measuring**. The same auditor then reported a 43 % regression that did not exist, and retracted it: a 2-run protocol produces artefacts up to **2,8×**. | 5.5.6 → 5.8.2, sixth request | Medium — process, not code | **
|
|
61
|
+
| R2 | **WebFlux functional routing is not modelled.** Eighth consecutive cycle. `ask endpoints halo` returns `total: 0` over a CMS with a full REST API: `functional_routing: {files: 58, route_registrations: 183, modeled: false}`, `gaps: ["webflux_functional: 58 files"]`. Independently counted in the repository: 3 `@RestController` (all three are tests) against 89 files using `RouterFunctions`/`coRouter`/`SpringdocRouteBuilder`. | 5.6.1 → 5.8.5, every cycle | Medium — **the signalling is exemplary and carries no misreading risk**: the zero is declared with its cause, its count and a prose warning naming the failure mode. What is missing is the analysis, not the honesty | **closed 5.8.6 — modelled, and the cascade with it.** Paths are composed along the builder chain (`.path(…)`, `.nest(…)`, the predicate form `route(GET("/x"), handler)`, and Kotlin's `coRouter { }` / `"/x".nest { }`), so a route declared `/posts/{name}` under a nested prefix is published at the path it is served on — which is exactly what the old refusal existed to protect: emitting the bare literal would have been a path the app does not serve. Measured on halo at HEAD (1 349 `.java`, cloned for this): **`endpoints` 0 → 155**, 169 routes resolved across 53 files, `supported_styles.webflux_functional: true`. **The cascade closes too**, because the modelled routes enter `route_surface` in `build_repo_ir` and not only the `endpoints` command: `posture halo` and `spring-audit halo` go from 0 to **173 endpoints analysed**. ⚠ **What is still not resolved is published as unresolved rather than invented**: 98 of halo's 169 register a *relative* literal whose base is decided where the router is mounted (a group/version, a plugin base), and those carry `path_resolution: unresolved`, `confidence: medium` and the reason; the old whole-style `coverage.gaps` entry is replaced by two smaller and truer ones (routes whose base is elsewhere; registrations in a form this resolver does not model — 14 of 183 on halo). Candidate files come from the symbol table (a method returning a `RouterFunction`, or a file importing the functional-routing package), never a walk over the repository — the C3-127 rule applied where it was written. Vendor-agnostic: nothing branches on a framework's name, and the handler rule is B1's — a handler is a reference, a lambda or a call, never a literal. Regression `tests/test_webflux_functional_routes_r2.py`, 14 assertions; the two F-2 batteries that asserted `modeled: false` now assert the surface. Verified unchanged where there are no builders (BroadleafCommerce 272 endpoints / 299 route-surface entries with zero functional routes contributed, spring-petclinic 17). **History, kept because it took eight cycles**: open (8 cycles), and as of cycle 8 the only open defect that corpus held against the product — every other row it ever opened is closed, so this row alone is what its evaluator's 9,0 needs moved to reach the 9,5 they defined, and it is the only item in either backlog that requires **new analysis** rather than disclosure, process or optimisation. Counts unchanged on 5.8.5 (58 files, 183 route registrations, 3 `@RestController`, all three tests). Cascades: `posture`, `risk`, `data-exposure` and `spring-audit` all derive their population from `cir.endpoints`, so a repository at 0 endpoints makes four commands report an empty surface honestly and uselessly (`spring-audit halo` = 0 findings on a full REST CMS). Acceptance: `endpoints halo` returns a non-empty inventory agreeing with Halo's published API on a 20-route sample, `supported_styles.webflux_functional: true`, and path resolution composes the builder chain (`route().path("/apis/x").GET("/{name}", …)`). **The only open row that requires new analysis, and the one both audits name as the single item separating the product from a 9,5.** ⚠ **Re-measured unchanged in 5.8.4**: `functional_routing: {files: 58, route_registrations: 183, modeled: false}`, 3 `@RestController` (all three are tests) against 89 files using `RouterFunctions` / `coRouter` / `SpringdocRouteBuilder`, and the variants the resolution has to cover are named — `RouterFunctions.route()`, `coRouter` (Kotlin DSL), `SpringdocRouteBuilder`, and nested `nest()` — with the path composed along the builder chain (`route().path("/apis/content.halo.run/v1alpha1").nest(accept(JSON), b -> b.GET("/posts/{name}", handler::get))` resolving to `GET /apis/content.halo.run/v1alpha1/posts/{name}`) |
|
|
62
|
+
| C3-127 | **`validation` is the only command with sustained monotonic degradation.** Steady-state, three consecutive versions worsening: 17 268 → 18 273 → 21 337 ms, **+23,6 % over the record**, while every other command returned to record level or better after the cache converged. `ASK_PROGRESS=1` attributes 100 % of it to one phase, `mapping validation surface`. | 5.8.2 re-audit | Medium | **closed 5.8.6 — by refuting the premise the row carried, with the measurement inside the phase.** Instrumented on BroadleafCommerce: of the 10 126 ms the phase costs, **14,0 s is method extraction over 2 766 files** and the walk it feeds runs in **0,01 s** and finds nothing. So the cost was never the analysis: it was parsing a universe the walk cannot reach. The walk starts at parameters annotated as HTTP inputs (18 of 2 766 files on that repository) and resolves callees **by name**, and a file whose text does not contain that name followed by `(` cannot declare it — the same regex that finds methods says so, which makes the pruning an equivalence and not a heuristic, the shape of the C3-88 guard one level up. `_DemandParsedMethods` parses the seeds first and then only what the walk asks for, transitively; in the worst case that is every file, which is exactly the old cost and never more. Measured end to end through the CLI: **`risk` 13 396 → 5 722 ms (−57 %)**, the phase 10 126 → 526 ms, with findings, defects and every row byte-identical (only clocks and run ids move). Also identical on keycloak-config-cli, spring-petclinic and jobrunr — whose 12,7 s of parse for zero seeds the C3-88 comment already recorded. One extractor (`_methods_in_text`) serves both universes, the demand-driven one never publishes itself onto the CIR (a partial universe cached as the complete one is the failure the eager path's own comment warns about), and the C3-121 budget check still runs per file inside the parse. Regression `tests/test_http_input_walk_demand_parse_c3_127.py`, 9 assertions, including a positive chain asserted equal to the eager reference. **History:** open, premise corrected by measurement (5.8.3, `e57f3e7`). `risk` publishes `timings` now, and on BroadleafCommerce the two substrates this row is about are **2,6 s of 13,5 s (19 %)** — `indexing the validation surface` 2 103 ms, `resolving the conditional bean graph` 525 ms — while **75 % of the clock is `reading HTTP-input query sinks` (10 051 ms)**, a phase the row does not mention. Two consequences: the ~27 s figure is a property of the audited corpus, not of the command; and within a single `risk` invocation each substrate already runs exactly once, so the saving this row imagines is a **cross-command** cache — a feature with its own contract, not a patch. What stays open is the real one: the query-sink walk. ⚠ **The drift half resolved itself in 5.8.4, measured**: `validation` returns to **18 068 ms** on the audited corpus — −15,3 % against 5.8.2 and +4,6 % over its 5.7.2 record — so the monotonic series (17 268 → 18 273 → 21 337) is broken without the cross-command cache having been built, and the row is now only about the substrate. The corpus divergence is two-sided and stays: the same audit measures the two substrates at ~24 s inside `risk`'s 40 554 ms there (`posture` 5 842 ms and `validation` 18 068 ms as standalone commands, both announced as stages of `risk` by `ASK_PROGRESS=1`), against 19 % on BroadleafCommerce. A cross-command cache must therefore publish its measured saving per corpus rather than inherit the ~24 s figure. ⚠ **Re-profiled by the field on 5.8.5, and the reporter retires half of their own premise**: in `risk`'s trace at 43,7 s on the audited corpus, `resolving the conditional bean graph` **no longer appears as a stage at all** (*"o lo habéis cacheado ya, o cae por debajo del muestreo de 5 s. Retiro esa mitad de mi propuesta"*), which agrees with our own 525 ms measurement. What is left is one substrate and one hot spot: `indexing the validation surface` ~10 s of the 43,7 s (against `validation` standalone at 19,6 s), so a cross-command cache is worth ~−22 % of `risk` here and makes `validation` free once `risk` has run; and **`composing risk factors (reading HTTP-input query sinks)` at ~15 s of 43,7 s (34 %)**, unchanged since round 4 and the same phase our BroadleafCommerce run puts at 75 % — **two corpora now name the same walk as the largest single cost in the CLI**, which makes it the profiling target with the best return and outranks the cache. The parallel lever above both stays `C3-84`: the rule pass is serial on a 20-core host |
|
|
63
|
+
| P1-proc | **A performance regression battery still does not exist, and the sixth request now arrives with a validated protocol.** `R6` (the parse cache never converging, so timings were irreproducible) was a real defect, was fixed well in 5.8.2 — dispersion between consecutive warm runs fell from 2,81× to a median of 1,04×, and `cache status` no longer prints its `over by N MB` line — and **was found only because an auditor happened to be measuring**. The same auditor then reported a 43 % regression that did not exist, and retracted it: a 2-run protocol produces artefacts up to **2,8×**. | 5.5.6 → 5.8.2, sixth request | Medium — process, not code | **closed 5.8.6 — the harness ships, and the protocol with it.** `perf.steady_verdict` (six runs, not four; an unconverged sample publishes **no** figure), `perf.contention_verdict` (the control command interleaved: `clean` / `host` / `command`, and only the last is about the product), `perf.host_verdict` (another `ask` process disqualifies the host before anything is measured) and `perf.measurement_protocol()`, with `scripts/perf_harness.py --steady` running them — warm-up discarded, control interleaved between every target pass, both cache bases isolated (E-37) and `ASK_PARSE_CACHE_MAX_MB` pinned. It exits non-zero when a cell did not converge **or** when the session is not attributable to the build. The field's three sessions are replayed as data the way `ANCHOR_MULTIPLE_VALIDATION` replays the six measured releases — the 5.8.5 round-10 session (control 1,29× beside target 1,25×) must classify as `host`, round 9 as `clean`, and a held control beside a moving target as `command` — so a later change to either threshold that stops classifying them correctly fails in the battery instead of in a round. `docs/perf/REGRESSION-GATE.md` §5.1 publishes the protocol. Regression `tests/test_measurement_protocol_p1proc.py`, 15 assertions. **History:** open — 5.8.2 shipped the *assertions* (`P1 / F-BR`: absolute ceilings and `max(sample) < 8 × posture_warm`, whose validation table the audit independently reproduces and extends with 5.8.2 at 6,8× ✅). What is still missing is the **harness that runs them**, and the audit supplies the missing half — a measurement protocol this ledger should treat as binding: `wall_steady()` = one discarded warm-up, then **4 runs**, report the **minimum**, and fail the measurement itself when `max/min ≥ 1,25` because at that point the cache is still evicting and no number is comparable. **Two independent corpora now report the same confounder**: 5 of 7 false positives in one audit and 4 of 7 in the other came from cache state or CPU contention — a competing `ask` process took `spring-audit` from 11,0 s to 71,4 s (+549 %), and `analysis_time_ms` inherits the bias (2 454 → 8 109 ms), so it is not an independent metric either. Acceptance: the battery runs `wall_steady`, asserts the reproducibility gate first, and no performance figure enters this ledger without a clean-CPU check. ⚠ **The cheap half closed 5.8.4, and the cause was not `endpoints`**: it was instrumented all along — one `Progress()`, started and finished — but a phase was announced by the *heartbeat*, which only fires once the interval has elapsed, so a run that finished inside 5 s printed nothing and an instrumented fast command was indistinguishable from an uninstrumented one. The counts the audit reported (posture 1 · spring-audit 2 · validation 3 · risk 5) were measuring how slow each command was, not how well it reports. Entering a phase is now announced whether or not the interval has passed, at `start()` and at every `update()`; the `_last_emit` stamp is still taken there, so the timed loop waits a full interval behind it and the two cannot double-print, and a counted stage is still rate-limited (asserted). `endpoints` also names the snapshot write as its own phase rather than charging that time to the scan. Measured: `endpoints` **0 → 2** phase lines on a 3-file repository, and on `tutorials` the first line arrives at `elapsed=0.0s` instead of after five seconds of silence. Battery `tests/test_phase_boundaries_are_announced_p1proc.py`, 8 assertions. ⚠ **Seventh request, 5.8.4, and the reproducibility gate now passes on everything measured**: `posture` 1,02× · `impact-chain` 1,05× · `impact` 1,08× (1,56× in 5.8.2) · `spring-audit` / `endpoints` / `validation` 1,09× · `risk` 1,23× — seven of seven under the 1,25× gate for the first time, with `cache status` at 293,65 MB of a 512 MB budget and no `over by` line. The absolute ceilings the harness should assert, from 5.8.4 steady state on the audited corpus (3 342 `.java`): `impact` < 5 000 ms (measured 4 192) · `endpoints -o f` < 6 000 (5 166) · `posture` < 6 500 (5 842) · `impact-chain` < 7 000 (6 460) · `spring-audit -o f` < 11 000 (9 716) · `validation -o f` < 19 000 (18 068) · `risk -o f` < 42 000 (40 554), plus the short-circuit `verify-edit` on a genuinely clean tree < 6 000 (4 848). The reuse assertion stays the **v3** formulation — `max(sample) < 8 × posture_warm` — whose validation table extends with 5.8.4 at 6,8 ✅; the two earlier formulations are recorded here as invalid so nobody re-derives them: extremes-only (`samples[0]/samples[-1] > 1.5`) passes 5.6.1 at 2,95 with reuse almost gone, and flatness (`median/min < 1.5`) passes 5.5.5 at 1,00 with reuse broken, because flat at 80 s and flat at 30 s score identically. Population and staleness assertions to carry with them: `symbols_analyzed == PREVIOUS or "symbols_excluded" in metadata`, and no `unchanged_for` line in the progress output. What is still missing is only the harness that runs them. ⚠ **Seventh request, and the protocol is amended by its own author after a ninth false positive — caught before it was reported, which is the point.** Four amendments, all binding here: **(1) four runs are not enough.** A transient survived four passes and died on six: `endpoints` measured 21–28 s (a reported +141 %/+250 %) against a steady state of 7,7–8,0 s, and `spring-audit` 22,9 s against 9,2 s. The minimum is now **6 runs to convergence**, minimum reported. **(2) A control command is mandatory, and it is the piece that was missing for six rounds.** Interleave a cheap, stable command with the expensive one in the same session — `impact <Class> .` (~4,2 s, the most stable in the CLI) against `risk . -o f` — and fail the measurement if the *cheap* one disperses: `assert dispersion(control) < 1.15`. This is what stopped round 10 from reporting a 5.8.5 regression: `spring-audit` at 1,39× and `risk` at 1,49× (both over the 1,25× gate, where all seven were under it in 5.8.4) sat beside a control that dispersed 1,29× when it had measured 1,08× in the same round — proportional movement is the signature of host contention, not of a degraded command. **(3) Host hygiene is asserted, not assumed**: `ask` processes = 0 before measuring (cross-session contention has produced +300 % to +549 % in this ledger), and the round-10 host was carrying 404 active processes. **(4) Isolate *both* cache bases (`E-37`) and pin `ASK_PARSE_CACHE_MAX_MB`; read `metadata.timings`, never `analysis_time_ms`**, which inherits the contention bias it is being used to detect. The one figure round 10 leaves unresolved — that dispersion — is explicitly **not attributed to 5.8.5** and is exactly what this harness, run on a verified-idle host, settles in a single run instead of a round of argument. Of the reporter's nine retired false positives, **eight are of one family (cache state or contention)**, which is the strongest argument this row has ever carried |
|
|
64
|
+
| ASK-17 | **The parse store's default budget cannot hold a multi-repository workspace warm, and the cold cost of the largest repository is 7,6 minutes.** Measured on the 8-repo corpus (43 986 `.java`): `cache status` reports 56 730 entries at **511,12 MB against a 512 MB budget** — i.e. permanently sweeping by LRU — so `tutorials` (24 073 `.java`, the largest contributor) is the first candidate for eviction. Cold `--compact` on it: **457,6 s**, independently reproduced at 449 s, against 2,0 s on the second pass. | 5.8.4 corpus re-audit | Medium — warm plus `--compact` is still the answer (2,0 s), but a workspace this size cannot keep every repository warm at the default budget, and nothing tells the caller which repository is cold before it pays for it | **closed 5.8.5 — the disclosure shipped and the owed measurement taken, under our own control.** ⚠ **The +66 % does not reproduce, and the mechanism the row suspected is worth ~3 %, not 66 %.** Protocol: `tutorials` (24 073 `.java`), root `--compact`, both cache bases isolated **and emptied between runs** (`SOURCECODE_CONTEXT_CACHE_DIR` *and* `SOURCECODE_CACHE_DIR` — the first attempt isolated only the first, and the second pass of each version answered from the L2 view its own first pass had written: 2,4 s with an empty parse store, which is how a measurement of a cold path becomes a measurement of a warm one), `ASK_PARSE_CACHE_MAX_MB` fixed at 4 096 MB so no sweep can confound the comparison, 2 passes per version. **5.7.2 (`e803c8b`): 112,0 s / 112,6 s. This tree: 113,8 s / 115,2 s — +2,3 %**, with per-version dispersion of 1,005× and 1,012× and a payload 2,2 % larger. Then the audit's own condition, isolated as the only variable — the store pre-filled to 489 MB against the **default** 512 MB budget, so every 32 MB written sweeps: **117,0 s, +2,7 %.** So the LRU sweep is not where 457,6 s comes from, and neither is the analysis path: the auditor was right to hold the regression, and the held figure is now retracted **with a number** rather than on suspicion. ⚠ Scope, stated because the difference is unexplained rather than explained away: 457,6 s on Windows 11 / pipx is **not reproduced here** (113 s on 20 cores), and that gap is not assertable in either direction from this measurement — what is assertable is that 5.7.2 → this tree did not get slower and that a saturated store costs ~3 %. ✅ **What the measurement does confirm is the row's own claim, with our number: one repository of 24 073 `.java` leaves 46 840 entries and 470 MB in the store — 92 % of the 512 MB default budget** — so a workspace with a second repository of any size is permanently sweeping by construction, exactly as reported. Sizing rule, measured rather than guessed: ~20 KB of store per Java file, so ~500 MB per 24 000-file repository. Collateral confirmation of `AS-18`: after the saturated run the store rests at 669 MB — 157 MB over the budget and **under** the 736 MB effective ceiling it publishes at 7 writers — so the ceiling holds under the condition that produced the complaint. **The disclosure half:** `cache status` now publishes **coverage per repository**, which is the half the row itself recommended and the half that changes a decision: *«tutorials: 3 100 of 24 073 files cached (13 %)»* replaces a blind guess about whether to raise the budget, and an LRU eviction becomes visible **before** somebody pays 457,6 s to discover it. Both halves of the attribution were already held and thrown away — the walk knows the repository and it computes the store key for every file — so the run records the pairs (`parse_cache.record_repository_index`, from **both** readers of the store: `build_repo_ir` and the route-surface extractor, so the figure does not depend on which command was typed) and `store_stats` intersects them with the keys it collects **in the entry walk it already performs**: coverage costs an intersection, never a second scan of the store and never a scan of the repository. Three rules keep it honest. The index is **merged, never replaced**, because a `--changed-only` or `--since` run would otherwise shrink a 24 000-file population to the twelve files it read and publish *«12 of 12 cached (100 %)»* about a repository that is cold. The number travels with its **basis** — a file deleted since its last analysis still counts as recorded and reads as uncached, which errs toward *colder than it is* and says so. And the index is **neither an entry nor evictable**: the entry walk, the byte accounting and the LRU sweep all glob `*.json`, so its bytes are published under their own name (`repository_index_bytes`) rather than folded into a total that means entries — an index swept away with the entries it describes cannot report the eviction, which is the one moment it exists for. It lives inside the generation root, so retiring a generation retires its indexes with it: the keys are only readable by the build that wrote them. Regression `tests/test_parse_store_repository_coverage_ask17.py`, 11 assertions, including the one the row is about — entries deleted underneath a recorded repository make coverage **fall** while the population holds. ⚠ **Still owed, and unchanged:** the controlled cold measurement (5.7.2 against this tree, `ASK_PARSE_CACHE_MAX_MB` fixed, store emptied between runs, on a >20 000-file repository). Until it exists neither the +66 % nor its absence is assertable, and the row stays open on that half alone. **History:** **open** — ⚠ **not filed as a regression, deliberately**: the same cold figure was 274,8 s in 5.7.2 (+66 %), and the auditor refuses to call it one because the conditions are not comparable — in 5.7.2 the store had been retired by a version change, here it was mid-LRU-sweep. Seven false positives of exactly this class have been retracted over seven cycles; this is the eighth candidate and it is being held. What is owed is a measurement **we** control: cold `--compact` on a >20 000-file repository, 5.7.2 against 5.8.4, with `ASK_PARSE_CACHE_MAX_MB` fixed and the store emptied between runs — until that exists, neither the +66 % nor its absence is assertable. Recommended beside it, and cheap because both halves already exist: `cache status` should publish **coverage per repository** — how many entries belong to each analysed repository and what fraction of its files are covered — so *tutorials: 3 100 of 24 073 files cached (13 %)* replaces a blind decision about whether to raise the budget. Entries are content-addressed and the walk knows the repository, so this is a projection of facts we hold, not new analysis. **Refutation reproduced independently in cycle 8, under the reporter's own protocol**: both stores isolated and emptied, `ASK_PARSE_CACHE_MAX_MB=4096`, 2 passes per version — 5.7.2 at 112,0 / 112,6 s against 5.8.5 at 113,8 / 115,2 s (~3 %), and an A/B of the budget itself (512 MB sweeping by LRU against 4 096 MB that cannot sweep) at 9,1–10,1 s against 9,0–9,7 s: **the LRU sweep costs nothing measurable**. The +66 % is retired as the reporter's eighth false positive, with `E-37` named as its confounder. |
|
|
65
|
+
| C3-128 | **`timeline` is the only gate-shaped command above 120 s, and it has not come back to its record.** Steady state, 4 runs, audited corpus: `timeline --since HEAD~5` costs **198 469 ms**, +27,3 % over its 5.7.2 record of 155 950 ms. The two other commands over 120 s are there structurally — `delta` (234 s) and `contract-diff` (136 s) analyse two whole trees — while `timeline` analyses **five** and costs less than `delta` does with two, so its cost is not explained by the number of trees it walks. | 5.8.2 → 5.8.4 re-audits | Low-Medium — an investigation command rather than a gate, and the last performance figure of the round sitting outside its own best | **closed 5.8.5 by measurement — the clock is attributed, and there is nothing left unaccounted to tune against.** The row's own instruction was ASK-16's: instrument before tuning. `timeline` published a per-sample total over what is really four costs — materialising a tree, measuring each watched metric, releasing the tree, and the remainder — so *«198 469 ms»* named a command rather than a phase. `perf.PhaseTimings` (the ASK-16 authority, always on) now splits it, with the phase names taken from `--watch` so the split cannot drift from the population, and the tree materialisation kept as its own phase because git's work must not be attributed to an analysis. **Measured, BroadleafCommerce (2 985 `.java`), `--since HEAD~5 --watch posture`, 5 samples: wall 38 921 ms — `measure:posture` 32 273 (82,9 %), `materialise_tree` 5 376 (13,8 %, ~1 075 ms per tree), `release_tree` 1 272 (3,3 %), unaccounted 0,45 ms (0,0 %).** So the answer to the row's premise — *«it analyses five trees and costs less than `delta` does with two»* — is that five sixths of the cost **is** the analysis, re-run per tree by construction, and the git work is a sixth of it: `timeline` is N × one analysis and there is no timeline-specific overhead to remove. Any future gain belongs to the metric being sampled (`C3-84`, `C3-127`), which is where it would also help every other command, and the payload now says so per run instead of per audit. Regression `tests/test_timeline_timings_c3_128.py`, 9 assertions, including that the series itself is byte-identical across two runs once the clock readings are removed — instrumentation that moved an answer would be a worse defect than the row. **Original note:** **open** — ⚠ the cache-reuse half of `B7` must **not** be reopened on this evidence: the v3 assertion (`max(sample) < 8 × posture_warm`) passes at 6,8 in both 5.8.2 and 5.8.4, against 14,8 / 13,4 / 12,4 / 8,5 in the four versions that genuinely failed it. What has not returned is the absolute cost. Attribution comes before tuning and is now cheap: `metadata.timings` (`ASK-16`) exists, so the per-phase split across the five trees can be published before anything is changed. |
|
|
66
|
+
| B20 | **Thirteen declared renames with no cut-off date, and two distinct `1.0` identifiers meanwhile.** The registry publishes `pending_renames` for the 13 non-conforming `schema_version` values with their canonical name — exactly the policy `ASK-11` exists to enforce: the rename is an incompatible change, declared before it is made, never applied in silence. The consequence is published by the registry itself as `ambiguous_identifiers: 1` — `spring-audit` emits `1.0` for `core-analysis-v1` and `impact-chain` emits `1.0` for `impact-chain-v1`, so a consumer dispatching on the emitted version cannot tell them apart. | 5.8.4 re-audit (residual of `B19` / `B6`) | Low — declared debt rather than a defect, and the audit says so in as many words | **closed 5.8.5 — the window is declared where every other incompatible change is, and the reported ambiguity was understated by five shapes.** `BC-002` in `sourcecode.breaking_changes`: **announced 5.8.5, takes effect 6.0.0**, printed by `ask schema breaking-changes-v1`, carried in the CHANGELOG's `## Upgrading` section above the release history, and projected into `ask schema schemas-v1` as `pending_renames_window` — **one fact, two surfaces**, with a structural assertion that the registry source holds no second copy of the date. The registry's policy is generalised rather than loosened: a declared change is now `kind: exit_code` (must move an exit code) **or** `kind: contract` (must move a **published value** *and* name the version it takes effect in), because a rename breaks a consumer with every exit code still 0 and a registry that only knew about exit codes had nowhere to put it. A contract change publishes no `exit_code_before`/`after` at all — inviting a reader to check a field that cannot move is how a disclosure becomes noise. The 13 affected shapes are **read from `schema_registry.canonical_migrations()` at call time**, never copied: a shape that starts conforming leaves the declaration by itself (asserted by swapping the registry for a conforming one and watching the list empty). ⚠ **Correction to the reported cause, measured**: the audit named *two* shapes spelling their version `1.0`; there are **seven** — `core-analysis-v1`, `impact-chain-v1`, `pr-impact-v1`, `migration-blast-v1`, `spring-impact-v1`, `event-topology-v1`, `test-gap-ranking-v1` — so `ambiguous_identifiers: 1` was counting one *identifier* over seven shapes, not two. `ask schema 1.0` already resolved to all seven with each canonical name; the count is now asserted from the registry so it cannot be quoted from prose again. The rename itself is deliberately **not** made early: that is the incompatible change this policy exists to prevent. Regression `tests/test_schema_rename_window_b20.py`, 11 assertions, plus the ASK-11 battery generalised to both kinds. **Original note:** **open** — the missing half is a date, not a decision. Announce the cut-off window in `breaking-changes-v1` with the target version, the way every other incompatible change is announced, so a consumer can pin `core-analysis-v1` today and know when the bare `1.0` stops being emitted. Until then `ask schema 1.0` resolving to all seven shapes, each offering its canonical name, is the correct behaviour and must not be *fixed* by renaming an emitted value early — that is the incompatible change this policy exists to prevent. ⚠ **Round 10 ran on the 5.8.5 build and still reports the renames as *«deuda bien declarada, pero sin fecha»*, asking for precisely what `BC-002` already ships.** The row stays closed — the window exists, is announced 5.8.5 / effective 6.0.0, and is printed by `ask schema breaking-changes-v1`, carried in the CHANGELOG's `## Upgrading` and projected into `schemas-v1` as `pending_renames_window`. What it leaves behind is a **discoverability check, not a defect**: the reporter quoted `pending_renames` and `counts` out of the registry payload and did not see the window beside them, so verify that `pending_renames_window` travels in the same payload those two keys do — and if it does not, that is where it belongs. A declaration a ten-round auditor cannot find is not yet declared to a consumer. |
|
|
67
|
+
| E-37 | **Two environment variables move three cache stores, and no surface said which moves which.** `SOURCECODE_CACHE_DIR` relocates the per-repository snapshot store (core snapshots, the rendered L2 view, the RIS); `SOURCECODE_CONTEXT_CACHE_DIR` relocates the shared ones (the Canonical IR and the per-file parse store). Their defaults are siblings under `~/.sourcecode`, so the split is not derivable from the paths, and `cache status` published a path per store with no variable beside it while `docs/CACHE.md` named both variables in one sentence after the table. | found here 2026-08-18 while taking the controlled A/B `ASK-17` owed, not reported by the field | Medium — it does not make an answer wrong, it makes a **measurement** wrong, silently: an operator who redirects or empties *the cache* moves one store and is served answers out of the other | **closed 5.8.5** — measured first: with only `SOURCECODE_CONTEXT_CACHE_DIR` redirected, the second cold pass of each version answered from the L2 view its own first pass had written — **2,4 s against 113 s, with an empty parse store** — and the run reported `cache_source: L2_view` without anything naming the store it came out of. Now every store in `cache status` publishes `base_env`, from one authority (`cache._STORE_BASE_ENV`), the text answer prints *«<path> ($VAR moves it)»* beside each one, `stores.base_env_note` states the consequence rather than the layout, and `docs/CACHE.md` carries a **column** instead of a sentence covering both. Regression `tests/test_cache_store_base_env_e37.py`, 8 assertions, and the ones that matter are **behavioural**: setting the variable a store names moves *that* store and leaves the others where they were — a published mapping nobody exercises is the class of claim this repository refuses everywhere else. The CLI is forbidden a second copy of the mapping (structural assertion). **Confirmed from the outside in cycle 8, by the reporter falling into it first**: isolating only `SOURCECODE_CONTEXT_CACHE_DIR`, their second *cold* pass answered from the L2 view their first pass had written — **2,4 s against 113 s with an empty parse store** — and their write-up now quotes the shipped `base_env` mapping back as the fix (*"dos variables mueven tres almacenes y ninguna superficie decía cuál"*). A defect found while measuring, that was itself corrupting the measurement. |
|
|
68
|
+
| B21 | **Six of the thirteen shapes being renamed name no command that emits them.** `schemas-v1` publishes `emitted_by` per shape; for `migration-blast-v1`, `spring-impact-v1`, `event-topology-v1`, `test-gap-ranking-v1`, `canonical-ir-v1` and `hibernate-strategy-v2` it is `[]`, beside seven shapes where it is populated. Read the way a consumer reads a list, an empty `emitted_by` says *nothing emits this shape* — which tells the one consumer that **is** affected by `BC-002` that it is not. | found here while declaring `BC-002` (5.8.5), not reported by the field | Low — a disclosure gap in a declaration whose whole purpose is to let a consumer decide whether it is affected | **closed 5.8.6 — derived, not typed out.** `schema_producers` reads the two authorities the CLI already holds — the registered command tree and the import graph of the module that defines each callback — and answers with the evidence it derived on: `command_import` (the command's own body imports the producing module), `module_import` (one hop, through an intermediate narrow enough to attribute), `substrate` (reached from more commands than a producer would be) or `unresolved` (nothing found, said in a sentence). Five of the six now name a command — `migration-blast-v1` → `migrate-check`, `spring-impact-v1` and `event-topology-v1` → `impact-chain`, `test-gap-ranking-v1` → `impact`, `hibernate-strategy-v2` → `migrate-check`/`migrate-recipe` — and the sixth, `canonical-ir-v1`, is reached from **28 modules**: naming every command would be less true than naming none, so it publishes `substrate` with the count and the instruction to match on `subject`. A declared producer still wins, because some producers are not one registered command (`ask (root)`, `baseline capture`). Regression `tests/test_schema_producers_derived_b21.py`, 33 assertions, parametrised by the registry so a fourteenth unnamed shape fails the battery. **History:** open, and named rather than shipped as a fact: every `BC-002` row with an empty producer list carries `emitted_by_basis` — *«not declared in the registry: this shape's producing command is not named yet, so match on `subject`»* — so the gap is visible instead of being read as a negative claim. What remains is to fill the six, and the honest way is derivation rather than six more hand-written strings: the producer is discoverable from the command that constructs each payload, which is the same authority every other population in this repository comes from. Until then the assertion in `tests/test_schema_rename_window_b20.py` holds the weaker invariant a reader can rely on: a row either names its emitters or says why it cannot. |
|
|
69
|
+
| C3-129 | **`timeline`'s absolute cost, now fully attributed and still 27 % above its own record.** `C3-128` closed by instrumenting rather than tuning, which was the right order and left this behind: the clock is split, nothing is unaccounted, and the number has not moved. Round 10 on 5.8.5 measures `timeline --since HEAD~5` at **~198 000 ms against the 5.7.2 record of 155 950 ms (+27 %)** — the only gate-shaped command over 120 s that is not there structurally (`delta` 191 s and `contract-diff` 115 s walk two whole trees, `compare` 46 s walks N candidates; `timeline` walks **five** trees and costs more than `delta` does with two). | 5.8.2 → 5.8.5 re-audits (residual of `C3-128`, which closed the attribution half in 5.8.5) | Low-Medium — an investigation command rather than a CI gate, and the last performance figure of the round sitting outside its own best | **closed 5.8.6 — by not paying twice for what does not change.** A metric at a commit is a pure function of that tree, and the tree at a sha never changes: the only cache in this product whose key can be exact rather than heuristic (analyzer fingerprint + metric + commit sha). `timeline_cache.SampleCache` stores measured values under the per-repository core store, and when every watched metric for a sample is a hit the tree is **never materialised**, which is the 13,8 % the split attributes to git. Measured on BroadleafCommerce over the same range: **26 845 ms → 12 ms**, samples and transitions byte-identical. What makes it admissible rather than merely fast, all asserted: a failed sample is never stored (one bad run cannot become permanent), the fingerprint is in the key (corrected analysis is never served from a previous release), the hit is published (`sample_cache`, `from_cache` per sample, and `cost` separates the cached population from the measured one instead of reporting a median over both), and `--no-cache` / `ASK_TIMELINE_NO_SAMPLE_CACHE=1` measure everything again so the command stays measurable without emptying a store (E-37). `cache_model` carries the layer, so `cache model` and `docs/CACHE.md` state it from one authority. Regression `tests/test_timeline_sample_cache_c3_129.py`, 11 assertions. **History:** open — and it opened with the answer already published, which is why it is cheap to attack and expensive to leave: `timeline`'s own `timings` say **82,9 % `measure:posture`, 13,8 % `materialise_tree`, 3,3 % `release_tree`, 0,0 % unattributed**. So this is not a `timeline` defect at all: it is `posture` paid five times over five materialised trees, and it makes the same substrate question `C3-127` asks — with the difference that here the five payers are one command, so the cache is intra-invocation and needs no cross-command contract. ⚠ **Do not reopen `B7`**: cross-tree cache reuse is healthy, the v3 assertion passes at 6,8×. Acceptance: `timeline --since HEAD~5` back inside +10 % of 155 950 ms on the audited corpus with `timings` still summing to 100 %, and `measure:posture` falling as a share rather than the total falling for an unnamed reason. |
|
|
64
70
|
|
|
65
71
|
---
|
|
66
72
|
|
|
@@ -239,7 +245,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
239
245
|
| C3-81 | **A family the budget cut in flight publishes `units_done == units_total`, so its own entry says it finished.** Fifth round on saint-server, verbatim from 4.14.0: `rules_partially_run: [{"rule_ids": ["SEC-004","SEC-005","SEC-006","SEC-007","SEC-008","DEAD-001"], "units_done": 3342, "units_total": 3342, "unit": "java files"}]` — a complete population, in a list whose meaning is *"this family did not finish"*. It is not a display artefact: `DEAD-001` yields **248** findings against **254** on the complete run, so six findings really are missing. The cause is that the family walks **four populations in sequence** (`security_config_scan`: Java sources, then configuration files, then deployment descriptors, then MyBatis mappers) and `FamilyBudget` records the counts of the stage it happened to be in. The deadline fell on the **last** Java file, so the entry names the one population that did finish and says nothing about the three that never started — the same shape as C1-42 one level down: a container standing for its members | 4.14.0 (eval #18) | Low — the numbers beside it are honest (`partial`, `counts_are_floor`, `confidence: low`), and this is the one field a reader consults to decide *how much* more time to give the run | **closed 4.15.0** — the unit is the stage. Every walk a family makes now ticks with what it has **finished** and names itself (`scanning java sources`, `scanning configuration files`, `scanning deployment descriptors`, `scanning mapper files`, `walking endpoints`, `resolving gate annotations`, and on the TX side the declared-boundary and method-body walks), so a stage in flight can never report its own population complete — the literal field contradiction is asserted unreachable at every cut point, not at one. The entry carries `stage_stopped_in` and a `units_basis` stating that the two numbers count the stage rather than the family, and the `PARTIAL:` sentence a human reads carries the same fact as the payload. Off-by-one was the second half and the invisible one: the counter reported the unit it was *about to* process, so a walk stopped at its own last file published a finished population |
|
|
240
246
|
| C3-82 | **The heartbeat is silent for 13,6 minutes *between* batch boundaries, and the batch is the sampling constant a different row chose.** C3-80 made the work emit in-band and the field confirms the mechanism arrived — the line now carries the family, a per-file count and an ETA. It also measures what it did not fix: `elapsed=5.0s stage=linking 3206/3342` · `elapsed=10.0s stage=security rules (SEC-004) 1856/3342 java files` · **13,6 minutes of nothing** · `elapsed=13.6m … 3328/3342 eta=3.4s` · `done (13.6m)`. The emitter is reached only from `RulePassProgress.tick()`, which returns early on `done % sample_every` — so between two boundaries 64 units apart nothing can print, and a stretch of expensive units (or one pathological file, which the 4 files/s average implies) is invisible for as long as it lasts. The 39-minute hole became a 13,6-minute hole because the *work* got shorter (C3-79), not because the emission got denser | 4.14.0 (eval #18, partial since 4.10.7) | Medium — fifth version of the same question (*"is it working or is it hung?"*) going unanswered on the most expensive command, now with the content already correct | **closed 4.15.0** — one number was doing two jobs, and now each has its own. The **clock** is still read once per `sample_every` units, which is C3-79's reason unchanged and what took the overshoot from 5,66× to 1,94×; the **counter** reports on every unit, because `Progress` already throttles emission on an interval, so a per-unit report costs one clock read and produces a line only when one is due. Asserted both ways: a thousand cheap units print at most one line, and five units that each outlast the interval print five — the shape the field watched go silent |
|
|
241
247
|
| C3-83 | **A budget inherited from the environment truncates the audit and nothing in the answer says where the limit came from.** Measured both ways on the same build: without `ASK_MAX_ANALYSIS_SECONDS`, **395 findings / 98 defects**; with `=420`, **267 / 11** — GATE-001's 82 gate-bypass defects among what is missing. Everything about the truncation is declared impeccably (`_partial`, `summary.partial`, `counts_are_floor`, `confidence_level: low` with its basis — C2-31 and C3-71 closed that), and the residue is provenance: the variable is inherited from a shell or a runner, so the person reading `total_defects: 11` is often not the person who set it, and nothing in the payload names it. *"Quien fije la variable en CI y solo lea `total_defects` verá 11 donde hay 98"* | 4.14.0 (eval #18) | Medium — a configuration trap rather than a false claim, and the trap is on the axis (audit completeness) the product is bought for | **provenance half closed 4.15.0; the contract halves stay F-AV.** The budget carries its origin verbatim (`ASK_MAX_ANALYSIS_SECONDS=420 (process environment)`) from the place it is read to the places it is published: under the class floor the measured warning C3-72 shipped gains a line naming the variable and its value, over the floor — where there is no advice to give — a single provenance line replaces silence and says what a run that hits the limit publishes and how to get a complete audit, and `_partial` carries `budget_source` beside `budget_seconds`. A value that does not parse is not a budget and invents no source, and a budget that was not inherited publishes no origin. Verified end to end: `spring-audit` under a 0,01 s budget publishes `budget_source` in `_partial`. The exit code and any renaming of a truncated total remain **F-AV** |
|
|
242
|
-
| C3-84 | **`--jobs` parallelises the phase that is not the cost, and the help implies otherwise.** The evaluator corrected their own four-round diagnosis this round (*"reporté 'sin paralelismo'. Es inexacto"*): `--jobs` / `ASK_JOBS` exist and default to `cpu_count()-1` (=19 there). What the OS sampling shows is which phase they cover — `t=7,6s threads=4` during parsing, `t=441,1s threads=1 CPU/wall 0,97` for everything after — matching the run's own stages exactly: `linking` ~10 s parallel, `security rules … 3342 java files` ~13,6 min serial. On a **warm** cache, which is the normal case and the one the field measures, there is nothing left to parse, so `--jobs` buys nothing at any value while 19 workers sit idle. The flag's help is honest about *what* it parallelises (*"Parallelism only warms the content-addressed parse cache — the answer is byte-identical at any value"*) and says nothing about the phase that holds ~99 % of a repo-wide run | 4.14.0 (eval #18, fifth round on the cost) | Medium as a claim, **Critical as a capability** — it is the single item the field has priced the CI verdict on for five rounds: *"con 16 workers, `spring-audit` bajaría de 13,6 min a ~1 min y este producto pasaría a otra categoría"* | **claim half closed 4.15.0; the capability is F-AT.** `JOBS_OPTION_HELP` — the single string every command that publishes the flag declares it with — now states that the rule pass is **not** parallelised and that a warm cache leaves the flag nothing to parse, with F-AQ's promise (byte-identical at any value) untouched. The README and the user guide are qualifications of that one claim rather than three versions of it, and the battery asserts every `--jobs` parameter in the registry carries it, so a second copy cannot keep the old sentence. The **capability** is **F-AT** (parallel rule evaluation, with the map-reduce shape the field spells out: per-file families map and reduce on the `(category, defect_kind, symbol)` identity `defect_identity` already publishes; graph-scope families such as GATE-001 stay in the serial tail) and does not start before this queue finishes |
|
|
248
|
+
| C3-84 | **`--jobs` parallelises the phase that is not the cost, and the help implies otherwise.** The evaluator corrected their own four-round diagnosis this round (*"reporté 'sin paralelismo'. Es inexacto"*): `--jobs` / `ASK_JOBS` exist and default to `cpu_count()-1` (=19 there). What the OS sampling shows is which phase they cover — `t=7,6s threads=4` during parsing, `t=441,1s threads=1 CPU/wall 0,97` for everything after — matching the run's own stages exactly: `linking` ~10 s parallel, `security rules … 3342 java files` ~13,6 min serial. On a **warm** cache, which is the normal case and the one the field measures, there is nothing left to parse, so `--jobs` buys nothing at any value while 19 workers sit idle. The flag's help is honest about *what* it parallelises (*"Parallelism only warms the content-addressed parse cache — the answer is byte-identical at any value"*) and says nothing about the phase that holds ~99 % of a repo-wide run | 4.14.0 (eval #18, fifth round on the cost) | Medium as a claim, **Critical as a capability** — it is the single item the field has priced the CI verdict on for five rounds: *"con 16 workers, `spring-audit` bajaría de 13,6 min a ~1 min y este producto pasaría a otra categoría"* | **claim half closed 4.15.0; the capability is F-AT.** `JOBS_OPTION_HELP` — the single string every command that publishes the flag declares it with — now states that the rule pass is **not** parallelised and that a warm cache leaves the flag nothing to parse, with F-AQ's promise (byte-identical at any value) untouched. The README and the user guide are qualifications of that one claim rather than three versions of it, and the battery asserts every `--jobs` parameter in the registry carries it, so a second copy cannot keep the old sentence. The **capability** is **F-AT** (parallel rule evaluation, with the map-reduce shape the field spells out: per-file families map and reduce on the `(category, defect_kind, symbol)` identity `defect_identity` already publishes; graph-scope families such as GATE-001 stay in the serial tail) and does not start before this queue finishes ⚠ **The remaining half — parallelise the rule pass — is refuted by measurement (5.8.6), and the figure is published so nobody re-derives it.** Instrumented with `metadata.timings` on BroadleafCommerce (2 766 `.java`): `cir_build` **8 711 ms of a 9 915 ms run (88 %)**, `security_rules` 740 ms (7,5 %), `tx_rules` 53 ms (0,5 %), `semantic_model` 61 ms, `waivers` 0,04 ms. **Perfect parallelism of every rule would save at most ~8 % of the clock**, not the *«mayor palanca absoluta»* two field rounds estimated — which is ASK-16's finding again, one level down: the cost is the build, not the rules. The lever that remains is the CIR build, and `--jobs` already parallelises the parse inside it. Kept open only as the *disclosure* half: the help still implies the flag parallelises the audit. |
|
|
243
249
|
| C3-69 | **Mojibake on the Windows console: we write correct UTF-8 bytes and the console decodes them in the ANSI codepage.** Field payload, verbatim: `"analysis_warnings": ["Self-referential exclusion: 1 member(s) ... â€" a class's own methods ..."]` — U+2014 arriving as `â€"`. The same corruption appeared in the baseline when read back from Python. Verified in source: the shared emit seam (`cli.py:1219`) writes `content.encode("utf-8")` to `sys.stdout.buffer`, so **our bytes are right**; every `json.dumps` in the package uses `ensure_ascii=False`. The defect is real to the consumer regardless of where it is produced: the JSON stops being valid UTF-8 *as received* | 4.10.4 (eval #14) | **Low** — it breaks downstream parsing, and the diagnosis matters more than the severity: this is the output-side mirror of C3-58, which was closed on the input side by letting bytes name the encoding instead of a locale | **closed (4.10.6), and not by escaping everything.** Encoding the stream harder cannot fix a *decoder*, so the first move is to fix the decoder: `SetConsoleOutputCP(65001)` at entry tells the Windows console to read UTF-8, and when it works nothing else changes — the payload keeps its em-dashes and accents on every platform, byte-identical to today. Only when that call fails does the emit seam escape the JSON to pure ASCII, which is the subset every codepage agrees on, so `\u2014` arrives intact and `json.loads` gives the em-dash back. The decision is made **once**, at entry, in `output_encoding.py`, and every emit seam reads it (`_serialize_dict`, `serializer.to_json`, the error and `_meta` envelopes) — no command can make a different one, and redirected output never involves a console codepage so it never takes the fallback. Original remedy note: the whole class dies with `ensure_ascii=True` on the output `json.dumps` (ASCII is a subset of every codepage the console might choose), at the cost of `\uXXXX` escapes in a payload the field also reads by eye. Alternative: set the console codepage on Windows at entry, and document it. Decide once, for every emit seam, and lock it in the battery |
|
|
244
250
|
| C3-85 | **The stretches between counted stages report nothing, and the frozen `n/n` left on screen reads as the stage still running.** Sixth round on saint-server, verbatim from 4.15.0: `elapsed=10.0s stage=security rules (SEC-004) 37/3342 java files`, then **14,5 minutes with no line at all**, then `elapsed=14.5m stage=security rules (SEC-004) 3340/3342 java files eta=0.5s`. The field reads this as the periodic emitter being dead and proposes re-arming it. **Measured here, the emitter is alive and the diagnosis is one seam further in.** A per-unit tick loop driven through the real seam (`FamilyBudget.tick` → `RulePassProgress.detail_sink` → `Progress.work`) emits one line per interval, exactly as designed: 100 units over 5,4 s at a 1 s interval produced **5 lines, evenly spaced**. What produces the silence is the stretches no counter covers. Instrumented on keycloak (5 486 files, warm, every `work`/`step`/`update` call timestamped): **8,2 s of a 24 s run — 34 % — pass between the last `linking` tick and the first rule tick**, with `linking 5486/5486` frozen on screen throughout; 2,7 s more inside `GATE-001` between two of its own ticks; and on a cold run **24,7 s after `parsing 50/5486`**. Scaled to the field's repository and its per-file cost, those are the minutes they watch | 4.15.0 (eval #19, sixth round on the emission) | Medium — it is the question *"is this working or is it hung"* on the command that costs the most, and the answer currently on screen is a count that finished | **closed 4.16.0** — every stage of an audit now either counts its population or names itself, and no run ends a counted pass without announcing what follows it. Named: the IR build's tail (`assembling the IR`, `recovering spec-declared routes`, `scanning XML security configuration`), the semantic-model build, the whole-tree walk *inside* the configuration family (through `FamilyBudget.stage`, so the line says which family it belongs to), the rule pass's dedup-and-order tail, and each of the five repository-wide walks `risk` performs before it composes. Inventing a denominator for a single walk is the fabrication C3-56 refused, so none is invented. Re-measured on keycloak after the fix: the 8,2 s that read `linking 5486/5486` now read `assembling the IR` and `scanning XML security configuration`, and NS-05 asserts the invariant directly — a build may not end on a completed counter. The naming is also what lets the *next* round say which walk the wall time is in, which is C3-88 |
|
|
245
251
|
| C3-86 | **A count that has not moved is re-emitted as live progress, with an ETA extrapolated across the stall.** The same field line is the evidence: `3340/3342 java files eta=0.5s`, printed **14,5 minutes** after the counter last advanced. Both halves are false claims of the kind this product does not otherwise make — the line asserts the stage is 99,9 % done *now*, and the ETA is a rate computed over a window in which nothing happened, so the longer the stall lasts the more imminent the finish looks. `_eta_seconds` divides the remaining units by `done / (now − stage_t0)`, and `now` keeps advancing while `done` does not | 4.15.0 (eval #19) | Medium — C2 in nature: presentation asserting something measurement did not establish. It is the one place in six rounds where the terminal states a fact the run cannot support | **closed 4.16.0** — a count that has not advanced for a whole interval publishes `unchanged_for=<duration>` and withholds the ETA, in both line mode and on the spinner. The staleness clock is deliberately **not** `_stage_t0`: that one answers *"when did this stage start"*, and conflating the two would mark a long, healthy, advancing stage as frozen — asserted. The count itself is still printed, because it is true and it is what a reader wants; what is withheld is the claim that it is *current*, and what is added is how long it has stood there. An uncounted stage is never called stale: it never claimed a denominator, which is C3-56's refusal to fabricate one |
|
sourcecode/_docs/USER_GUIDE.md
CHANGED
|
@@ -136,7 +136,7 @@ pipx install sourcecode # isolated install, no venv needed
|
|
|
136
136
|
|
|
137
137
|
# Verify
|
|
138
138
|
ask version
|
|
139
|
-
# ask 5.8.
|
|
139
|
+
# ask 5.8.6
|
|
140
140
|
```
|
|
141
141
|
|
|
142
142
|
Requires Python 3.9+.
|
|
@@ -177,7 +177,7 @@ estimate scaled by file count would be wrong in the direction that costs you a s
|
|
|
177
177
|
and an anchor printed without its release goes on recommending a nightly job for a command
|
|
178
178
|
that has come to finish in seconds (C3-97).
|
|
179
179
|
|
|
180
|
-
Thirty-
|
|
180
|
+
Thirty-nine commands and six command groups exist. Four of them carry most of the measured
|
|
181
181
|
value in field use, and they are the ones to learn first:
|
|
182
182
|
|
|
183
183
|
| Start with | Because |
|
|
@@ -197,15 +197,15 @@ listed in the README command table.
|
|
|
197
197
|
|
|
198
198
|
Every command carries a tier, printed by `ask --help` and listed per command in the README
|
|
199
199
|
table. A tier is a **stability promise — what an output is worth relying on** — and nothing
|
|
200
|
-
else: it is not a value ranking (`
|
|
201
|
-
command here), and it is not the [pricing tier](PRODUCT_TIERS.md), which gates
|
|
202
|
-
and never capability.
|
|
200
|
+
else: it is not a value ranking (`archetype` is experimental *and* answers a question no
|
|
201
|
+
other command here answers), and it is not the [pricing tier](PRODUCT_TIERS.md), which gates
|
|
202
|
+
repository size and never capability.
|
|
203
203
|
|
|
204
204
|
| Tier | Promise | Commands |
|
|
205
205
|
|---|---|---|
|
|
206
206
|
| **core** | contract stable within a major — safe to gate CI on | `endpoints` · `spring-audit` · `migrate-check` · `impact` · `impact-chain` · `pr-impact` · `verify` |
|
|
207
207
|
| **supported** | maintained; fields are added, never removed without a major | every command not named in another row |
|
|
208
|
-
| **experimental** | shape may change in a minor — do not gate CI on it | `
|
|
208
|
+
| **experimental** | shape may change in a minor — do not gate CI on it | `enrich` · `archetype` · `timeline` |
|
|
209
209
|
| **parked** | kept working, no longer developed | `retrieve` |
|
|
210
210
|
|
|
211
211
|
Read it as: a `core` command is one to put behind an exit code in a pipeline. An
|
|
@@ -360,7 +360,7 @@ a clean `spring-audit` is still not a clean security review. Pair it with a SAST
|
|
|
360
360
|
CodeQL) and a dependency scanner (Snyk, Trivy, Dependency-Check). See [posture.md](posture.md)
|
|
361
361
|
for what the security surface *is* authoritative about.
|
|
362
362
|
|
|
363
|
-
### `ask risk` *(
|
|
363
|
+
### `ask risk` *(supported)*
|
|
364
364
|
|
|
365
365
|
**The command to reach for when the question is "what do we fix first".** Every other command
|
|
366
366
|
answers one axis and leaves the composition to you. `risk` does the join:
|
|
@@ -475,7 +475,7 @@ Two things it will not do:
|
|
|
475
475
|
symbol) become one defect with several witnesses, and the composed sentence names whether
|
|
476
476
|
its input-path evidence is bounded flow or only adjacency.
|
|
477
477
|
|
|
478
|
-
### `ask audit-report` *(
|
|
478
|
+
### `ask audit-report` *(supported)*
|
|
479
479
|
|
|
480
480
|
Packages existing ASK evidence into a human audit deliverable. It reads `risk` and `posture`,
|
|
481
481
|
shows top composed risks and runtime access posture, embeds the source evidence, and repeats
|
|
@@ -580,7 +580,7 @@ It also adjudicates the routes that look like duplicates of each other —
|
|
|
580
580
|
`/v1/agrupadorGfh` beside `/v1/agrupador-gfh` — by who calls them: live aliases,
|
|
581
581
|
or one live route plus debris.
|
|
582
582
|
|
|
583
|
-
### `ask migrate-recipe` *(
|
|
583
|
+
### `ask migrate-recipe` *(supported)*
|
|
584
584
|
|
|
585
585
|
**The migration report, in the form the executor takes.** `migrate-check` already names
|
|
586
586
|
the OpenRewrite recipe each finding carries — 296 of 309 on openmrs-core — and a reader
|
|
@@ -617,7 +617,7 @@ cannot be reviewed as one change (on openmrs-core, including it takes the popula
|
|
|
617
617
|
— its own, and `migrate-check`'s narrower headline `blocking_count` — because two
|
|
618
618
|
populations under one name is the defect this project keeps finding in its own output.
|
|
619
619
|
|
|
620
|
-
### `ask data-exposure` *(
|
|
620
|
+
### `ask data-exposure` *(supported)*
|
|
621
621
|
|
|
622
622
|
**Which routes can carry the data you called sensitive, and who reaches them.** Nothing
|
|
623
623
|
here decides what is sensitive — that is a judgement about a domain, and a field named
|
|
@@ -886,6 +886,54 @@ Three properties make the series usable as evidence rather than as a chart:
|
|
|
886
886
|
|
|
887
887
|
---
|
|
888
888
|
|
|
889
|
+
### `ask selftest <repo>` *(supported)* — this product's defect ledger, run against your repository
|
|
890
|
+
|
|
891
|
+
```bash
|
|
892
|
+
ask selftest . # every row the ledger publishes
|
|
893
|
+
ask selftest . --rows ASK-09,R2 # the ones you care about
|
|
894
|
+
ask selftest . --entry-point /usr/local/bin/ask
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Every row in the published [defect ledger](DEFECT-LEDGER.md) carries an acceptance
|
|
898
|
+
criterion. Those criteria run here — against **your** repository, through the entry
|
|
899
|
+
point **you** invoke — and each verdict travels with its subject: the repository, its
|
|
900
|
+
size, the platform, and the CLI that answered.
|
|
901
|
+
|
|
902
|
+
A row whose precondition your repository does not meet answers `not_applicable`
|
|
903
|
+
**with the reason**, never `pass`. That rule is the whole point: `ASK-09` was declared
|
|
904
|
+
closed twice against an artefact while it reproduced in the field for five cycles, and
|
|
905
|
+
a green run over nothing is how that happens.
|
|
906
|
+
|
|
907
|
+
Nothing is written to the repository: every row runs a read-only command.
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
### `ask regress <before.json> <after.json>` *(supported)* — what moved, and what explains it
|
|
912
|
+
|
|
913
|
+
```bash
|
|
914
|
+
ask spring-audit . -o before.json # on the build you have
|
|
915
|
+
ask spring-audit . -o after.json # on the build you are considering
|
|
916
|
+
ask regress before.json after.json
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
Two payloads of the same command, diffed on their **counts**, with every delta
|
|
920
|
+
attributed to a declaration the answers already carry — `test_source_excluded`,
|
|
921
|
+
`symbols_excluded`, `coverage.gaps`, the `*_cap` contracts, `waivers`. A delta one
|
|
922
|
+
of those explains is accounted for; a delta **nothing** explains is `unattributed`,
|
|
923
|
+
which is the operational definition of a silent regression.
|
|
924
|
+
|
|
925
|
+
It also refuses. `cache_layers`, `base_env` and the analyzer fingerprint say what
|
|
926
|
+
each run was measured under, so two payloads taken under different conditions are
|
|
927
|
+
reported as **not comparable** on the axis that condition governs instead of being
|
|
928
|
+
diffed anyway. Clocks (`analysis_time_ms`, `wall_ms`) are never content deltas —
|
|
929
|
+
they inherit the contention they would be used to detect.
|
|
930
|
+
|
|
931
|
+
The verdict is one of `unchanged`, `explained`, `unattributed_deltas` or
|
|
932
|
+
`not_comparable`, and none of them is *"the release regressed"*: that judgement
|
|
933
|
+
belongs to the reader, and this gives them the three facts it needs.
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
889
937
|
### `ask trend <dir>` — how the architecture moved
|
|
890
938
|
|
|
891
939
|
```bash
|