sourcecode 5.6.1__py3-none-any.whl → 5.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/_docs/DEFECT-LEDGER.md +50 -3
- sourcecode/_docs/USER_GUIDE.md +1 -1
- sourcecode/archetype.py +8 -1
- sourcecode/cache_model.py +4 -2
- sourcecode/canonical_ir.py +6 -1
- sourcecode/change_plan.py +7 -0
- sourcecode/cir_graphs.py +67 -0
- sourcecode/cli.py +412 -46
- sourcecode/container_wiring.py +226 -17
- sourcecode/contract_diff.py +6 -1
- sourcecode/detectors/java.py +7 -1
- sourcecode/explain.py +46 -0
- sourcecode/integration_coordinates.py +109 -1
- sourcecode/migrate_check.py +59 -0
- sourcecode/non_coverage.py +31 -0
- sourcecode/output_ceiling.py +195 -0
- sourcecode/parse_cache.py +23 -1
- sourcecode/posture.py +138 -28
- sourcecode/pr_impact.py +52 -1
- sourcecode/reconciliation.py +33 -0
- sourcecode/release_info.py +6 -4
- sourcecode/repository_ir.py +1091 -126
- sourcecode/risk_model.py +188 -0
- sourcecode/security_posture.py +27 -7
- sourcecode/semantic_integration_engine.py +33 -0
- sourcecode/serializer.py +123 -15
- sourcecode/spring_impact.py +188 -56
- sourcecode/spring_model.py +155 -19
- sourcecode/verify_edit.py +110 -1
- {sourcecode-5.6.1.dist-info → sourcecode-5.8.0.dist-info}/METADATA +11 -4
- {sourcecode-5.6.1.dist-info → sourcecode-5.8.0.dist-info}/RECORD +36 -34
- {sourcecode-5.6.1.dist-info → sourcecode-5.8.0.dist-info}/WHEEL +0 -0
- {sourcecode-5.6.1.dist-info → sourcecode-5.8.0.dist-info}/entry_points.txt +0 -0
- {sourcecode-5.6.1.dist-info → sourcecode-5.8.0.dist-info}/licenses/LICENSE +0 -0
- {sourcecode-5.6.1.dist-info → sourcecode-5.8.0.dist-info}/licenses/NOTICE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -28,6 +28,13 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
28
28
|
| AS-06 | `@InitBinder` alone was treated as evidence of programmatic validation, contaminating gaps and confidence. | 5.5.1 audit | High | **closed 5.5.1** — only a Spring `Validator` implementation or `setValidator` wiring declares the limit; 5.5.4 recovery publishes that population separately from annotation validators. |
|
|
29
29
|
| AS-07 | Nested types used as field types were reported statically unreferenced by modernization. | 5.5.1 audit | Medium | **closed 5.5.1** — field-type references contribute an incoming static edge. |
|
|
30
30
|
| AS-08 | `spring-audit` severity census could sum to zero beside nonzero findings. | 5.5.4 re-audit | High | **closed 5.5.4** — severity partitions reconcile to `total_findings`; this is asserted in the audit battery. |
|
|
31
|
+
| AS-09 | The ~50K-token output ceiling existed on `migrate-check` and `repo-ir` only, so the same size was refused by one command and printed in silence by another — `endpoints` on a 7 885-file repository emitted 353 112 B (~88K tokens) with `exit=0`. | 5.5.6 audit (ASK-03) | High | **closed 5.7.0** — one policy in `output_ceiling`, applied at the single emit seam, with `--max-output-tokens` / `ASK_MAX_OUTPUT_TOKENS`; the escapes are derived from the running command's own Click parameters, so a hint never names a flag the caller already passed. |
|
|
32
|
+
| AS-10 | `migrate-check --compact` could not fit under the ceiling on a many-module build: every list it capped scaled with *findings*, while the weight scaled with *modules* (keycloak: 38 428 B across four uncapped module-scaled sections against 1 519 B of findings). | 5.5.6 audit (ASK-01b) | Medium | **closed 5.7.0** — `evidence_manifest.claims`/`sources`, `java_lts_inventory.evidence` and `hibernate.module_exposure_map` carry the `*_cap` contract, and `--top-n` reaches the compact answer rather than only the `--table` rendering (80 094 → 36 190 B; 26 665 B at `--top-n 1`). |
|
|
33
|
+
| AS-11 | The parse store published "least-recently-used, enforced on write" and, over budget, "the next write evicts", while the field watched it grow 25 MB past a 512 MB budget. Enforcement is a sweep every 32 MB written, per process. | 5.5.6 audit (ASK-05) | Low | **closed 5.7.0** — the policy line names the interval, `sweep_interval_mb` is published beside the size, and an over-budget store states the bound on its overshoot; `cache model` and `docs/CACHE.md` corrected from the same fact. |
|
|
34
|
+
| AS-12 | `cache warm` named `endpoints` and `migrate-check` as gaining nothing from a warm, while `cache model` measures `endpoints` at 3,3 s → 1,4 s — a hand-written sentence beside a generated table. The field timed those two commands and concluded a warm does not amortize below ~1 500 Java files. | 5.5.6 audit (ASK-07) | Low | **closed 5.7.0** — the population is read from `cache_model.COMMANDS`, and what a warm buys is stated before the cost is paid. The size threshold is **not** published: measured, the root view goes 2,5 s → 0,3 s at 47 files, 5,8 → 0,3 at 1 126 and 15,4 → 0,3 at 2 985, so a "too small to warm" claim would be false at every scale measured. |
|
|
35
|
+
| AS-13 | `verify-edit` rebuilt two repository-wide models when the only differences from HEAD were files no axis reads — a README, a build log, or this tool's own `.sourcecode-cache/`. Measured on keycloak: 32,5 s to answer `changed_files: []`, `verdict: pass`. | 5.6.1 audit (B4) | High | **closed 5.7.0** — the short circuit asks whether anything an axis *reads* differs, using the file walk's own prune set; 32,5 s → 0,58 s, `model_built: false`, and `disregarded_changes` names what it did not look at. An unrecognised extension is never disregarded. |
|
|
36
|
+
| AS-14 | `spring-audit` fell from 3 SEC-003 findings to 0 when 131 sources were classified `test-source`, with neither the exclusion nor `--include-tests` anywhere in the payload or the help — while `endpoints`, fed by the same authority, published both. | 5.6.1 audit (ASK-08) | Medium | **closed 5.7.0** — `metadata.test_source_excluded(_detail)` is filled by the file walk that made the exclusion, in the shape `endpoints` uses, and `--include-tests` audits the wider population (BroadleafCommerce: 20 → 26 findings over 197 sources). |
|
|
37
|
+
| AS-15 | `--compact` capped `key_dependencies`, `env_map`, `code_notes` and entry points while publishing none of it (BroadleafCommerce: 40 dependencies, 144 environment variables and 64 code notes hidden behind lists that read as complete), and the root analysis published no response envelope in any view — `--agent` dropped `schema_version` as well. | 5.6.1 audit (B14) | High | **closed 5.7.0** — every cap publishes `total`/`shown`/`omitted` with the registered display-cap effect, a cap that did not bite claims nothing, the root analysis is named to the envelope stamper (identity + tree state, over the analysed path rather than the CWD), and `schema_version` survives every view. |
|
|
31
38
|
|
|
32
39
|
---
|
|
33
40
|
|
|
@@ -50,7 +57,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
50
57
|
| C1-13 | **Hub-guard publishes `endpoints_total: 0` instead of `unknown`.** A service with 780 callers is capped at `depth=1` for cost — defensible — but reports 0 endpoints reached, contradicting the epistemic standard the tool imposes on itself everywhere else. Hubs are precisely where blast radius is asked for | 3.2.1 (eval #4) | Medium | **closed 3.6.0** — no battery repository crosses the guard (openmrs-core's largest hub, `Context`, has 215 caller classes against a threshold of 500), so it was reproduced on a generated fixture: `bfs_truncated: true`, `depth_reached: 1`, `endpoints_affected_count: 0` beside four more zeros nothing looked for. A reach-derived count of 0 under the cap is now `null`, named in `stats.unknown_under_cap`. The distinction is the point and is asserted both ways: a **non-zero** count under the cap stays a number (it is a floor the effect block already describes), and an **uncapped** run keeps its measured zero, because converting that would be the mirror-image defect. The hub threshold moved to module level so the fixture crosses the real guard instead of a copy of its value |
|
|
51
58
|
| C1-14 | **File-count drift across commands**: 3 334 / 3 337 / 3 342 / 3 374 files for one repository in one session. No command is wrong in isolation; together they cost the one thing a deterministic tool cannot lose — that its numbers agree with themselves | 3.2.1 (eval #4) | Medium | **within-document half closed 3.6.0 · cross-command half does not reproduce.** Of six commands measured on openmrs-core (`--compact`, `endpoints`, `spring-audit`, `migrate-check`, `modernize`, `validation`) only `migrate-check` publishes a scanned-file count at all, so there are no two figures left to disagree. What does reproduce is the same defect *inside one document*: `affected_files` named two populations — 265 under `summary` (every finding, any scope) against 264 under `effort_breakdown` (blocking findings only). Both correct; the shared name was the contradiction. Same remedy as C1-5: the narrower becomes `blocking_affected_files`, both carry a `_basis`, the old key stays an alias, and the battery asserts the two diverge exactly when a finding is non-blocking |
|
|
52
59
|
| C1-15 | **Analysis assumes a single destination.** Asked about a JDK move, `migrate-check` answers `headline_blocker: boot3_migration` and emits fix hints that send a developer to migrate namespaces they never planned to touch (481 `javax.persistence` under MIG-001). There is no way to say "JDK 11, keep Boot 2.7" | 3.2.1 (eval #4) | **High (product)** | **closed 3.6.0** — `--target-jdk N` and `--keep-boot` say which migration is being planned, filtered over the `migration_target` vocabulary every rule already carried rather than a parallel one. Measured on openmrs-core with `--target-jdk 11 --keep-boot`, the row's own acceptance: **MIG-001 85 → 0**, blocking 114 → 0, `headline_blocker` `hibernate_rewrite` → `none_applicable`, and the Hibernate 5→6 rewrite drops with it because Hibernate 6 arrives with Boot 3. Narrowing is not silencing: `metadata.route` publishes the target, the flag and every excluded axis, a `limitations` entry states how many findings were left out and that they are unresolved rather than fixed, and the route's own blockers stay (a JDK 11 route still reports MIG-021, asserted). The default is untouched, verified identical on three repositories |
|
|
53
|
-
| C1-16 | **Four fan-in figures for one class, and the note that promises the relationship is itself false.** Field: `modernize.in_degree` 1 026 / `impact-chain.direct_callers` 611 / `explain.incoming_callers` 519 / `impact.stats.direct_caller_count` 484 for one service, plus 3 222 vs 3 458 endpoints affected. **Re-measured on the battery, 3.4.0, `Money` @ BroadleafCommerce: 673 / 1 868 / 151 / 418** — `impact-chain` is absent from the published relationship and is **2,8× larger than the figure the note declares "always the largest of the three"**, while `explain` vs `impact` differ by **177 %**, not the *"small margin"* the same note promises. In the same runs `impact.stats.endpoints_affected_count: 0` against a populated `endpoints_affected` from `impact-chain` — a **false zero** on the blast-radius axis | 3.2.1 (eval #5), reopened 5.5.4 | **High** — this is the number a risk decision is taken with: *"¿el blast radius son 484 o 611 clases?"* | **reopened 5.5.4.** The DI and subtype fixtures remain closed, but the claimed one-authority implementation was false for service locators: `impact` kept a local BFS that elevated `Locator#accessor` to every caller of `Locator`, while `impact-chain` used `caller_reach` and did not. 5.5.4 recovery removes that elevation and adds a two-accessor regression fixture; authority unification and cross-command parity remain open as Q-03. |
|
|
60
|
+
| C1-16 | **Four fan-in figures for one class, and the note that promises the relationship is itself false.** Field: `modernize.in_degree` 1 026 / `impact-chain.direct_callers` 611 / `explain.incoming_callers` 519 / `impact.stats.direct_caller_count` 484 for one service, plus 3 222 vs 3 458 endpoints affected. **Re-measured on the battery, 3.4.0, `Money` @ BroadleafCommerce: 673 / 1 868 / 151 / 418** — `impact-chain` is absent from the published relationship and is **2,8× larger than the figure the note declares "always the largest of the three"**, while `explain` vs `impact` differ by **177 %**, not the *"small margin"* the same note promises. In the same runs `impact.stats.endpoints_affected_count: 0` against a populated `endpoints_affected` from `impact-chain` — a **false zero** on the blast-radius axis | 3.2.1 (eval #5), reopened 5.5.4 | **High** — this is the number a risk decision is taken with: *"¿el blast radius son 484 o 611 clases?"* | **reopened 5.5.4.** The DI and subtype fixtures remain closed, but the claimed one-authority implementation was false for service locators: `impact` kept a local BFS that elevated `Locator#accessor` to every caller of `Locator`, while `impact-chain` used `caller_reach` and did not. 5.5.4 recovery removes that elevation and adds a two-accessor regression fixture; authority unification and cross-command parity remain open as Q-03. **Three further field witnesses, 2026-08-16, all on Q-03 and none needing a row of their own.** (a) `keycloak-config-cli` F-5: `KeycloakProvider` fan-in published as **10** (`impact.stats.direct_caller_count`), **12** (`explain.incoming_callers_count`) and **22** (`impact-chain.metadata.direct_caller_symbol_count`) in one session — every figure carries its declared unit and `caller_metric_note` reconciles them, but `impact-chain`'s *prose* prints the symbol count under the words "22 direct callers", contradicting its own metadata two lines below. The unit belongs in the sentence, not only in the field. (b) `keycloak-config-cli` F-4 → **closed `ea4d791`**: `impact` and `impact-chain` published 77.26 vs 74.19 and 79.81 vs 76.79 for the same symbols while `impact` shipped no `risk_model`, so the divergence could not be read off the payload; `impact` now publishes the basis it already shares. (c) BroadleafCommerce D7: `impact-chain` 24 direct callers / 227 indirect / 37 endpoints against `impact` 12 classes / 34 symbols / 249 / 38 for `ExploitProtectionService`, plus `project_summary: "41 transactional boundaries"` beside `transactional_boundaries.class_count: 29`. The evaluator's verdict is the one that matters commercially: *"un cliente que compare dos comandos verá una contradicción antes de leer la nota."* |
|
|
54
61
|
| C1-17 | **A fact with an authority still contradicts itself across two commands**: `--compact.analysis_gaps` says *"1 test file for 3 336 Java files"* while `review-pr.test_coverage_risk.repository_has_test_sources` says `false`, in the same repository. C1-2 was closed in 3.2.1 with five consumers rebound, and this evaluation ran **on 3.2.1** | 3.2.1 (eval #5) | Medium | **does not reproduce (measured 3.6.0)** — re-measured as the row instructed. On openmrs-core, keycloak and spring-petclinic neither statement is emitted, and both consumers reach one authority: `--compact` through `analyze_test_sources`, `review-pr` through `is_test_path`. What was missing is now asserted: the authority has **two entry points** and nothing held them to each other, which is exactly the shape a sixth consumer would take. Zero mismatches across a case table including the C1-2 discriminator (a `test` package inside a *main* source root is production code), plus the literal contradiction — "N test files" and "no test sources" — asserted unreachable in both directions |
|
|
55
62
|
| C1-18 | **The profile population differs per command, and the narrower one is what an agent reads.** Field: `--agent` reports a two-name profile set while `prepare-context fix-bug` reports four for the same repository in the same session — and the two names missing from the agent payload are the ones the *build* names for its deployable artefacts. Every finding about conditional security is keyed on that set, so the default agent surface answers the profile question with the population that lost the environments | 3.5.0 (eval #6) | **High** — it is the input to the one capability nothing else in the market has | **closed (unreleased)** — **fixture first, as C1-10 required**, and it reproduced worse than reported: on `tests/fixtures/spring_profile_population` the same repository in one session answered **`["qa","dev"]`** (default view — filenames + overlay directories), **`["dev","prod","staging"]`** (`posture` — config documents + `@Profile`) and **`["dev","prod","qa","staging"]`** (`--agent` / `fix-bug`, the union). Three populations under one name; every conditional-security finding is keyed on it. Authority `spring_profiles.profile_population`: four named signals (`file_naming`, `overlay_directory`, `config_documents`, `annotations`, plus `caller_supplied`), the union, and `by_signal` published so a name's provenance is auditable. The stack detector, `posture` and the serializer section all read it — asserted per surface, not claimed. Closing it exposed two false positives in the signals it inherited, both fixed structurally (never by name — VAI): an `application-{x}` file counts only **beside its base config** (alfresco's `alfresco/messages/application-model_de.properties` + 19 siblings invented twenty environments; now 0) and an overlay directory counts only when it **holds a config file** (spring-petclinic's `db/ messages/ static/ templates/` reported four; now `mysql, postgres`, which are its real profiles) |
|
|
56
63
|
| C1-19 | **File-count drift across commands, with witnesses this time.** 3 337 (`compact.project_summary`) / 3 336 (`analysis_gaps.testing`) / 3 342 (`migrate-check.metadata.java_files_scanned`) for one repository in one session | 3.8.0 (eval #7) | Medium | **closed 3.9.0** — measured, not equalised: the three figures count code-extension files in any language, the non-test denominator of a test ratio, and the .java files handed to the migration rules. Each names its population now, and `migrate-check.files_scanned_basis` states the two it is not. Original note: open — **C1-14's cross-command half, reopened.** 3.6.0 recorded that half as *not reproducing* because on openmrs-core only `migrate-check` published a scanned-file count at all; eval #7 shows three commands publishing one on a repository we do not have. Three populations are plausible and each may be right in isolation (all Java files / files admitted to the graph / files the migration rules scanned), which is exactly the C1-5 remedy: name the unit per figure, or bind them to one authority. Do not fix by making the numbers equal — measure which population each counts first |
|
|
@@ -76,6 +83,8 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
76
83
|
| C1-44 | **`has_uncommitted_changes: false` sits in the same document as `git_context.uncommitted_files: 1`, and it is the field cache freshness is decided on.** `ask <repo> --compact --git-context` publishes both, one run, one payload: `git_context.uncommitted_files` counts the file, `_cache.has_uncommitted_changes` says there is none, and `git status --porcelain` on the same tree prints `?? .claude/settings.json`. The third authority in the build agrees with the first — `--changed-only` reports `changed_files_count: 1` for that file and its `--help` defines the set it counts as *"staged, unstaged, untracked"* — so two of three surfaces admit the file and the one a consumer trusts for staleness does not. Under that published definition the boolean is simply false | 5.3.1 (audit #28) | Medium | **closed 5.4.0** — one predicate (`baseline_autocapture.counts_as_dirty`), extracted from `worktree_dirty` and read by the count in both the fresh path and the cache-hit patch, so the boolean cannot drift from the function that defines it. What it excludes is published (`uncommitted_files_not_read`, `uncommitted_files_basis`) rather than subtracted in silence, and only when something was actually excluded. Original remedy note: one predicate over one definition of *the working set*, read by both fields; the boolean is derived from the count rather than measured a second time, and where the two cannot be derived from one another the document may not publish both |
|
|
77
84
|
| C1-46 | **`existing_test_count: 6` sits beside `test_files: 0` and `has_test_sources: false` in one `prepare-context generate-tests` payload, and the 6 are five production files in a business package.** C3-115 closed the half that was reported — `test_gaps` now declares its population — and left a second authority for *"how many tests are there"* answering a different number in the same document. Re-measured in the field: 0 Java tests, 1 `.spec.ts`, 3 karma/jest configs; no population is worth 6. Root cause is in `test_sources.declared_test_root`: `_TEST_DIR_NAMES` carries the bare segment `it`, the final loop accepts it **at any depth**, and the only disarm — `_under_main_root` — knows `src/main` alone, which is a Maven layout. An Angular app lives under `src/app`, so `saint-client/src/app/shared/akita/it/` — *Incapacidad Temporal*, a business entity whose `it.model.ts` declares `ENFERMEDAD COMÚN` and `ACCIDENTE DE TRABAJO` — is read as a test source root and contributes 5 files; the 6th is the one real `.spec.ts`, and the reconciliation is exact. **It generalizes off this repository:** `it` is also the ISO 639-1 code for Italian, so `web/src/assets/i18n/it/messages.json` and `app/locales/it/common.ts` both answer `is_test_path: true` on the shipped build. Sibling of C1-28, in the direction C1-28 was written to prevent | 5.4.0 (audit #29) | **Medium** — the path-classification half is closed in 5.4.1; `existing_test_count_unit` now declares its all-path, multi-language task population in 5.4.2, distinct from the Java IR population | **closed 5.4.2** |
|
|
78
85
|
| C1-45 | **The SQL taint join keys a sink on a bare statement id, so a mapper method in one namespace is joined to a call site that resolves to another — and the result is published as the repository's #1 critical risk with a call trace that does not exist.** `risk`'s top row on a 3 342-file monolith is `severity_effective: 29.57` at `ProcesosGerenciaMapper.xml:74`, `factors.query_construction = http_input_reaches_sql_interpolation`, with evidence naming `AutocoberturasRestController#actualizar(id, obj)` *calls* `actualizar(…)` in `ProcesosGerenciaMapper`. The controller calls its own service's `actualizar`; the join matched on the method name alone. **The correct key already exists in the code and is not used** — `sql_taint` line 76 returns `f"{self.namespace}#{self.statement_id}" if self.namespace else self.statement_id`, and `namespace` is captured (141, 151) and then spent only on an explanatory sentence (313-314), while the published basis claims the sinks are *"keyed by namespace and statement id"*. **Accompanying defect, same row:** the interpolated expression is stored (72, 144) and emitted (86) and never inspected, so `bloqueada = ${dto.bloqueada ? "'S'" : "'N'"}` — an OGNL ternary whose two branches are string literals, a closed allowlist by construction and precisely the mitigation SEC-008's own `fix_hint` recommends — is ranked as a text-controllable sink | 5.3.1 (audit #28) | **High** — F-BF's whole argument is that this axis fires on evidence rather than on multiplication by 1,0; a name-collision join inverts the first row of the ranking, which is the product | **closed 5.4.0** — both sides keyed on `namespace#statement_id`: a call carries the type its receiver resolves to and is that statement's only when the receiver resolves to the mapper it lives in; an unresolved receiver is not the same answer as any type name, so the sink stays `undecidable` rather than `reaches`. `does_not_reach` carries the same qualification, because it is a confident no. `splices_free_text` reads the interpolation at last — anything it cannot prove closed is free text, and what it closes leaves the population into a published `bounded_interpolations` list. The regression fixture this row specified is in the suite. Original remedy note: key both sides on `namespace#statement_id` (the property is already written, the call sites read the wrong half), and give the interpolation a three-state read: an expression whose reachable values are all literals is `does_not_reach`, an unresolvable one stays `undecidable`, never `reaches` by default. Regression fixture: two mappers in different namespaces sharing a `statement_id`, one reached by HTTP input and one not — expected one `reaches`, one `undecidable`; today, two `reaches` |
|
|
86
|
+
| C1-47 | **`_cache.is_stale` is a literal `False` on the cache-hit path, not a computed value — and it disagrees with `ask cache freshness` for the identical repository state.** `cli.py:4004` injects `{"is_stale": False, ...}` into the reused payload on every cache hit, unconditionally; `ask cache freshness` computes real staleness from `ris_is_stale` (`cache.py:360`, comparing `_ris.git_head` against the current HEAD). Reproduced on three independent repositories with the same HEAD and the same uncommitted state on both sides of the comparison: jobrunr and neo4j (golden-repo battery #1) and tutorials (battery #2, RIS left unbuilt by a hung `cache warm`) all published root `_cache.is_stale: false` while `cache freshness` published `STALE` in the same breath. `cli.py:4916` (the fresh-generation branch, not the hit branch) is unaffected — `is_stale: false` there is correct by construction, since the content was just computed. Sibling of C1-9 (closed 3.2.2): that row fixed the invalidation *key* so a hit can no longer serve genuinely stale content; this row is the *field* that reports the fact of staleness on a hit, which was never wired to a computation and has stayed a placeholder | 5.7.0 (golden-repo battery #1 2026-08-14, #2 2026-08-15) | Medium — the invalidation itself is not shown to be wrong, but the two published surfaces for "is this current" disagree on the same state, and a consumer reading only the root envelope is told `false` unconditionally | **closed 5.7.2** — the two answers were both right about different stores, and neither payload said which. `_cache` describes the view cache that produced the answer; `freshness` describes the RIS, rebuilt by a different path. Three changes: `is_stale` is derived from the two heads printed beside it and is `null` when either could not be read (an axis that was not measured is unknown, never *fresh*); the hit branch recovers `git_head_at_generation` from the cached content the way it already recovered `generated_at`, instead of overwriting the one value that could contradict the claim; and a served-from-cache answer carries the RIS fact under its own name (`ris_exists`, `ris_git_head`, `ris_is_stale`) plus, when the two disagree, the sentence that reconciles them. The fresh path publishes no RIS verdict **on purpose**: that run rebuilds the RIS after the block is written, and the question is only asked of content the caller did not just compute. One construction (`_cache_meta_block`) serves both branches. Fresh-path values are unchanged on a repository with git. Regression: `tests/test_cache_provenance_c1_47.py`, 12 assertions |
|
|
87
|
+
| C1-48 | **`impact` and `impact-chain` score the identical symbol from two independently-maintained formulas, and the two can disagree on the risk band, not just the number.** `repository_ir.py:7837-7857` (`impact`): `raw_score = n_direct*2 + n_indirect*0.5 + n_ep*3 + n_txn*2.5 + n_mappers*1.5 + n_modules*1.0 + n_sec*2.0`, plus a floor independent of the score (`n_ep>=1` → at least `medium`, `n_ep>=3` → at least `high`, `n_ep>=5 and n_txn>=2` → `critical`). `spring_impact.py:705-740` (`impact-chain`, `_compute_risk`): `raw_total = finding_score + endpoints_affected*3 + direct*2 + indirect*0.5` — no `n_modules` term, no floor, band cutoffs at 25/12/4 rather than `_RISK_SCORE_FLOOR`. Reproduced on identical inputs across three repositories: `ApiGatewayController`/spring-petclinic-microservices (0 callers, 1 endpoint, 0 findings on both sides) — `impact-chain` returns `low`/3.0, `impact` returns `medium`/4.0, the gap entirely the floor rule; `GuidesController`/sagan (identical caller/endpoint counts on both sides) — `medium`/9.0 vs `high`/10.0; `UmsAdminController`/mall — 34.21 vs 40.0, both land `critical` so the gap is masked at that scale but the numbers still disagree. Same failure shape as C1-9/C1-47: one concept, `risk_score`, computed twice | 5.7.0 (golden-repo battery #2 2026-08-15) | Medium-High — a CI gate or a reader comparing the two commands' verdicts on the same symbol gets a different answer depending which command they ran, including a different band on ordinary (non-blind-spot) symbols; C1-41/C2-32 (closed) fixed the null/floor contradiction *within* each command separately and did not compare the two commands to each other | **closed 5.7.2** — one authority, `risk_model`: the weights, the knee (60 / 400), the band floors and the exposure floors, moved out of `impact` unchanged so the surface with the widest measurement keeps every number it published. Both commands score through it and publish the axes they measured (`risk_model.axes_measured` / `axes_not_measured`), so a remaining difference between two answers can only be a difference in **measurement**, which the payload then names; an axis nobody weighted raises instead of contributing a silent zero. `impact-chain` is `core` tier — contract stable within a major — so the model's fifth level, `none`, is **not** introduced into its published vocabulary in a patch: a `--fail-on risk >= low` gate switching on four values must not stop firing. The empty case maps to `low` at the publication boundary and the model's own verdict travels beside it as `level_shared_model`, with the note naming `ask impact` as the surface that says `none`; 6.0.0 removes the shim. Measured on the same six symbols: `impact` byte-identical on 6 of 6 (zero movement on the established surface) and the two commands agree on the band on 6 of 6 — the two rows that disagreed now match; chain scores moved onto the shared scale (56,9 → 63,55 · 34,21 → 39,0 · 30,56 → 33,0). Regression: `tests/test_one_risk_model_c1_48.py` (22) plus the chain-scale battery restated against the shared model |
|
|
79
88
|
|
|
80
89
|
|
|
81
90
|
## Class C2 — Presentation leaking into measurement
|
|
@@ -235,7 +244,7 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
235
244
|
| C3-109 | **A field anchor is refreshed once and expires again on the next release, because nothing in this repository can re-measure it.** C3-97 built the mechanism and C3-100 completed it: every field figure carries the build and the cache state it was taken on, and `field_currency()` names the rows that are behind. The tenth round confirms the mechanism works and reports the hole it leaves: the `spring-audit` anchor reads *"8,4 s (field, warm cache, on 4.18.0)"* and the same command measured **40,8 s on 5.0.1** on the same machine, so the anchor was *"born expired"* — correct as history, useless as advice, and the reader has no in-house figure to fall back on. The evaluator's conclusion is the right one: *"arreglaron la epistemología del ancla pero no el proceso que la mantiene cierta"*. The anchors are the **field's** measurements on a private repository and a Windows box; this repository can never refresh them | 5.0.1 (eval #25) | **High** — the anchors drive `here:`, the budget warning and the `cache model` header, i.e. every place the product advises how to run something, and their half-life is one release | **closed (unreleased)** — the gate's cells are carried into the package by `scripts/sync_gate_anchors.py` and published beside the field figure: `gate:` in `cache model`, `gate_measurement` per command in the JSON, and `gate_currency()` naming the build that produced them and — when it is behind — the command that fixes it, which is the whole difference from a field anchor. Beside, never instead: a measurement at field scale outranks one at gate scale, and both print their size, cache state, host class and build. One authority (the baseline JSON); `tests/test_gate_anchor_currency.py` fails when the generated module and the committed cells disagree, so a refreshed capture that never reached the package is a red suite rather than a silent lie. The release step is written down in `docs/perf/REGRESSION-GATE.md` §7. Original plan: publish, beside every field anchor, the **release gate's own measurement of the build in hand**: the gate already measures a 2 985-file repository per release under a declared host class, and its cells are committed (`docs/perf/baselines/gate-latest`). One authority, no hand-written number, and the property that closes the row — an anchor nobody can re-measure is no longer the only figure a reader has **✅ the labelling half closed 5.1.0; the second half closed 5.2.0 with C3-110** — a figure that *advises* now says the build it was taken on **and** that this build has not been re-measured, and the anchors themselves were refreshed from evaluation #26. |
|
|
236
245
|
| C3-110 | **The release performance gate has run nightly for six releases and has never blocked one, because nothing in the release path asks it anything.** F-AP shipped the detector, C3-98 gave it a baseline, C3-99 corrected its exclusion list and C3-108 taught it to measure cold as well as warm — and the eleventh round measures, on the same commit and the same machine, `validation` at **35,4 → 14,0 → 36,8 → 35,8 → 41,8 → 17,8 → 39,8 → 13,8 → 34,1 → 39,5 s** across eleven releases: four complete oscillations of ~2,9× on the cheapest command in the affected set. `risk` covers two orders of magnitude over the same series. Three separate holes, all of them ours: the gate's population is **six commands** (`ask`, `endpoints`, `spring-audit`, `migrate-check`, `validation`, `posture`) against the **eighteen** the field measures every round — `risk`, `verify-edit`, `delta`, `contract-diff`, `modernize`, `explain`, `repo-ir`, `data-exposure`, `pr-impact`, `impact-chain`, the root `--agent` view and the cold root build are all outside it; the workflow runs on `schedule` and `workflow_dispatch`, so no tag and no release commit is gated by it; and the baseline it compares against is `gate-latest`, which the previous release overwrote, so a release compares against *a* release rather than against **the best measured build** the product has ever had (4.18.0). The evaluator's framing is the one to keep, because it is the reason this row is critical rather than a performance chore: *"B10 no es deuda de rendimiento. Es el mecanismo que impide que las demás correcciones se mantengan ciertas"* — C3-109's anchor expired the release after it was closed for exactly this reason, and the two are one task | 5.1.0 (eval #26) | **Critical** — every other row in this class is a fix that can silently come undone, and eleven rounds of evidence say some of them do | open — the population becomes every command the field measures that takes only a path (`risk`, `modernize`, `explain`, `repo-ir`, `data-exposure`, the root `--agent` view, and `delta`/`contract-diff` against one ref pair), the gate runs on the release commit and not only at 03:00, and the comparison is against **two** baselines: the previous release (regression) and the best measured build (drift). The same run writes `gate_anchors.py`, which is C3-109's maintenance and not a second task **✅ closed 5.2.0** — `push: tags: v*` gates the release (the nightly stays, to catch a regression the day it lands); the population is every path-only command the field times, with `risk`, `modernize`, `repo-ir` and `data-exposure` added and every exclusion carrying its reason; `drift_report()` publishes the distance from `reference-best` without deciding, because a staircase of sub-threshold steps is invisible to a release-to-release comparison and unmissable here. Two defects found while measuring the gate: cold cells were not cold (only the snapshot layer was isolated, so every cold run used the previous cell's parse store) and a failed run was recorded as a fast one (`--no-cache` is rejected by most subcommands — `ask risk . --no-cache` exits 2 — and the harness timed the error envelope at ~0,29 s). Both fixed; a cell that did not run is not written, so the gate reads it as `missing` and fails. **The gate went red on its own first run over a release (v5.2.0), and not on a timing** — which is the row proving itself: the default JSON run of `migrate-check` on the gate repository **exits 1** with `OUTPUT_TOO_LARGE` (~269 K estimated tokens on 2 985 files), and the cell had sat in `gate-latest` since 4.13.0 at 8,72 s because the analysis does run and only the emit refuses; and `data-exposure` measured **0,28 s warm and cold**, the cost of its abstention (`answered: false`, no `dataLabels` in the gate repository) rather than of the command. `migrate-check` is measured with `--compact` — its own published remedy — and `data-exposure` is excluded with the reason. Both were invisible for as long as the harness ignored exit codes. First honest cold cells at gate scale, from the same run: `ask` 24,2 s cold / 0,36 s warm · `spring-audit` 16,9 / 3,2 · `validation` 15,9 / 8,0 · `posture` 16,2 / 2,6 · `risk` 31,1 / 17,7 · `repo-ir --summary-only` 8,4 / 4,3 · `modernize` 8,6 / 4,8. |
|
|
237
246
|
| C3-111 | **`repo-ir`'s size ceiling guards stdout and nothing guards the file, and the advisory that would have warned is terminal-gated — so the run that actually produces 101 MB is the one that says nothing.** CL-26 answered eval #23's *"`repo-ir` has no cap and no warning"* with the stdout path, and that half is accurate: `OUTPUT_TOO_LARGE` refuses above ~50 K estimated tokens with four ways out in the hint. The half nobody read is `-o`: the write path has **no ceiling at all** (*"--output FILE skips the payload budget entirely"*), and the pre-build band (C3-18/C3-105) is emitted through `_notice`, which is silent off-TTY by contract (C3-26). A scripted or CI run — which is every run that passes `-o` — therefore gets **101 254 852 B, byte-identical across three releases, with no line anywhere**, into the panel titled *Context for AI agents*, whose consumer is the one that tolerates 101 MB worst. The product's own discipline is the contrast the evaluator draws: `endpoints --servlets` bounds its population and declares it, `--compact` publishes its budget and `direction: under_reports` | 5.1.0 (eval #26) | **Medium** | open — the ceiling applies to the answer, not to the pipe it goes through: above a declared byte estimate the `-o` run refuses **before it builds** with the same envelope and the same four ways out, and the advisory that survives the refusal travels in the payload rather than through a TTY-gated notice **✅ closed 5.2.0** — an estimate above 50 MB refuses **before the analysis**, through the error envelope (which a script reads and a TTY-gated notice does not), naming the band, its basis and the four ways out that already existed. The band has one derivation now, shared by the advisory and the guard, and the ceiling compares against its midpoint: the low end estimates the field's repository at 34 MB and would have let 101 MB through. Broadleaf refuses in 0,3 s; spring-petclinic writes 319 KB unchanged. |
|
|
238
|
-
| C3-112 | **The clean-tree short-circuit is decided by every path git prints, so one untracked note beside the code buys a full repository-wide model — and the payload prints `changed_files: []`, so the reason is invisible.** C3-102 short-circuits on `working_cir_reused`, which `head_vs_working` sets from `git_answered and not all_changed`; `all_changed` is the tracked diff ∪ untracked adds over **all** paths, while the `changed_files` the verdict publishes is only its `.java` subset and `changed_build_files` only the admitted build/config subset. So an untracked `NOTES.md`, an audit output written into the repository, a `.sourcecode-cache/` directory — none of which any axis can read — flip `reused` to False, both sides are modelled, and the basis reads *"0 changed .java file(s) and 0 changed build file(s): HEAD and the working tree were both modelled and diffed"* without ever naming the path that forced the build. Measured at HEAD (5.2.0) on jobrunr (964 Java files), which carries exactly one untracked `.md` — with it present **3,74 s**, `model_built: true`; with it moved aside **0,31 s**, `model_built: false`: **12×**, one file, no code difference. Eval #27 is the same shape at 3 342 files (**68,3 s** to answer *"you have not edited anything"*), and its payload proves which path ran: it carries per-axis `detail` strings (*"no custom rules"*, *"no build or configuration file changed"*) that the short-circuit never emits, because it returns `AxisResult(False)` with an empty detail. Collateral, and the reason this outranks its cost: the release gate excludes `verify-edit` on the written premise that *"its clean-tree path builds no model at all since C3-102, so the figure a gate could take here would not be the figure a user pays"* — in the field's regime that premise is false, and the figure a user does pay is ungated | 5.2.0 (eval #27) | **High** — the one command whose value is bounded by its latency, defeated in the state every working copy is actually in; and a fix measured as closed in-house (9,94 → 0,62 s) that the field cannot reach is indistinguishable, from outside, from a fix that came undone | open — the condition must be the change set the axes can read (`.java` ∪ `_is_build_config_path`), not every path git prints: the two are already computed side by side in `head_vs_working`, so this is which tuple `reused` reads. The verdict then publishes the paths it saw and did not admit (`ignored_paths`: count, sample, and the rule that excluded them), because an answer that costs nothing must still say what it looked at — and once the clean-tree path is the one the field takes, `verify-edit` loses its gate exclusion and joins the gated population |
|
|
247
|
+
| C3-112 | **The clean-tree short-circuit is decided by every path git prints, so one untracked note beside the code buys a full repository-wide model — and the payload prints `changed_files: []`, so the reason is invisible.** C3-102 short-circuits on `working_cir_reused`, which `head_vs_working` sets from `git_answered and not all_changed`; `all_changed` is the tracked diff ∪ untracked adds over **all** paths, while the `changed_files` the verdict publishes is only its `.java` subset and `changed_build_files` only the admitted build/config subset. So an untracked `NOTES.md`, an audit output written into the repository, a `.sourcecode-cache/` directory — none of which any axis can read — flip `reused` to False, both sides are modelled, and the basis reads *"0 changed .java file(s) and 0 changed build file(s): HEAD and the working tree were both modelled and diffed"* without ever naming the path that forced the build. Measured at HEAD (5.2.0) on jobrunr (964 Java files), which carries exactly one untracked `.md` — with it present **3,74 s**, `model_built: true`; with it moved aside **0,31 s**, `model_built: false`: **12×**, one file, no code difference. Eval #27 is the same shape at 3 342 files (**68,3 s** to answer *"you have not edited anything"*), and its payload proves which path ran: it carries per-axis `detail` strings (*"no custom rules"*, *"no build or configuration file changed"*) that the short-circuit never emits, because it returns `AxisResult(False)` with an empty detail. Collateral, and the reason this outranks its cost: the release gate excludes `verify-edit` on the written premise that *"its clean-tree path builds no model at all since C3-102, so the figure a gate could take here would not be the figure a user pays"* — in the field's regime that premise is false, and the figure a user does pay is ungated | 5.2.0 (eval #27) | **High** — the one command whose value is bounded by its latency, defeated in the state every working copy is actually in; and a fix measured as closed in-house (9,94 → 0,62 s) that the field cannot reach is indistinguishable, from outside, from a fix that came undone | open — the condition must be the change set the axes can read (`.java` ∪ `_is_build_config_path`), not every path git prints: the two are already computed side by side in `head_vs_working`, so this is which tuple `reused` reads. The verdict then publishes the paths it saw and did not admit (`ignored_paths`: count, sample, and the rule that excluded them), because an answer that costs nothing must still say what it looked at — and once the clean-tree path is the one the field takes, `verify-edit` loses its gate exclusion and joins the gated population **✅ closed by B4 (5.7.0), verified 5.7.3 — this row was left open on a stale state.** B4 replaced the predicate with `_can_move_a_verdict`, which is exactly the change this remedy asked for, arrived at from the other side: rather than admitting only `.java` ∪ build/config, it disregards inert directories and a short inert suffix list and analyses everything else — conservative in the direction that costs time rather than truth. Re-measured on jobrunr, the row's own repository, with an untracked `.md` present: **0,44 s**, `model_built: false`, against the 3,74 s the row recorded at 5.2.0. The disclosure half is also there: `analysis.disregarded_changes` lists the paths, with a count, a basis sentence and the reason the model was not built. Nothing was changed for this closure — it is a measurement |
|
|
239
248
|
| C3-113 | **The release gate ships armed and its two baselines are empty or stale, so the release it gates is compared against 5.0.0's six warm cells and its distance from the best build is never computed.** C3-110 closed the mechanism — `push: tags: v*`, a widened population (`risk`, `modernize`, `repo-ir --summary-only`, cold cells, every exclusion with its reason), `drift_report()` against `reference-best` — and the artefacts it decides from were not promoted with it. At HEAD: `docs/perf/baselines/gate-latest/` holds **six** `warm` cells, all stamped `tool_version: 5.0.0` (`ask`, `endpoints`, `migrate-check`, `posture`, `spring-audit`, `validation`) — the pre-C3-110 population, no `risk`, no `modernize`, no `repo-ir`, no cold cell; `docs/perf/baselines/reference-best/` holds **no cells at all**, only its README, so the drift half prints its honest absence every run and 4.18.0 — the build the field still calls the best measured — is a number nothing compares against; and `gate_anchors.py`, generated from `gate-latest`, carries `GATE_MEASURED_VERSION = '5.0.0'` into a 5.2.0 build, which makes `gate_currency()` non-current and suppresses the one sentence in `cache model` that would have told a reader what the gate measured on the build in hand. A cell present in the current run and absent from the baseline is *recorded, not compared* (only the reverse fails), so the widened population is measured and ungated until a capture is promoted. This is the exact residue of the field's B10 — *"cero regresiones, pero sigo sin evidencia de que exista el gate"* — and the residue is real rather than a reporting gap | 5.2.0 (eval #27) | **High** — C3-110's argument applies to C3-110: a gate whose baseline is two releases old and whose drift reference is empty cannot hold the line it was built to hold, and the mechanism being correct is invisible to anyone outside this repository | open — promote the v5.2.0 tag run's cells (warm **and** cold, full population) to `gate-latest` and re-run `scripts/sync_gate_anchors.py`, so the anchors a build publishes are the build's own; capture `reference-best` once from the reference build in the gate's host class (`gh workflow run perf-gate.yml --ref v4.18.0`, cells committed) so drift has a floor to measure from — the field's *"congelad esta versión como línea base"* is the same request, and 5.2.0 is now the better candidate on their own table; and publish the verdict where a consumer reads it (the gate stanza in `cache model`, and the release notes), because *"the gate ran and was green"* is a fact about the release, not about this repository |
|
|
240
249
|
| C3-114 | **Two runs of `endpoints` over one unchanged tree returned two different security censuses, and the disagreement is stable on each side.** Run #12 and one run taken directly after `cache clear -y --all` + warm published `custom_gate_inferred: 939 / none_detected: 2635`; eight later runs — including `--no-cache`, `--jobs 1` and `--jobs 4` — published `935 / 2635 / programmatic: 4`. The four that moved are `NominaRestController#{obtenerNominasEjercicio, NominaActiva, cerrarNomina, estado, desbloquearProcesos}`, verified in source at `NominaRestController.java:125-158` to carry a literal `@M3FiltroSeguridad`, with zero programmatic security calls anywhere in the file — so the **939 answer is the correct one** and the stable, reproducible 935+4 is wrong. The trigger was irreproducible state: the shared parse cache held 13 368 entries the build reports as *"written by another build … can never be read again"*, which the first run retired. Two facts make it a row rather than an anecdote: a security verdict differed between two runs of one command on one tree, and the `programmatic` fallback — a whole-file scan whose result is memoised per `declaring_file` — is the only classifier in the path that can produce exactly this pair | 5.3.1 (audit #28) | **High** — determinism is an eleven-round property of this product and the axis that moved is the security verdict | **closed 5.4.0** — three causes, all in the fallback. The read was unrooted (`declaring_file` is repository-relative and was resolved against the process working directory, so the verdict depended on `cd`); the scan could not tell code from prose (`hasRole(` inside `@PreAuthorize("hasRole('ADMIN')")` — a declarative gate on another method read as runtime enforcement of this one, now blanked through the E-1/E-2 authority); and a whole-file regex preempted a per-method gate on both surfaces. An unreadable file now abstains and is cached as `None`, which is not the same entry as "read it, found none". Original triage: **re-measure after E-3 and only then investigate separately**: E-3 is the same subsystem and the same four handlers, and its fix changes which member the policy is read from. If it survives, the suspect is named: `_prog_sec_cache` is keyed on the file and populated from a `Path.read_text` inside a bare `except`, so a read that fails once poisons the answer for every handler in that file for the rest of the run |
|
|
241
250
|
| C3-115 | **`prepare-context generate-tests` publishes a `test_gaps` key whose population nothing declares, on a repository with 3 342 ranked gaps.** The command's `--all`/`--include-config` help documents the key; the payload emits it empty on a repository where `test_gap_ranking.population.test_files` is `0` and 3 342 sources rank as gaps. The degradation itself is honest — the budget block states in words that empty is not the same as *nothing found* — which is exactly why the second key is the defect: it is the one field in the payload with no declared population, beside a ranking that has one | 5.3.1 (audit #28) | Low-Medium | **closed 5.4.0** — present whenever the ranking ran, with `untested_sources_unit` naming what it counted over and pointing at `test_gap_ranking.population` as the authority. Original remedy note: declare the population `test_gaps` counts over, or delete it in favour of `test_gap_ranking.population.test_files`, which is already the authority. One fact, one authority (C1) |
|
|
@@ -248,6 +257,8 @@ The facts these rows are keyed to are published: `ask schema facts-v1` prints th
|
|
|
248
257
|
| C3-122 | **`ask risk <repo> -o <path>` spent 444 s, exited 0, printed nothing and wrote no file.** One occurrence, from bash with a Windows path in forward slashes; a later recursive search found the file nowhere in the user tree. The same path shapes worked in the same session for `ask endpoints -o`, `ask config` and `ask baseline capture --dir`. Not reproduced — the auditor stopped invoking `risk` after the freeze in C3-121 — and recorded with that limitation. An analysis command that consumes 444 s and reports success without producing its output owes the reader a line on stderr at minimum | 5.4.0 (audit #29) | **Low** (unreproduced) — but it is a C5 shape: the exit code is the published fact and it is wrong | **open** |
|
|
249
258
|
| C3-123 | **A static asset reaches `broken_integrations`, the one verdict this command presents as unconditional.** C3-119 did the main work — 19 → 1, with 14 relocated to `partial_client_calls` and the `meaning` stating its condition. The survivor is `GET /assets/i18n/{}.json` from `transloco-root.module.ts:18`: a translation bundle served by the container, not a Spring route. True under the published definition and not a broken integration in any sense a reader means. Remedy: exclude paths under known asset roots (`/assets/`, `/static/`, `/public/`) or ending in a static file extension, and say so in the `meaning` | 5.4.0 (audit #29) | **Low** | **closed 5.4.1** — static asset roots and file extensions are excluded and the meaning names the exclusion |
|
|
250
259
|
| C3-124 | **`risk` renamed three published keys inside a minor with no alias.** 5.3.1 emitted `total_defects`, `total_findings` and `risks_cap`; 5.4.0 emits `total_defects_floor`, `total_findings_floor` (plus `counts_basis`, `counts_are_floor`, `partial`, `_partial`) and dropped `risks_cap`. Permitted by the command's own experimental tier — *shape may change in a minor* — and the new names are better; the payload still carries no notice, so a consumer bound to the old keys reads `None` with nothing telling it why. `auth status` already publishes a `deprecated_fields` block: the channel exists and this command did not use it | 5.4.0 (audit #29) | **Informative** | **closed 5.4.1 as not a defect** — a complete 5.4.1 run retains the legacy keys; the renamed floor fields are emitted only for budget-cut payloads and are explicitly marked as floors |
|
|
260
|
+
| C3-125 | **`ask <repo> --compact` writes its complete, valid answer to stdout and then does not exit.** On tutorials (24 074 java files, hundreds of Maven modules — the largest repository this battery has run, first time past ~9k files), the documented `OUTPUT_TOO_LARGE` fallback (`ask . --compact`) finishes writing full valid JSON by roughly 100-116 s wall clock, but the process stays alive — polling confirmed stdout stable at t=103 s while the process remained running and consuming CPU past 180 s, when it was killed by hand. Reproduced twice independently in the same session. A caller that waits on process exit rather than reading stdout and moving on (a CI step, a subprocess wrapper with a completion callback) hangs indefinitely on a repository at this scale; suspected an unshutdown worker pool (`parallel.py`/`ast_extractor.py`), not confirmed by reading source this session | 5.7.0 (golden-repo battery #2 2026-08-15) | High — a fast path completing correctly and then hanging is worse for automation than a slow path that returns, because nothing downstream distinguishes "still working" from "done, stuck" | **closed 5.7.1** — reproduced, and the suspicion this row carried is **refuted**: nothing leaks and the process does exit. A cold `ask tutorials --compact` (24 074 java files) ran **305,26 s** wall / 276,74 s user and exited 0 — the field's kill at 180 s landed inside a run that had 125 s still to go. What looked like a hang is the L1 cache write: `core_view` renders compact, agent **and** standard again, after `_emit_command_output` has already written the answer, and it did so with nothing on stderr. Sampled stacks put **57 %** of the process inside one function — `_spring_profiles_context`, a pure derivation of the snapshot, derived five times at ~50 s each. Two costs inside it: per (profile, file) pair it built three `pathlib.Path` objects (40 profiles × 35 142 paths × 3 = **4,2 million** constructions against a 24 000-file live heap), and `agent_view`'s `_spring_event_signal` re-parsed the whole repository into a private CIR. Three changes: the stems are derived once from the file list (same matching rule, same order), the context is memoized per snapshot and hands back a copy, and the event signal goes through `context_cache.shared_cir` — the one door C3-94 established. Measured after on the same repository, same 17 426-byte answer (byte-identical but for run id, timings and the analyzer fingerprint): **102,31 s** wall / 78,17 s user — **2,98× wall, 3,54× CPU**; the answer's own `total_ms` fell 97 035 → 45 816. The tail is also no longer silent: the post-answer build runs under a spinner reading *building cache entry (the answer above is complete)*, so "still working" and "done, stuck" are distinguishable from outside. Regression: `tests/test_post_answer_recompute_c3_125.py`, 7 assertions |
|
|
261
|
+
| C3-126 | **`ask cache warm` on the same 24k-file repository hangs near-idle rather than computing.** Killed after 190 s wall clock at 0.21 s user CPU — not mid-computation, genuinely stuck. No files were written under `~/.sourcecode/{context-cache,parse-cache-v1}` during the window. The following `ask cache freshness <repo>` reported `Status: STALE`, `RIS HEAD: (none)` — the warm never produced anything to be fresh. Distinct symptom from C3-125 (that one finishes the work and then fails to exit; this one never starts visible work) | 5.7.0 (golden-repo battery #2 2026-08-15) | High — the exact command the tool's own guidance recommends running before a repository this size is used interactively hangs instead of warming | **closed 5.7.1** — same root cause as C3-125, and the reading that made it look like a different defect was an artefact of where the CPU was measured: `cache warm` spawns `sourcecode <target> --compact` and waits on it, so **the 0,21 s of user CPU is the parent's own** — the work was in the child, which at that build needed 305 s. The 190 s kill landed before the child reached its cache write, which is why nothing appeared under `~/.sourcecode/{context-cache,parse-cache-v1}` and the following `freshness` read `RIS HEAD: (none)`. Nothing was stuck: no deadlock, no idle wait, no unstarted work. With C3-125's fix, `ask cache warm <tutorials>` completes in **131 s** (exit 0) reporting *Warmed in 131s: compact view, RIS, shared CIR, parse cache*, and the next `ask . --compact` on that repository answers in **2,2 s**. One change of its own: the warm no longer captures the child's stdout (`stdout=DEVNULL`), because it reads only the exit code and, on failure, stderr — a repository-sized payload was being buffered in the parent to be dropped |
|
|
251
262
|
|
|
252
263
|
## Class C4 — Discoverability (the ceiling as of 3.2.0)
|
|
253
264
|
|
|
@@ -344,13 +355,48 @@ contributes nothing to the graph looks exactly like a file with nothing in it.*
|
|
|
344
355
|
it, which is what audit #28 built. The class keeps its name and loses its claim about
|
|
345
356
|
provenance: E-3 was found from outside, on the surface where silence is loudest.
|
|
346
357
|
|
|
358
|
+
As of 5.7.3 the parenthetical is not merely falsified but inverted: **E-18 through E-33
|
|
359
|
+
were all found in the field**, by two adversarial evaluations run four hours apart
|
|
360
|
+
(`adorsys/keycloak-config-cli`, 227 files; BroadleafCommerce, 2 985). Silence is now the
|
|
361
|
+
class's subject, not its provenance.
|
|
362
|
+
|
|
347
363
|
| ID | Defect | Found | Severity | Status |
|
|
348
364
|
|---|---|---|---|---|
|
|
349
365
|
| E-1 | **Prose opened a declaration, and the file left the graph.** The declaration joiner decided whether a line *starts* a type declaration by scanning raw text. A comment carrying `class`/`interface`/`enum`/`record` followed by an identifier — `/** Implements the interface only for the legacy path. */`, or a trailing `// starter class is not an utility class` on an annotation — matched, so the join swallowed the annotation and the real declaration into one entry beginning with `@`, which the main loop reads as a pending annotation and never examines. The file then yields **zero symbols**: no type, no endpoint, no transaction boundary, nothing | 3.7.0 (battery, while building the C3-13 fixture) | Medium | **closed 3.7.0**. Comments are stripped before the keyword scan, block state carried across lines and string-literal aware; the join's terminating `{` must also be code, so a commented brace no longer cuts a real multi-line declaration in half. Measured A/B over six repositories (petclinic, eureka, open-banking-gateway, openmrs-core, Broadleaf, keycloak): **open-banking-gateway recovers 6 types — three of them Spring Boot application classes** — and **no repository loses a single symbol** |
|
|
350
366
|
| E-2 | **The security configuration scan cannot tell code from a comment.** `security_config_scan._scan_java` and `_scan_descriptor` match on raw text with no comment model. Reproduced at unit scope: `SEC-004` is returned identically for a live statement, a `//` comment and a `/* */` block; `SEC-007` identically for a live and a commented-out XML block. **Measured cost in the field: 4 of 13 `high` findings were false — 31 %** — and the damage is worse than the count, because the correlation engine wrote a persuasive paragraph over a commented-out block (*"these deployments share one credential, so the least protected decides the strength of all"*). **Confident prose about dead code is worse than a terse warning: it invites trust.** The same blindness runs in reverse and costs a *finding*: a transport-guarantee constraint that is commented out is invisible, so a mitigation somebody switched off is never reported, and it aggravates a cookie finding reported beside it. E-1 fixed exactly this class for the declaration joiner in 3.7.0; the configuration scan was written afterwards without the lesson | 4.5.1-class (eval #10) | **High** — it is the precision axis (scored 5,5/10) and the reason the evaluator concludes the output *"no es publicable sin verificación humana"* | **closed 4.5.3** — one authority, `source_text`, wired at the **three points a file is read** rather than inside each rule, so a rule added later is covered by construction instead of by whoever remembers. Comments are **blanked, not deleted**: offsets and line count are preserved, so every observation still points at the line a reader can open (asserted). Two properties are asserted as hard as the fix, because getting either wrong trades one wrong answer for another — **string literals survive** (`hibernate_strat._strip_comments_strings` blanks literal *content* on purpose, and these rules carry their evidence *inside* the literal: `MessageDigestPasswordEncoder("SHA-1")`, `<param-value>admin:{SHA-256}…`), and a `//` or `/*` **inside** a string is not a comment (`"http://example.com"`). A/B over the battery: BroadleafCommerce, openmrs-core, keycloak and spring-petclinic report byte-identical security findings — the fix removes false positives and loses no true one. `source_text.commented_spans` is published unused, so the *reverse* half — a control that is present but switched off — has a seam to be reported from rather than a second parser (DEAD-001, queued as F-A) |
|
|
351
367
|
| E-3 | **A handler that inherits its `@RequestMapping` loses the authorization annotation it declares, and 54,7 % of a monolith's HTTP surface is published as unguarded.** The inheritance projection in `_build_route_surface` indexes a projected route under the **declaring** member — the abstract method on the superclass that owns the mapping — and every security lookup downstream reads that symbol. The gate lives on the `@Override` in the concrete controller, which carries no mapping annotation and is therefore never consulted. Census on the subject (3 574 endpoints, parser validated 15/15 against hand-checked cases): **1 955 handlers inherit their mapping, carry a live gate in source, and are published `none_detected`**; 909 that declare their own mapping and carry the same gate are published `custom_gate_inferred`; 363 inherit and genuinely carry nothing; 115 declare and carry nothing; 111 carry a gate that is commented out (correctly `none_detected`, DEAD-001). The shape is the generic CRUD base class — one abstract class with the mappings and no security, 235 controllers extending it. Runtime semantics confirm the direction: the aspect's pointcut is `@Before("@annotation(…)")`, which matches the executing method — the override — so the annotation **is** applied and only the reading stops at the declaration. **Propagates to eight commands**: `endpoints`, `spring-audit`, `posture`, `data-exposure`, `risk`, `impact`, `enrich`, `audit-report`, plus `retrieve endpoint-security|security-surface`. It also retires this repository's own headline: *"2 635 endpoints with no security policy (73,7 %)"* is really **≈659** | 5.3.1 (audit #28) — **the first E row found in the field** | **Critical** — the largest single mis-classification this ledger has recorded, running in the unsafe direction, on the axis the product is bought for, in the answer the field quotes most | **closed 5.4.0** — the projection now names the member that runs. A projected route carries `effective_symbol` (the override where one exists, the declaration otherwise) beside `symbol`; the phase-3 security projection reads a gate the override declares for itself **method-level only**, so the fix can add a policy and never widen one away; and both `custom_gate_inferred` carry sites — the assembled route surface every CIR consumer reads, and the `endpoints` extractor — read the annotation set of **both** members of the pair. Second half, same root and found while fixing it: `_infer_gate_annotations` classified an inherited-mapping override as a non-handler, which puts the only members carrying the gate on the wrong side of the specificity ratio — diluting it at this subject's scale and **suppressing the gate outright** on a repository where most controllers extend a generic base. A member the route surface publishes as a route is a handler. 7 regression assertions over both surfaces and both vocabularies (a repository-declared gate and `@PreAuthorize`), **5 of 7 red on the previous build**; the two that are green are the negative controls — an ungated override stays ungated, because a fix in this direction must not invent a policy either. Suite 8 149 |
|
|
352
368
|
| E-4 | **E-3's residue: the annotation-to-method pairing is decided on raw text, so a blank line switches a security verdict off.** 24 endpoints in 14 controllers still publish `none_detected` with a live gate, out of the 1 955 E-3 corrected, and they partition perfectly into two lexical triggers. **(a) 18 endpoints** — a blank line between the annotation and the declaration (`@Override`, `@M3FiltroSeguridad(...)`, *blank*, `public List<MutuaDto> obtenerTodos()`): **0/18** detected, against 1 931/1 937 = 99,7 % where no blank line intervenes. `repository_ir` clears `pending_anns` in the fall-through at the end of the per-line loop, and the only blank-line guard sits **inside** the block-comment branch, so a genuinely empty line reaches the reset and empties the buffer. **(b) 6 endpoints** — a space between the type and its generic argument (`public List <CategoriaProfesionalDto> obtenerTodos()`): **0/6**. The `return_type` group is `[\w.<>\[\]?,]+`, which admits no space, so `List` matches, `\s+` eats the space, and `(?P<name>[a-z_]\w*)` is offered `<Categoria…>` and fails — **the whole declaration fails to match and the method is dropped**, which is the exact loss the regex's own comment three lines above warns about for modifier-position annotations. Java applies the annotation identically in both forms. In every one of the 14 controllers only `obtenerTodos` fails while its siblings (`crear`, `borrar`, `actualizar`, `obtenerPorId`) resolve. **E-3's 7 regression assertions cover neither form** | 5.4.0 (audit #29) | **Medium** — small population, unsafe direction, and it shows the pairing is still lexical rather than structural | **closed 5.4.1** — two edits, one per trigger. A blank line is skipped before the fall-through that clears the pending-annotation buffer: Java binds an annotation to the next declaration whatever whitespace separates them, and nothing about an empty line is a statement the reader must forget. The `return_type` group admits a space before the generic argument, so `List <Dto>` matches and the method exists again with its endpoint, its gate and its return-type edge. The spelling is normalised where it enters `stable_id` — `List <Dto>` and `List<Dto>` are one type and may not become two identities — and only the space *before* a bracket is removed, so `Map<String, List<X>>` keeps the id it already had. Four assertions added to E-3's battery, all four red on the previous build's source: both forms on the assembled route surface, both on the `endpoints` extractor, and the identity. ⚠ The class of defect is not closed by this row: the pairing is still lexical, which is what F-BP is queued for |
|
|
353
|
-
|
|
369
|
+
| E-5 | **A route without its own recorded `source` defaults into the Spring MVC bucket, so a JAX-RS-only repository is reported `spring_detected: true`.** `repository_ir.py:6693`: `"source": route.get("source", "annotation")`. `repository_ir.py:7126`: `coverage.detected_styles.spring_mvc_annotations` sums every route whose `source == "annotation"`. JAX-RS routes are identified separately (`framework == "jax_rs"`, line 7131) but are never given their own `source` key, so they fall into the same default and are counted in **both** buckets. Reproduced on four independent repositories: neo4j (0 `org.springframework` imports anywhere in the tree, `spring_detected: true` published anyway, golden-repo battery #1) · killbill (282 pure-JAX-RS endpoints, all double-counted, battery #2) · examples (`OrdersService`'s JAX-RS routes counted into `spring_mvc_annotations: 3` alongside `jax_rs: 3`, battery #1) · tutorials (`spring_mvc_annotations: 2275` equals the entire `by_source.annotation` population, `jax_rs: 172` counted a second time on top of it; sum of `detected_styles` exceeds `total` by 236 — direct proof of overlap, battery #2). `migrate-check` classifies the same repositories correctly (`spring_present: false` on neo4j and killbill), so the correct classification signal already exists in the codebase and is not shared with the endpoint-source default | 5.7.0 (golden-repo battery #1 2026-08-14, #2 2026-08-15) | **High** — a `core`-tier command (`endpoints`) and the `spring_detected` field `spring-audit` also publishes both assert a confident falsehood on a repository with zero real Spring usage, reproduced on 4 of 16 repositories tested | **closed 5.7.1** — the annotation family is recorded where the route is built, at **both** construction sites. Phase 2 already computed it for `framework` and now writes `source` from the same condition, so the two fields cannot disagree. Phase 3 — the inheritance projection — was the half the triage missed: it is not Spring-only, and it builds its own dict. neo4j's four `/tx` routes are declared by an abstract **JAX-RS** resource (`AbstractCypherResource`) and projected onto `CypherResource`; with no family recorded they fell to the same default, which is why fixing phase 2 alone left neo4j reading `spring_mvc_annotations: 4` on a repository with zero Spring. The family is read from the declaring symbol's annotations rather than from phase 2, because an abstract parent emits no route of its own and `routes` has no entry to answer from. Third edit, same row: `detected_styles.jax_rs` was the one style counted over the **raw route surface** while its three siblings counted the published census — the actual source of *"styles sum past `total`"*, which was two denominators, not only double counting. It now counts the census, and `by_source["jax_rs"]` is the same number. `_SEMANTIC_MODEL_ENDPOINT_SOURCES` gains `jax_rs`: these handlers **are** analysed by `spring-audit`, so splitting them out of the `annotation` bucket may not shrink `semantic_model_endpoints` — the parity assertion is carried as a negative control. Measured: neo4j `spring_mvc_annotations` 4 → **0** (10/10 jax_rs) · killbill 282 → **0** (282 jax_rs, styles sum 564 → 282 = total) · keycloak 695 → **0** (695 jax_rs) · examples 3 → **0**; spring-petclinic (17) and BroadleafCommerce (268/4/1) byte-identical. 6 assertions, **4 red on the previous build**, the two green ones being the negative controls. Suite 8 351 (21 previous reds unchanged). **Re-measured on the full golden set before the bump** (23 repositories × `endpoints`/`spring-audit`/`posture`, A/B against `3ebd2b6`, `--no-cache`, `ASK_READONLY=1`): **62 of 69 pairs byte-identical**, 7 moved and every one in this direction, 0 status or exit changes. The battery found a **fifth** repository the row did not name — eureka, 18 endpoints `annotation` → `jax_rs`, `spring_mvc_annotations` 18 → 0. It also moved one number that is **not** a correction to a route: jobrunr's `detected_styles.jax_rs` 3 → 0, because its three JAX-RS routes belong to `org.jobrunr.quarkus.it.JobRunrFunctionalityResource` in the integration-test module and never entered the census. Verified not to be a loss: the payload publishes them in `test_util_endpoints` with `scope: "test_util"` and `test_util_excluded: 3`, each now carrying the `source: "jax_rs"` this row added. Same shape on keycloak (764 raw routes → 695 census). ⚠ **The row's own headline is half wrong and that half is NOT closed here**: `spring_detected` is not computed from `source`. Its one authority is `spring_model.spring_detected` — a bean container or a declared transaction boundary — so neo4j still publishes `spring_detected: true` after this fix. That half is **E-12**: bean detection matches the annotation's simple name, and neo4j declares its own `@Service` |
|
|
370
|
+
| E-6 | **A module-name heuristic meant for `test-framework`-shaped modules excludes a real, deployed Spring Boot surface, and the top-level fact is published with no gap flag.** `repository_ir.py:6363-6382` (`is_test_source_file`): a file under `.../src/main/...` is correctly not a test by the file-level authority (`test_sources.is_test_path`), but the function separately excludes it when the enclosing module's path prefix has any segment `p.lower().startswith("test")` (line 6375) — written for modules like `test-framework`/`test-providers`/`testsuite`, which the docstring says "have no product of their own." Apache SkyWalking's own e2e harness is rooted at a top-level directory literally named `test/` (`test/e2e-v2/java-test-service/*/src/main/java/...`), which matches the rule on the first path segment even though the files under it are real `@RestController`/`@GetMapping` classes (`HealthController`, `AlarmController`, `LogController`) — not test infrastructure with no product. Effect: `ask endpoints` on the full repository returns `total: 0`, `spring_mvc_annotations: 0`; `ask spring-audit` returns `spring_detected: false`, for a repository with confirmed (narrow, 23-file) real Spring usage. `--include-tests` recovers all 20 routes, and running `ask endpoints` directly on the submodule finds them without any flag — proving detection works once the module-exclusion is bypassed. `coverage.gaps` stays empty on the default run; the exclusion note (`test_source_excluded_detail`, "29 route annotation(s) from test source trees excluded") reads as routine test filtering, not as "an entire production-shaped module was treated as test infrastructure" | 5.7.0 (golden-repo battery #2 2026-08-15) | Medium — a judgment-call heuristic operating exactly as designed for its intended target, overreaching on a repository whose harness happens to be named `test/`, but landing on the same headline field (`spring_detected`) E-5 does, with no hedge published | **closed 5.7.2 — the disclosure half, and the admission left standing on purpose.** Whether an e2e harness rooted at `test/` is product or infrastructure is a judgement, and overturning it on one repository would move surface on twenty-three; what was indefensible was publishing it as routine test filtering. `test_source_excluded_detail.excluded_by_module_name_rule` now names the modules, counts the route annotations, states that the rule judges the **module** rather than the file, and gives both ways back (`--include-tests`, or run the command on the module path); `coverage.gaps` carries the entry, because that list is what a reader checks before trusting a zero. Only the module-name branch is reported — `is_test_path` never fires inside a declared main source root, so it cannot remove a deployable module. Measured: skywalking names 3 modules and 29 route annotations where it named none; spring-petclinic (17) and jobrunr (12) gain nothing. ⚠ Residue, deliberate: `spring-audit`'s `spring_detected: false` still carries no hedge on this repository — the exclusion fact does not reach that surface, and wiring it there is its own row |
|
|
371
|
+
| E-7 | **A security-policy classifier regex matches a method's own declaration text, not a call site, and the resulting false flag is cached per-file and leaks onto a handler explicitly marked bypass-all.** On sa-token (a non-Spring-Security Java auth framework with its own `@SaCheckLogin`/`@SaCheckPermission`/`@SaIgnore` annotations), `ask endpoints` labels `AtCheckController`'s handlers `policy: "programmatic"` although no `sourcecode.config.json` custom-security declaration exists (`ask config` confirms "Declaration: none"). The only evidence found is a regex matching the literal text `checkPermission(` in the file — which matches the demo's own method **declaration**, `public SaResult checkPermission()`, not a call to a security check. Reported at `repository_ir.py:394-408`/`:5064-5076` (field-agent-located; not independently re-read this session, one level less verified than E-5/E-6/C1-47/C1-48). The flag, once set, is applied to every handler in the file lacking its own recognized policy — reaching `ignore()`, a handler annotated `@SaIgnore` (sa-token's explicit "skip all checks" marker), which is reported as `programmatic`-protected, the opposite of what the annotation on it says. Contamination reproduces in `impact-chain`'s `endpoints_affected` for the same controller | 5.7.0 (golden-repo battery #2 2026-08-15) | **High** — this is an auth-verdict false positive on the exact axis `ask risk`'s own documentation says its composition depends on (`severity_effective = ... × auth_verdict × ...`); a wrongly-`programmatic` handler would suppress severity on a genuinely open endpoint | **closed 5.7.1** — **re-verified at source first, as this row required**, and the field report was exact. `_PROGRAMMATIC_SECURITY_RE` matched `\b(?:…|checkPermission|…)\s*\(`, which is the shape of a *call* and equally the shape of the *method that declares one*. On `AtCheckController` the regex finds **one** match in the entire file — line 46, `public SaResult checkPermission() {` — and on that evidence all **seven** handlers were published `policy: programmatic`, `ignore()` among them, which carries `@SaIgnore`, sa-token's explicit skip-all-checks marker: the verdict stated the opposite of the annotation on the method. Repo-wide the false population was **23 endpoints (programmatic 41 → 18)**, and every one of the 15 surviving files was re-read and carries a real call site (`StpUtil.checkPermission("")`, `stpLogic.hasRole(role)`, `SaRouter.match(…)`). The discriminator is what precedes the name on its own line: a call is led by a receiver, an operator, a bracket, a statement boundary or one of `return`/`new`/`throw`/…; a declaration is led by its return type — an identifier, a closing generic, or a closing array bracket. ⚠ **The first attempt introduced a false negative and the cross-repository A/B is what caught it**, not the suite: applied to the whole pattern the filter also ran on the receiver alternatives, and `ReactiveSecurityContextHolder.getContext()` reads as identifier-then-match — exactly what a declaration looks like — so spring-security-samples' reactive `MeController` silently lost a correct verdict (that repository ships **two** `example.MeController` classes, which is how it surfaced). The pattern is now split: the ambiguous `name(` half is filtered, the unambiguous receiver/member/`throw` half never is, and that negative control is the sixth assertion in the battery. **Measured across all 23 golden repositories: 22 of 23 policy censuses byte-identical**, sa-token the only mover — BroadleafCommerce (12), keycloak (147) and spring-security-samples (3) unchanged. 6 assertions, **3 red on the previous build**, the 3 green ones being the negative controls. Suite 8 761. ⚠ Residue this row does not close: sa-token's handlers now read `none_detected`, which is honest but incomplete — `@SaCheckPermission` is a vendor annotation this analyzer models only when declared in `sourcecode.config.json`, and that is the documented custom-security path, not a new gap |
|
|
372
|
+
| E-8 | **`impact-chain` silently merges unrelated same-named classes from different modules into one answer; `impact` on the identical symbol correctly refuses.** tutorials (Baeldung's mega-repo, hundreds of independent modules) has 4 distinct classes named `PersonService` in unrelated modules. `ask impact PersonService <repo>` returns `resolution: "ambiguous"` and lists all 4 `matched_fqns`. `ask impact-chain PersonService <repo>` returns `resolution: "partial"`, but its `direct_callers` mix classes from two different, unrelated modules (`com.baeldung.activej.*` and `com.baeldung.hibernatejfr.*`) into one merged answer and a single risk score (25.0, `critical`), with no field disclosing which of the 4 classes contributed which caller. The command that should have said "ambiguous" said "partial" and guessed | 5.7.0 (golden-repo battery #2 2026-08-15) | Medium-High — only demonstrated on a repository large and diverse enough to carry real name collisions across modules (tutorials, 24 074 java files — the largest repository this battery has run against); a confident composite risk score built from an admittedly-ambiguous symbol resolution is the opposite of `impact`'s own behavior on the same input | **closed 5.7.2** — on C1-41's precedent rather than a second one of its own. The facts stay (every caller listed is a real caller of *some* candidate, and dropping them would hide reach the reader asked about); the figure that reads as *this symbol's risk* is void — `risk_score` and `risk_score_raw` null, `risk_level` `unknown` — with the reason published beside the null in `risk_score_basis`. The candidates are named in `metadata.matched_classes` / `matched_classes_count`, and both a warning and the explanation say so, because an agent quotes the explanation and a script reads the metadata. `impact-chain` is `core` tier, so no published enum value is invented: `resolution` stays `partial` and `risk_level` uses `unknown`, which its schema already documents, and every new key appears only on an ambiguous match — an unambiguous answer is byte-identical. Measured on tutorials: 4 candidates named where the payload named none, `high`/25.0 replaced by `unknown`/null, the six direct callers still listed. Regression: `tests/test_ambiguous_chain_e8.py`, 8 assertions |
|
|
373
|
+
| E-9 | **An unresolved JAX-RS path's caveat is dropped between `endpoints` and `impact-chain`, and the path's embedded regex syntax collides with the `endpoint_id` field separator.** killbill's `AccountResource` declares `@Path("/{accountId:" + UUID_PATTERN + "}")` (string-concatenated). `ask endpoints` correctly marks it `path_resolution: "unresolved"`. `ask impact-chain AccountResource <repo>` shows the same route without that caveat and truncates the path to `"/{accountId:"`, and because the embedded `:` collides with the `endpoint_id` separator the emitted id is malformed: `DELETE:/{accountId::org.killbill.billing.jaxrs.resources.AccountResource:...#closeAccount` | 5.7.0 (golden-repo battery #2 2026-08-15) | Low | open |
|
|
374
|
+
| E-10 | **Malformed Java is silently absorbed into the symbol export with full confidence, and the parse failure is not named anywhere in the payload.** spaghetti-api's `BrokenSyntax.java` has 2 open braces and 0 close braces — invalid Java, confirmed by brace count and by reading the file. The root command's `contracts` export nonetheless reports it with a clean `class BrokenSyntax` export plus a `method` export (signature `()->void`), no `parse_error`, absent from `analysis_gaps` — and it never surfaces in `spring-audit`, `migrate-check` or `risk` output either, no "N files failed to parse" anywhere in any of them | 5.7.0 (golden-repo battery #1 2026-08-14) | Medium — a file that cannot be Java is exported as if it parsed cleanly, on the exact axis this repository's own CLAUDE.md is most explicit about ("never a confident falsehood... unknown, never 0") | **closed 5.7.3** — the recovery stays (a file that fails to compile still tells us its type exists); what changes is that it is qualified. A `parse_structure` gap names every file whose braces do not balance **once comments and strings are masked**, and states what is missing from the model: the members after the unclosed block and the type's own end. Distinct from `parse_coverage`, which fires on *total* extraction absence — this one fires when extraction succeeded over source that cannot be complete, and both readings of "this file was not fully read" now live in `reconciliation`. Cost is a `str.count` per file; the mask, which is not cheap, runs only for the rare file that already looks unbalanced, so a brace inside a string or a comment is not a false positive (asserted). Measured on spaghetti-api: `BrokenSyntax.java` named, its symbols retained. ⚠ Residue: the gap reaches every CIR consumer through `analysis_gaps`, but the root command builds its own list from `ConfidenceAnalyzer`, so `ask <repo> --compact` still does not carry it. Regression: `tests/test_parse_structure_gap_e10.py`, 7 assertions |
|
|
375
|
+
| E-11 | **`impact`'s DI-interface caller resolution fabricates direct callers that never reference the target, inflating the risk band; `impact-chain` on the identical symbol does not.** On examples, `ask impact OrdersService` reports `critical` (score 35.5, 4 direct + 35 indirect callers); `ask impact-chain OrdersService` reports `medium` (score 11.0, 1 direct caller) for the same repository state. The extra "direct callers" `impact` adds — `MicroserviceUtils` and its two nested classes — never reference `OrdersService` anywhere (grepped, 0 hits); they only import the shared `Service` interface `OrdersService` implements. `impact`'s own explanation names the mechanism: `"callers resolved via interface (Service) — Spring/CDI/Guice DI pattern"` — the resolver treats any class touching a common interface as a caller of every implementer of it. This compounds C1-48 (the two commands already score from different formulas) with a caller **set** that disagrees on its own, independent of which formula is applied to it | 5.7.0 (golden-repo battery #1 2026-08-14) | Medium-High — same family as C1-21 (a fabricated identity reported as protection): here a fabricated call edge is reported as reach, on the exact factor `ask risk`'s reachability axis depends on | **closed 5.7.3** — the DI recovery is right and stays: a container binds implementations to interfaces, and a walk that stops at the interface loses every real production dependent (C1-16). What was wrong is the name it was filed under. A class that reaches the target only if the container binds *this* implementation sits at the same conditional distance as a transitive caller, not at the distance of a class that names the target. `interface_mediated_callers` / `interface_mediated_caller_count` publish the population, `via_interface_note` says what it is and is not, the explanation carries both figures, and they are scored on the **indirect** axis (0,5) rather than the direct one (2,0). Reach is untouched — they remain BFS seeds and members of the blast cone, so endpoints, transactions, mappers and cross-module reach are unchanged. Measured across 8 symbols on 7 repositories: **7 byte-identical**, including BroadleafCommerce `Money` (134 direct, 1 584 indirect) and keycloak `UserResource`; `examples/OrdersService` moves 4 → 1 direct, 35 → 38 indirect, 35,5 → 31,0, `endpoints_affected` unchanged at 3. Regression: `tests/test_interface_mediated_callers_e11.py`, 7 assertions |
|
|
376
|
+
| E-12 | **Spring bean detection matches an annotation's *simple name* with no check that it is Spring's, so a repository that declares its own `@Service` is published `spring_detected: true`.** `spring_model.py:41-46` (`_BEAN_ANNOTATIONS`) holds bare spellings — `@Component`, `@Service`, `@Repository`, `@Controller`, `@RestController`, `@Configuration`, `@Bean` — and `BeanGraph.build` (`:184-202`) admits a node whose `annotations` list intersects that set. The list carries simple names only, so the match is on the token, not on what it resolves to. `spring_detected` (`spring_model.py:455`, the F-AY single authority) is then `has_spring_beans() or tx_total > 0`, and the bean half decides alone. **Measured on neo4j**: `grep -r org.springframework` over the whole tree returns **0** occurrences and the graph holds **0** edges mentioning it, while `import org.neo4j.annotations.service.Service` appears **199** times — neo4j's own SPI marker, declared at `annotations/src/main/java/org/neo4j/annotations/service/Service.java`. `BeanGraph.build` over the CLI's own CIR (5 598 files) returns **34 beans, every one stereotype `service`**, all of them that annotation; `tx_index.stats()["total"]` is **0**. So `ask spring-audit neo4j` publishes `spring_detected: true` in the same payload as `tx_stats: {total: 0}`, under both `--scope security` and `--scope tx`. The discriminating evidence is **already in the graph and already walked**: pass 1 of the same function iterates annotation-type nodes to build the meta-annotation map, and one of those nodes is `org.neo4j.annotations.service.Service` — a repository that declares the annotation itself is the witness that the token is not Spring's. Negative control: killbill (0 `org.springframework`, 0 bare bean annotations) correctly publishes `spring_detected: false`. **Not observed**: no false finding fires on neo4j today (`total_findings: 0`), so this row is against the published fact and what reads it, not against a finding — but `has_spring_beans()` is also the AOP-premise witness (`spring_security_audit.py:638-642`), the one C2-era fix installed so a self-invocation finding cannot fire where nothing proxies, and on this repository that premise is satisfied by a non-Spring annotation. **Propagates to every surface that reads the authority**: `spring-audit` and `spring-audit --scope tx` (`spring_tx_analyzer.py:1039`), `posture` (`posture.py:1207`), `risk` (`risk.py:1593`), `ris.py:355`, and the MCP orchestrator's `repo_type: "java_spring"` (`mcp/orchestrator.py:360`). ⚠ Unquantified exposure: sa-token (134) and dubbo (164) also carry bare `@Service`/`@Component` spellings — dubbo does use Spring, so its verdict may be right for the wrong reason; neither was measured this round | 5.7.0 (split out of E-5 while closing it, 2026-08-15) | **High** — this is the vendor-agnostic rule this repository enforces, inverted: the logic branches on a proprietary name and treats it as the rule rather than as evidence, so the blast radius is every repository that spells an annotation the way Spring does, and the field it decides is the one that says whether the Spring axes are measuring this repository at all | **closed 5.7.1** — the annotation is **resolved**, not merely spelled. `BeanGraph.build` walks the `imports` edges the graph already carries (109 475 of them on neo4j, indexed by the same class FQN the bean nodes use) and asks where the spelling binds, in the order the compiler would: an explicit single-type import **decides it either way**; a wildcard import of a Spring package leaves Spring reachable, so the spelling stays admissible; failing that, an annotation this repository declares in the owner's own package is the owner's own; and **nothing resolved stays `None`** — an absent import edge is not evidence, so the node keeps exactly the behaviour it had rather than acquiring a verdict from silence. The node is not deleted: `BeanNode.spring` records the verdict and `annotation_fqn` records what it bound to, so `get_stereotype` still answers for `explain` and only the **Spring** claim is withdrawn — `has_spring_beans()` is what filters. A meta-annotation is decided by what it *carries*, not by its own name: `@DomainService` is repository-declared by definition, so resolving its own spelling would have rejected every meta-bean the codebase deliberately supports — the question asked is where the `@Service` **on it** binds. The two annotation passes were split so the same-package map is complete before any resolution reads it, and the import walk shares the edge traversal that was already being made for the injection edges. **Measured**: neo4j 34 beans → **0 Spring** (all 34 bind to `org.neo4j.annotations.service.Service`), `spring_detected` **true → false**, and the `stack_fit` block — the honesty affordance that tells the reader to read the Spring axes as `unknown` rather than `none` — is **now published on that repository, where it was suppressed before** (verified by stashing the fix and re-running). Two more repos were silently wrong and are now right: sa-token had **35** beans bound to Solon (`org.noear.solon.annotation.*`) and loveqq, jobrunr **1** bound to Micronaut (`io.micronaut.http.annotation.Controller`) — both repositories still read `spring_detected: true` on their genuine Spring beans, so the correction shows up in the bean census rather than the headline. **Negative controls, unchanged**: spring-petclinic (12 Spring beans, 9 correctly withheld as `jakarta.persistence.*` entities the stereotype rule already excluded), dubbo 133/133, mall 158/158, spring-boot-admin 199/199, sagan 76 with 12 JPA withheld, killbill and eureka still `false`. 9 assertions, **7 red on the previous build**. Suite 8 360, the same 21 previous reds. **Re-measured on the full golden set before the bump**: in the 23-repository A/B battery neo4j's `spring_detected` is the only headline that moved, and the two independent authorities now **agree on all five repositories checked** — `migrate-check`'s `spring_present` and `spring-audit`'s `spring_detected` read false/false on neo4j, killbill and eureka, true/true on examples and spring-petclinic; before this fix neo4j was the one disagreement. Cost: `BeanGraph.build` goes from 7,2 → 17,7 ms on skywalking, 28,2 → 58,8 ms on keycloak and 41,7 → 82,4 ms on neo4j (284 338 edges), best-of-3 — the import walk roughly doubles a step that is tens of milliseconds inside a multi-second command. The three apparent `spring-audit` slowdowns in the battery log (skywalking +259 %, open-banking-gateway +200 %, mall +162 %) were **contention noise, not this fix**: re-timed in isolation they are 2,5 s / 1,2 s / 1,1 s against baselines of 2,7 s / 1,3 s / 1,3 s. ⚠ Residue this row does not close: `@Bean`/`@Configuration` on a class with no import edge for the name still falls to the unresolved branch (137 of spring-boot-admin's 199, 87 of dubbo's 133) — admitted, which is the direction that preserves behaviour but is not a resolution; and `next(iter(match))` still picks arbitrarily when a node carries two bean annotations |
|
|
377
|
+
| E-13 | **No field in any repository carries a `contained_in` edge, so `ask verify` — the `core`-tier CI gate — returns `pass` on a repository that violates its own declared contract.** `repository_ir.py:3041-3052` emits `contained_in` for `sym.type in ("method", "field")`, and the owner is computed by `_enclosing_class` (`:3833`), which splits on `#` only. A method is spelled `Type#member` and resolves; a **field is spelled `Type.member`** and is handed back unchanged, so the `enclosing != sym.symbol` guard on the next line drops it. Measured on four repositories: **0 of 14** fields on spring-petclinic, **0 of 821** on mall, **0 of 1 086** on neo4j, **0 of 2 188** on keycloak. The consequence is a silent gate. `verify_rules.ForbiddenEdgeRule._endpoint_matches` is correct by design — it matches a selector against the edge endpoint **or the type that declares it**, resolved through `_owner_type_map`, precisely because *"edges are recorded at member granularity but client invariants are stated at type granularity"* — and it starves: for the shape `@RestController class Ctrl { @Autowired OrderDaoJpa dao; }` the graph holds `injects com.example.Ctrl.dao → com.example.OrderDaoJpa` and the `@RestController` lives on `Ctrl`, which the matcher can never reach. On mall, **176 of 181** `injects` edges originate at a field and are orphaned from their class this way. **How it survived is the second half of this row**: a test named `test_field_has_contained_in_edge` existed for exactly this property and asserted nothing — its filter (`"." in e["from"].split(".")[-1]`) cannot match, because the last dot-segment of an FQN never contains a dot, and `assert len(field_edges) >= 0` is true of every list. It passed green for as long as the defect existed | 5.7.0 (triage of the 21 standing red tests, 2026-08-15) | **Critical** — a gate that cannot fail is worse than no gate: a team switches CI on, sees green, and concludes the contract holds. `ask verify` is `core` tier, which this repository defines as *"safe to gate CI on"* | **closed 5.7.1** — one condition at the emission site. The dot is ambiguous with a package segment, so the last segment is stripped **only when what remains is a type this repository declares** — membership in `_local_classes` is the evidence, never the spelling, and `_enclosing_class` itself is untouched because its `#` behaviour is right for every other caller. **11 of the 21 standing red tests went green with this one edit** (`test_verify_repo.py` ×9, `test_contracts_file.py`, `test_verify_edit_v5.py::TestForbiddenEdge`) — they were never stale tests, they were an unread bug report. Collateral measured and nil: the 23-repository battery is **69 of 69 pairs byte-identical** across `endpoints`/`spring-audit`/`posture`, and `impact` — the one consumer that walks `contained_in` — returns identical band, score, caller and endpoint counts on spring-petclinic (×2), mall and sagan. Two assertions replace the vacuous one: the witness edge, and the general property that **no** member symbol is left without an owner. Suite 8 756 (21 → 10 reds). ⚠ Residue, deliberately not fixed here and filed as its own row: `BeanGraph.injections` is empty on all four repositories measured, because it keys on `frm in beans` where beans are class FQNs and the edges originate at fields and constructors. It has **no consumer in the codebase** — fixing dead structure to move a score is not a fix |
|
|
378
|
+
| E-14 | **A 3-level Python re-export chain resolves through a documented 2-level limit, and nothing is reported.** `test_reexport_chain_limit`'s own docstring states the contract — *"El tercer nivel NO debe resolverse (chain limit = 2). limitations debe contener algun indicador del limite alcanzado"*. Measured on its fixture (`a/__init__.py` → `b/__init__.py` → `c/module.py`, consumed from the root): the `SymbolLink` for `deep_func` resolves to **`a/b/c/module.py`** with `is_external: false`, and `summary.limitations` carries only `namespace_package:a/b/c` — no chain indicator. Either the limit is not enforced or it is enforced and unreported; both are the shape this ledger calls a confident answer past the edge of what was measured. **Found the same way as E-13 and by the same defect in the suite**: the test ended in `assert ... or True`, which accepted every outcome including this one | 5.7.0 (vacuous-assertion sweep, 2026-08-15) | Low-Medium — Python import resolution is not the Java/Spring core this product is bought for, and no headline field is built on it; the row exists because the property is declared in the code and is not held | open — **tracked, not forgotten**: `test_reexport_chain_beyond_the_limit_is_reported` asserts the intended property under `xfail(strict=True)`, so the day it is fixed the suite turns red and forces this row closed rather than letting the fix land unnoticed |
|
|
379
|
+
| E-15 | **No repository has ever published a `field_type`: the class-scope type-reference surface keys its output by the field's own FQN, so the first context named in its own acceptance claim is empty everywhere.** Same root as E-13, second consumer, found by triaging the reds E-13 left standing. `repository_ir.py:2381` (`_build_class_type_refs`) does `cls = _enclosing_class(s.symbol)` for a field, and that resolver split on `#` only — so a field indexed under `com.x.Svc.repo` instead of `com.x.Svc`, and `class_type_surface` querying by class FQN found nothing. Measured on the surface's own fixture (`@Autowired private OrderRepository repo;` plus `private java.util.Map<CustomerId, Order> ledger;`): the published contexts are `param_type`, `ctor_param_type`, `return_type` — and **`field_type` and `field_type_arg` are absent entirely**. On real repositories, spring-petclinic published `{ctor_param_type: 3, param_type: 24, return_type: 18}` and sagan `{ctor_param_type: 59, param_type: 103, return_type: 95}`, both with **zero** field contexts. The module's docstring states the claim this falsifies — *"the class's declaration structure — field, field generic arg, constructor param, method param, method return — is recovered as a pure ContextGraph query"* — and the field is named first | 5.7.0 (triage of the reds left standing by E-13, 2026-08-15) | **High** — a shipped acceptance claim for a Semantic IR capability, false in two of its five named contexts, on the two contexts that describe what a class *holds* rather than what it passes | **closed 5.7.1** — E-13's patch was at one emission site; a second consumer with the same root made that the wrong shape, so the resolution is now **one authority**, `_owner_type_of(symbol, symbol_kind)`, and both sites read it. The kind is the evidence: a field symbol is built as `f"{class_fqn}.{fname}"` at the single site that makes one, so the last dot-segment is the member and nothing is inferred from spelling; a caller that does not know the kind gets the `#` behaviour alone, because a bare dotted FQN is ambiguous with a package segment and this function may not guess. `_enclosing_class` survives as a kind-less delegate so its ten existing method-side callers are untouched. Measured after: spring-petclinic gains `field_type: 6, field_type_arg: 3` and sagan `field_type: 22, field_type_arg: 10`, with the other three contexts **byte-identical** — recovered, not double-counted. Collateral nil on the 23-repository battery (**69 of 69 pairs identical**) and on `impact` (4 symbols, 3 repositories, identical band/score/callers/endpoints). **21 standing red tests are now 7**, and 14 of the 21 were this one root |
|
|
380
|
+
| E-16 | **A second, hand-written test-source filter stands beside the authority and removes production code from the model — the exact substring shape C1-2 closed in 3.2.1.** `find_java_files` asks `is_test_source_file`, which knows that a Java package named `test` under a *declared main source root* is production. Three command entry points then asked again as `[f for f in find_java_files(root) if "/test/" not in f and "/tests/" not in f]` (`repo-ir`, `impact`, `export`), and three more populations did the same by hand (`serializer._mybatis_pairing`, `serializer._bootstrap_structured`, `detectors/java._collect_transactional_classes`). Measured over the 24 golden repositories the second filter removed production sources from four of them — **sa-token 82 of 919 files (8,9 %)**, ofbiz-framework 41 of 1 075 (3,8 %), BroadleafCommerce 4, tutorials 1 — and removed them *silently*: `analysis_gaps` was empty and no payload named the loss. What it cost, on sa-token: `repo-ir` read **837 of 919 files**, published **5 807 symbols** and **154 endpoint symbols**; `ask impact com.pj.test.TestController` — a fully qualified name for a class that is on disk — answered `resolution: ambiguous` over `com.lym.controller.TestController` and `com.pj.controller.TestController`, published `risk_score: 26.0, risk_level: high` for that substitution, and reported `endpoints_affected_count: 8` against `impact-chain`'s **53** for the same symbol. Second defect in the same lines: `--include-tests` was a **no-op** on `repo-ir` and `impact`, because the flag only skipped the redundant pass while `find_java_files` was still called with its default `include_tests=False` | 5.7.3 (cross-command coherence battery, 2026-08-16) | **High** — a repository that names a package `test` is analysed with 9 % of its production classes missing from every CIR consumer, and the blast radius of a symbol is 6,6× smaller than the same product's other answer for it | **closed — on `master`, unreleased.** One authority at all six sites: the three CLI populations read `find_java_files(root, include_tests=include_tests)` and nothing else, the three list filters read `path_filters.is_test_path`. Measured after on sa-token: `repo-ir` **919 files, 6 248 symbols, 381 endpoint symbols, 902 classes** (was 837 / 5 807 / 154 / 820); `impact com.pj.test.TestController` → `resolution: exact`, one matched FQN, **53 endpoints on both commands** — the C1-16 endpoints-affected divergence on this repository closes with it. `--include-tests` now widens the population it names. Collateral nil: the coherence battery over spring-petclinic, spring-boot-admin, sagan, eureka, examples, spaghetti-api and jobrunr returns identical endpoint totals and identical `impact` scores. Regression: `tests/test_one_test_source_authority_e16.py`, 10 assertions — including one that greps `src/` for the substring shape, because the defect is a shape and it has now grown back twice |
|
|
381
|
+
| E-17 | **A fully qualified target that is absent from the model is answered by every other class that shares its class name, with a confident band.** `_resolve_target` reduced every target to its last dotted segment before matching, so `ask impact com.acme.nonexistent.TestController` on sa-token — a package that exists in no repository on disk — returned `resolution: ambiguous`, `matched_fqns` naming **four classes across three unrelated packages**, and `risk_score: 69.87, risk_level: critical` for their union. Nothing in the payload said the name asked about was not there. This is C3-35 — *being more specific made the answer worse* — which was closed for **paths** and never applied to **names** | 5.7.3 (cross-command coherence battery, 2026-08-16) | **High** — the answer is about symbols the caller did not name, and it carries the band a CI gate reads; a typo in a package returns `critical` instead of an error | **closed — on `master`, unreleased.** A target carrying a package is matched as a *qualified* suffix (`fqn == t or fqn.endswith("." + t)`), so `test.TestController` still narrows to the two classes that carry that suffix and `com.a.Widget` resolves to itself alone. When nothing carries the qualified suffix the answer is `not_found` — never another class's numbers — and the near names are offered under `candidates`, which the existing ranking already orders exact-simple-name first, so the answer stays actionable. A **bare** class name keeps the suffix affordance the CLI documents (`ask impact OwnerRepository`), because there the caller has not said which package and ambiguity is the honest reading. Measured after on sa-token: `com.acme.nonexistent.TestController` → `not_found`, `risk_level: unknown`, candidates `[com.pj.test.TestController, com.pj.controller.TestController, …]`; `TestController` → `ambiguous` over the same 4; `com.pj.test.TestController` → `exact`. Regression: `tests/test_qualified_target_resolution_e17.py`, 9 assertions, including the negative control that the bare-name affordance is unchanged |
|
|
382
|
+
| E-18 | **A JAX-RS *client* proxy is published as this repository's HTTP exposure, on an application that declares it serves nothing.** `adorsys/keycloak-config-cli` sets `spring.main.web-application-type=NONE` (`application.properties:2`) and `endpoints` reported **9 endpoints** — all of them RESTEasy client proxies built at `KeycloakProvider.java:221-228` (`getInstance().proxy(cls, uri)`) against *Keycloak's* admin API. A `@Path`-annotated interface is a route declaration in either direction, and nothing asked which direction this one runs in. Cascade: `archetype` won with *"primary surface is HTTP"* and `validation` opened 4 gaps over routes belonging to another product. **Distinct from E-5** — that one put JAX-RS routes in the Spring MVC bucket; this one puts a client in the server population | 5.7.3 (field, keycloak-config-cli @`910728c9`, F-1) | **High** — the whole exposure surface of the answer, and the two commands downstream of it | **closed (local `f6b4506`)** — one structural rule, not a name filter: a type carrying mappings is exposure only if this repository can instantiate it. An **interface no type in the tree implements** (transitive closure over interface `extends`) and that no sub-resource locator returns is not served. Nothing is deleted — `route_surface` keeps the declaration under `scope: declared_not_served`, `NON_SERVED_ROUTE_SCOPES` is the shared authority for the CIR projection and `compute_blast_radius`, and `endpoints` publishes the bucket. `validation` 4 gaps → 0, `archetype` no longer wins on HTTP. Measured on **20 golden repositories: 0 routes excluded outside the field case**. ⚠ The path-fragment filter (`/admin-client/`, name-shaped, VAI) was **not** replaced — doing so moves keycloak's total (695) and needs its own measurement |
|
|
383
|
+
| E-19 | **A nested type inherits the reference set of the file that contains it.** `KeycloakProvider.JacksonProvider` — nine lines whose only statement is `super.locateMapper` — was published as a direct caller of `ResteasyUtil` and `VersionUtil`; `ResourceTypeMapping`, a bare enum, as a caller of `CloneUtil`. Every reference the outer type makes is attributed to every type declared in the same file, so `direct_caller_count` and every score weighting it are inflated by the file's own shape | 5.7.3 (field, keycloak-config-cli, F-7) | Medium — it inflates the one figure a blast radius is read from, and it inflates it more the larger the file | **closed (local `a4c84b8`)** |
|
|
384
|
+
| E-20 | **`explain` does not read edges the graph already holds.** `explain KeycloakProvider` published `outgoing_deps: ["KeycloakConfigProperties"]` — 1 of 4. The three missing (`ResteasyUtil`, `VersionUtil`, `KeycloakProviderException`) are present in the graph and `impact ResteasyUtil` returns the reverse edge, so this is a consumer that queries a narrower relation than the one it names, not a modelling gap | 5.7.3 (field, keycloak-config-cli, F-6) | Medium — `explain` is a *Start here* command, and an incomplete dependency list reads as a complete one | **closed (local `d0fb1aa`)** — a dependency does not have to be injected to be one |
|
|
385
|
+
| E-21 | **The conditional-bean census counts a thing that is not a bean, and publishes one condition for a bean that carries two.** `@interface ConditionalOnKeycloakVersion26OrNewer` — the annotation *declaration* — was counted as a conditional bean (77 against a ground truth of 76) and was also the single `unresolved` entry, so the active/inactive lists stayed clean while the total did not. Separately, `OrganizationRepository` and `OrganizationImportService` each carry two `@Conditional*` annotations and were published with one | 5.7.3 (field, keycloak-config-cli, F-2/F-3) | Medium | **closed (local `66cc75c`)** — the census counts beans, and counts every condition |
|
|
386
|
+
| E-22 | **A build-declared client is matched against the `<properties>` block, so the scope rule is never applied and the citation points at the wrong line.** `export --integrations` reported `unboundid-ldapsdk` as a declared LDAP client citing `pom.xml:102` — a `<unboundid-ldapsdk.version>` property. The dependency is at `pom.xml:412-417` and carries `<scope>test</scope>`, which the payload's own note says it excludes. One wrong region produced both halves: the scan walked every line for the coordinate as a **substring**, and then looked for `<scope>` four lines past *that* line | 5.7.3 (field, keycloak-config-cli, F-9/F-10) | Medium — an evidence-provenance error on an evidence-first product | **closed (local `d9c5498`)** — Maven declarations come from the POM's structure: `<dependency>` blocks outside `<dependencyManagement>`, the artifactId element inside them, scope read from the enclosing block rather than a fixed window. Two further inversions fell out on golden repos: mall reported `okhttp` as a declared client **from the block that excludes it** (`<exclusion>`), and `<dependencyManagement>` pins for modules that never depend on the artifact. The coordinate published is now the one the build declares (`okhttp-jvm`, not `okhttp`) |
|
|
387
|
+
| E-23 | **`matchIfMissing` decides the prose and not the verdict.** Under `--profile dev`, 77 conditional beans stayed `unresolved` while each one's own `reason` said *"Spring registers the bean"*. The condition was evaluated well enough to write the sentence and then not applied to the answer — under-declaration, the mirror of a confident falsehood and cheaper only because it is | 5.7.3 (field, keycloak-config-cli, F-11) | Medium | **closed (local `b383342`)** — an unresolved verdict states what the container would do |
|
|
388
|
+
| E-24 | **A class reached only by reflection is published as `"Low-risk isolated change"`.** `impact OrganizationImportService` → `direct_callers: []`, `risk_level: "low"`, `risk_score: 1.0`. `RealmImportService#invokeOrganizationImportIfAvailable` (`RealmImportService.java:265-278`) invokes it on **every import run** through `Class.forName(<literal>)` + `getBean` + `Method.invoke`, so the call leaves no typed edge. `modernize` did not catch it either (`framework_dispatched: []`, `no_static_callers: 0`), so the safety net never fired. The answer to *"who calls this?"* was `unknown`, not `none` — and a plain text search finds the call site the typed graph cannot | 5.7.3 (field, keycloak-config-cli, F-12) | **High** — the C1-36 failure mode with a second reacher, and this one arrives with a positive conclusion attached | **closed (local `ab3833e`)** — reflection becomes a **fourth signal in `container_wiring`** rather than a parallel authority, so all three consumers (`impact`, `impact-chain`, `pr-impact`) inherit it. Detection reads `literal_facts`, which the IR already carries with the enclosing callee attached — nothing re-scanned, no regex over source text — and needs two independent structural facts, neither a naming convention: the literal is **byte-equal to an in-repo class FQN**, and its enclosing call is a published reflective-lookup API of the JDK or the bean factory. A log line mentioning the simple name does not match. Gated on having no caller **outside** the class, because `_has_incoming_calls` counts self-calls and would have masked every instance (the field class has nine internal call targets and no external caller). `explanation_clause` owns the sentence too: printing *"the container invokes this component"* over a reflectively-dispatched class replaces one false statement with another |
|
|
389
|
+
| E-25 | **`pr-impact` publishes `risk_level: CRITICAL` beside `risk_reason: "No high-risk signals detected"`.** Two adjacent fields of one payload contradicting each other; whichever a reader believes, the other one taught them not to trust the answer | 5.7.3 (field, keycloak-config-cli, F-8) | **High** (trust) | **closed (local `8e2a5a4`)** — a verdict and its reason answer about the same evidence |
|
|
390
|
+
| E-26 | **The integration catalogue has no JAX-RS/RESTEasy client construct, so a tool whose entire purpose is outbound HTTP reports zero integrations.** `export --integrations` on `keycloak-config-cli` → **0**, on a codebase that exists to speak HTTP to Keycloak. This is the surviving half of the F-9 field row; the provenance half closed as E-22 | 5.7.3 (field, keycloak-config-cli, F-9) | Medium — a zero that reads as "no outbound coupling" on the one axis a migration cares about | **closed (local)** — the construct was added the way every other row in the catalogue is admitted: an **FQN-guarded import edge** to a type named by a published specification. Two families, `jakarta`/`javax.ws.rs.client` (`Client`, `ClientBuilder`, `WebTarget`, `Invocation`, `Entity`) and RESTEasy's implementation (`ResteasyClient`, `ResteasyClientBuilder`, `ResteasyClientBuilderImpl`, `ResteasyWebTarget`), plus the declarative half, `@RegisterRestClient`. Nothing is read from the build: the coordinate stays where E-18 put it, in `declared_clients`, and the record here cites the file and line where the client type is imported. `Client`, `Entity` and `Invocation` are ordinary words, so each is admitted **only** from the spec's own client package — which is what keeps `jakarta.ws.rs.core.Response`, returned by a *server* resource, out of an outbound-integration count. Measured: keycloak-config-cli **0 → 3** (`KeycloakProvider.java:66`, `UserProfileRepository.java:41`, `ResteasyUtil.java:42`), and seven golden repositories **byte-identical** — spring-petclinic 0, killbill 4, BroadleafCommerce 10, mall 7, open-banking-gateway 13, sagan 3, spring-boot-admin 21. Regression `tests/test_jaxrs_client_integration_e26.py`, 7 assertions, of which four are negative controls: the server side of the same specification, a same-named type from another package, a repository with no client at all, and a catalogue invariant that every JAX-RS entry carries a package guard |
|
|
391
|
+
| E-27 | **A call made through an *inherited* member is not attributed to the subclass that makes it — the single root of the largest scope error this ledger records.** Two halves, both measured in source at HEAD. **(a) Inherited field receiver:** `_receiver_typed_call_edges` (`repository_ir.py:5770`) does `fmap = field_types.get(owner_cls)` and skips the atom when the receiver is not a field of the owner class *itself*. `AdminBasicEntityController.java:1581/:1631/:1658` call `formService.populateEntityFormFieldValues(...)` on `protected FormBuilderService formService` declared at `AdminAbstractController.java:99` → no edge. **(b) Inherited method target:** `_intra_class_call_edges` (`repository_ir.py:1997`) does `sib = siblings.get(cls)` over the class's own declared methods, so `AdminBasicEntityController.java:1138/:1284` calling `getCriteria(requestParams)` — declared at `AdminAbstractController.java:502`, whose body calls `eps.cleanString(...)` — resolves to nothing. Neither pass climbs the supertype chain, although every supertype is in the model. Confirmed on three surfaces in one session: `impact FormBuilderService` lists 3 direct callers and not the class with 3 call sites; `explain AdminBasicEntityController` omits both dependencies from `outgoing_deps` while listing its 8 subtypes as `incoming_callers`; `impact ExploitProtectionService` misses the class at depth 4 **and** at depth 8 | 5.7.3 (field, BroadleafCommerce @`97705634`, D1) | **Critical** — this is the commercial blocker. The evaluator verified the inheritance chain of every controller outside the cone: of **236 endpoints excluded by default, 209 (88 %) belong to controllers that inherit the member invoking the migrated symbol**. The honest census is 247 IN / 24 UNKNOWN / 2 artefacts, not 38 IN / 235 OUT. It invalidates the *negative* blast radius — the differential promise — and it invalidates it silently, because the positive half of the same run was verified **15/15 exact** against the ground truth | **closed (local)** — both halves resolve against `_supertype_closure`, **one** authority for "what is above this type", derived from the `extends`/`implements` edges the graph already carries (breadth-first, so nearest first; cycle-safe). `_annotation_reference_edges` carried a private copy of the same walk and now shares it. (a) the field map is consulted along the closure, and the declared type is resolved against the class that **declares** the field — an inherited field's simple type name is imported where it is declared, not where it is used. (b) a new post-pass `_inherited_call_edges` resolves an unqualified `m()` / `this.m()` / `super.m()` against the closure, stopping at the **nearest** supertype that declares the name (Java's own rule — a redeclaration lower down shadows one higher up) and leaving a name the class declares itself to the intra-class pass. Both are joins over facts already extracted; no new parse. Acceptance, measured on the field's own repository (BroadleafCommerce @`97705634`, A/B against the parent commit): **`ask impact FormBuilderService .` 3 → 7 direct callers**, with `AdminBasicEntityController` and `AdminBasicOperationsController` among the new ones, `direct_caller_symbol_count` 5 → 21 and `endpoints_affected_count` 32 → 276; **`ask impact ExploitProtectionService .` 38 → 276 endpoints affected**, `security_surface_count` **34 → 247** — the figure this row set as the honest census — and 249 → 322 indirect callers. At method level, the two call sites this row cites go `AdminBasicEntityController#* → FormBuilderService#populateEntityFormFieldValues` **0 → 1** and `AdminBasicEntityController#* → AdminAbstractController#getCriteria` **0 → 2**. Fleet A/B of the CIR call graph: BroadleafCommerce 93 732 → 94 753 (**+1 021**, 607 of them inherited-method), killbill 75 901 → 83 111 (+7 210), keycloak-config-cli 11 310 → 12 210 (+900, of which 480 are the inherited-field half), open-banking-gateway +155, spring-boot-admin +36, spring-petclinic +4, sagan +3, and **mall +0** — the correct answer for a repository whose calls are not inheritance-mediated. ⚠ Method note, because it nearly cost this row its measurement: the editable install puts the **main checkout's** `src` on `sys.path` whatever the cwd, so a worktree A/B run through `run_cli.py` silently measures HEAD **twice**. The baseline must be run with `PYTHONPATH=<worktree>/src`, which precedes site-packages; the first pass of these figures was taken without it and reported no delta at all. Regression `tests/test_inherited_member_calls_e27.py` (11 assertions) asserts the two edges at method level on a fixture where the A/B is clean (base: neither edge; head: both), the transitive reach the inherited body opens, and the negative controls: a name no supertype declares yields no edge, and a same-named method on an unrelated type is never reached |
|
|
392
|
+
| E-28 | **`container_wired` classification compares the *immediate* `extends` clause against the framework vocabulary, so one repository-local level of indirection turns a security filter into `"Low-risk isolated change"`.** `container_wiring._detect` builds `own_supertypes = supertypes.get(fqn)` — the class's directly declared supertypes only — and matches their simple names against `_INTERCEPTION_SUPERTYPES` (`container_wiring.py:104-113`), with no transitive closure over supertypes defined in the repository. `X extends OncePerRequestFilter` is detected; `X extends Y extends OncePerRequestFilter` with `Y` in the tree is not, and `_WIRED_ROLES` is `{"config"}` so a `@Component` stereotype does not rescue it. Measured: `AdminSecurityFilter` (`:63 extends SecurityFilter`, `@Component("blAdminCsrfFilter")` — the filter Broadleaf's own javadoc documents as the admin chain's real one, wired at `AdminSecurityFilter:52` by `<sec:custom-filter ref="blSecurityFilter" before="FORM_LOGIN_FILTER"/>`) returns `risk_level: "low"`, `risk_score: 0.0`, `confidence: "high"`, `blind_spots: []`, *"No callers or endpoints found in the impact chain. Low-risk isolated change."* Its base class `SecurityFilter`, one level up, correctly returns `high` / `null` / `[container_wired]`. The same missing closure explains the omission of `AdminSecurityFilter -> SecurityFilter` and `AdminCsrfFilter -> CsrfFilter` from `spring-audit`'s 20-relation authentication pipeline | 5.7.3 (field, BroadleafCommerce, D2) | **Critical** — C1-36 and E-24 exist to stop exactly this sentence, and it is still reachable by one `extends` hop. A customer excluding that filter from a Spring Security migration retest is exposed, and the failure arrives with `confidence: high` | **closed (local)** — both readers of the clause now read the **chain**, through `supertype_closure` (the authority E-27 established, moved to `cir_graphs` so `container_wiring` can share it without importing the IR). The closure is built over *every* `extends`/`implements` edge, not only the queried classes', because a supertype of a supertype is usually not in `wanted_set` — which is the second reason the old code could not have worked. The immediate supertypes are kept beside it so the **evidence stays exact**: a class that declares the extension point cites `implements X`, a class that inherits it cites `inherits X via Y`, and a reader checking the citation is never sent to a clause the class never wrote. Acceptance, measured on BroadleafCommerce A/B against the parent commit (baseline run with `PYTHONPATH` pinned to the worktree — see the note on E-27): `ask impact AdminSecurityFilter .` goes from `low` / `1.0` / `confidence: high` / *"No callers or dependents found … Low-risk isolated change"* to **`high` / `risk_score: null` / `container_wired: [interception_point]`** citing `inherits org.springframework.web.filter.OncePerRequestFilter via org.broadleafcommerce.common.security.handler.SecurityFilter`; `impact-chain` publishes the same floor. `ask spring-audit .` carries **20 → 42** authentication-pipeline relations, including `AdminSecurityFilter` and `AdminCsrfFilter`, each with the hop published as `inherited_via`. Collateral, measured as a full container-wiring census over 8 repositories (every class, not only a queried one): BroadleafCommerce 576 → 802 classified (interception 115 → 172, reached-through-injected-interface 411 → 587), killbill 178 → 222, open-banking-gateway 171 → 192, spring-boot-admin 104 → 108, sagan 17 → 20, and **spring-petclinic, keycloak-config-cli and mall unchanged** — the correct answer where no local type sits between a class and the SPI. Each addition is the same structural fact one hop further out, and the direction is the safe one: this signal only ever **raises** a floor. Regression `tests/test_container_wired_supertype_chain_e28.py` (8 assertions) covers both consumers, the exact-evidence distinction in both directions, and a plain class that must not be swept in |
|
|
393
|
+
| E-29 | **Three shapes of route false positive in the population every HTTP ratio is keyed on.** All three confirmed against source at HEAD. **(a)** `_parse_route_paths` (`repository_ir.py:4026-4030`) falls back to `re.findall(r'"([^"]*)"', args_str)` after `value=`/`path=` miss, so *any* string literal in the annotation becomes the path: `AdminBasicErrorController:44` `@RequestMapping(produces="text/html")` → path `text/html`, and `BroadleafOauthRegisterController:97` `@RequestMapping(params="action=register")` → path `action=register`. **(b)** `_parse_route_http_methods` (`:4062-4067`) matches `method\s*=\s*\{?\s*((?:RequestMethod\.)?\w+(?:\s*,\s*…)*)` — the alternation keeps consuming past the closing token, so `method=RequestMethod.GET, produces=…` yields `["GET", "PRODUCES"]`, observed verbatim on `AdminCatalogActionsController`. **(c)** `_DSL_ROUTE_RE` (`:6932`) already rejects a **literal** second argument (the B1 fix, openmrs `swapChars.put("/", "slash")`), and the guard is exactly one shape too narrow: `BLVersionResourceResolverDefaultStrategyMap.java:37` is `this.put("/**", new …Strategy(…))` inside `@PostConstruct` **on a subclass of `HashMap`**, whose second argument is an expression — so it is published as `PUT /**`, `source: router_dsl` | 5.7.3 (field, BroadleafCommerce, D3) | Medium — 3 of 273 is a small error rate and a contaminated denominator: every ratio built on `endpoints.total` inherits it, and the customer-facing figure in a report is the one that gets checked | **closed (local `dab7fe5` for (a) and (b), `eb98158` for (c)).** A named `value=`/`path=` still wins outright; what changed is what the *positional* fallback sees — the argument list with every published non-path argument of `@RequestMapping` removed (`_strip_non_path_args`, one authority, used to **reject** candidates and never to admit one by name) — and the `method=` value, now bounded to the braces or to a single token and admitted only if it names a member of `RequestMethod`, whose enum is closed. A `method=` naming an unknown constant yields no verb and `@RequestMapping` falls back to `ANY`, which is the honest answer. The mirror defect was closed in the same pass: an annotation that never named a path no longer reports a `path_expression` it *could not compute*. Measured A/B on a fixture carrying the three field shapes: `/base/text/html` (×2, a path that exists nowhere) and `/base/action=register` → `/base`, and `["GET","PRODUCES"]` → `["GET"]`; the population stays at 4 of 4, because this row is about false positives and dropping a declaration would be the more expensive defect. **(c)** was closed second, and not with the discriminator this row first proposed. Widening the B1 literal guard was rejected by measurement: `put("/items/:id", new UpdateItemRoute(service))` is a real handler registration, so refusing a constructed second argument would trade this false positive for a false negative. The discriminator is the **receiver**: a `put`/`get` inside a type that *is* a collection is that collection's own API, because a router never registers a handler onto a map. Detection matches the **declared supertype** against JDK collection types and published Spring map contracts (`_COLLECTION_SUPERTYPES`), and generic arguments are stripped *first*, which settles two cases at once — a registry merely parameterised by a map (`extends AbstractRegistry<Map<String, Handler>>`) is not one, and a bounded type parameter (`class E<T extends Map<String, X>>`) does not offer its own `extends` to the clause reader. ⚠ The proposed source, `extends_map`, turned out **not** to work, and the first cut passed its own fixture while doing nothing on the real repository: a signature loses its whole supertype clause as soon as the type declares parameters of its own, which is exactly the field shape (`class BLVersionResourceResolverDefaultStrategyMap<T, V> extends HashMap<…>`). The clause is therefore read from the declaration text. **That gap is wider than this row and is filed as E-34.** Measured A/B over **8 golden repositories**: BroadleafCommerce 273 → 272 endpoints with the phantom `PUT /**` gone, and spring-petclinic, spring-boot-admin, open-banking-gateway, killbill, sagan, mall and spring-security-samples **byte-identical** — zero collateral. Regression `tests/test_route_argument_population_e29.py`, 45 assertions, of which 19 are negative controls: every path and verb spelling still resolves, and a genuine imperative router keeps all three of its registrations (lambda, method reference, constructed handler) |
|
|
394
|
+
| E-30 | **`impact-chain` deduplicates affected endpoints by `handler_symbol`, so two distinct HTTP routes sharing one inherited handler method collapse into one.** `GET /com.broadleafcommerce…InventoryImpl/{owningClass}/{collectionField}/typeahead` is present in `impact` and `plan` and absent from `impact-chain` — **37 against 38** for the same symbol in the same session. The route is mounted by `AdminInventoryBasicOperationsController:49` `@RequestMapping(...)` on an inherited method, which is the shape E-3's projection creates and this key cannot distinguish | 5.7.3 (field, BroadleafCommerce, D4) | Low/Medium — small in count, and it is a second cross-command discrepancy on the same symbol beside C1-16's, in the pair a customer compares first | **closed (local)** — the de-duplication was **not** the defect: it was already keyed on `endpoint_id`, which spells `(method, path, controller, handler)`. The loss was one step earlier, in how candidate controllers were collected: `impact-chain` walked from each caller to *its own* class, and for a handler declared by an abstract base the route's controller is a **subclass**, so neither controller was ever considered and every route they mount was dropped. The way in is the handler symbol, now indexed once by `EndpointIndex.controllers_by_handler` rather than re-derived by each consumer. Reproduction is sharper than the field's: on a fixture where one inherited handler is mounted by two controllers, `impact` reported both routes and `impact-chain` reported **none** — 2 vs 0, not 38 vs 37. Field A/B on BroadleafCommerce: `impact-chain ExploitProtectionService` **189 → 239** affected endpoints, `impact-chain FormBuilderService` **186 → 236**. ⚠ The cross-command gap narrows but is not closed — `impact` publishes 276 for the same symbol at the same depth, and that remaining divergence belongs to C1-16, which is open and about units. Regression `tests/test_inherited_handler_routes_e30.py`, 6 assertions, including that the handler is still attributed to the type that declares it (the route is projected, the method is not moved) |
|
|
395
|
+
| E-31 | **A bean reference made from a template is invisible, and — unlike every other thing this product cannot see — it is not declared.** `entityEdit.html:27` invokes `${@blExploitProtectionService.getCsrfTokenParameter()}` and `.getCSRFToken()` through SpEL. The bean is absent from the 78 components `plan` reports, and the symbol's `blind_spots` is `[]`. `NC-007` declares the servlet population and `NC-010` the container-wired one; **no `NC-*` covers templates**, so here a "not found" is presented without the "not looked at" label that is half of what this product sells | 5.7.3 (field, BroadleafCommerce, D5) | Medium — the miss is ordinary; publishing it as an empty result with no non-coverage is the defect | **closed (local)** — declared, which is what the row asked for and what the defect actually was: the miss is ordinary, the unlabelled silence is not. **NC-012** (`blast_radius` surface, `non-coverage-v1` shape) states that a bean reached from a template — a SpEL `@beanName` inside `${…}` — is outside what a call-graph projection looks at, why (a template is not compiled against the bean and lives in a file this analysis does not parse), and what answers it today (`grep -rn '@<beanName>' src/main/resources`, because the syntax is fixed and published). The second half of the row was that the block was not published where the field met the defect: `pr-impact` emitted it, `impact`, `impact-chain` and `plan` did not. All three do now, from the one non-coverage authority, so the eleven rows that were already true of them stopped being invisible too. The README table is generated-checked against the registry and carries the row. Modelling it as a fourth reacher stays **open and unclaimed** — it would be evidence-shaped for the same reason the declaration is, and this row does not pretend it happened. Regression `tests/test_template_bean_non_coverage_e31.py`, 7 assertions, including the shape the field met: a service with no Java caller and a template that names it, where the answer is empty and now says what it did not look at |
|
|
396
|
+
| E-32 | **`bfs_truncated` answers a different question from the one the reader asks, and the two answers disagree exactly where it matters.** `repository_ir.py:8392`: `_bfs_truncated = _effective_depth < max_depth` — it reports whether the **hub guard cut the depth below the requested one**, not whether the closure was exhausted. On `ExploitProtectionService` at the default depth 4 the frontier is still `{2: 89, 3: 84, 4: 76}` wide and the flag is `false`; at depth 8 the same run continues `{5: 18, 6: 5, 7: 2, 8: 2}` and finishes at **276 indirect callers and 39 endpoints** against 249 and 38. A reader takes `bfs_truncated: false` beside a published depth as *the cone is closed* | 5.7.3 (field, BroadleafCommerce, D6) | Medium — it converts a floor into a total on the axis a scope decision is taken from, and it does it in the same payload that gets the answer right | **closed (local)** — the walk now records whether a node dropped at the depth limit still had callers, which is the fact the name promises, and it costs one extra `_walk_callers` per boundary node with a short-circuit at the first live one. Four fields where one stood: `bfs_truncated` (**the closure was not exhausted** — the union, so the flag a reader already consults is the honest one), `closure_complete`, `depth_capped_by_guard` and `frontier_open_at_depth_limit`. The two causes are named apart because their remedies differ — a hub cap is lifted by choosing a lower-fan-in entry point, a live frontier by raising `--depth` — and every block that was written about the hub cap (the truncation reason, its effect note, the sampling prose, `indirect_callers_computed`) stays keyed on the guard, so nothing acquired a wrong reason. The prose says it too, because prose is what a reader quotes: *"the walk stopped at depth=4 with callers still unexplored, so every transitive figure here is a floor"*. Measured on the row's own subject: `impact ExploitProtectionService` at the default depth now publishes `bfs_truncated: true` / `closure_complete: false` / `frontier_open_at_depth_limit: true` beside `depth_reached: 4`, and at `--depth 8` the frontier is **still** open (322 → 349 indirect callers), which is the honest answer and not the one the old flag gave. Regression `tests/test_bfs_closure_completeness_e32.py`, 7 assertions over a chain deeper than the depth queried, with the exhausted case asserted in both directions |
|
|
397
|
+
| E-33 | **`verify-edit`'s contract diff attributes a removed method to the wrong type and misses a `throws` clause removal that breaks callers.** On a real working-tree change it reported `SpringBootBootstrappingServletContextListener#run` as a removed public method — `run()` belonged to an **anonymous inner class** — and did not report that `protected createInitializer(...)` lost `throws PrivilegedActionException`, which breaks every caller that catches it. The verdict itself was right (`break`, `contract_broken: 1 public API change`, correct `head_sha`, `security_delta: false`) | 5.7.3 (field, BroadleafCommerce, D8) | Low — the gate answered correctly; the evidence beside the answer is what is wrong, and this gate's product is its evidence | **closed (local)** — both halves are about what an owner and a signature *are*. **Owner:** the extractor now opens a scope on an anonymous class body (`new Runnable() {`) and names it the way Java does, `Outer$1`, so the members inside are attributed to it and the type itself is a symbol — a member's owner always exists in the model. Both public-surface readers (`verify_edit._nodes_by_fqn`, `contract_diff.extract_public_contract`) skip owners containing `$`: nothing outside can name an anonymous type, so nothing can depend on it, and the discriminator is the owner rather than a naming rule. A creation whose argument list spans lines is not matched — the scope stays with the enclosing type, as before, rather than being guessed. **Signature:** the `throws` clause is captured and published as `(String)->Object throws PrivilegedActionException`, which is DR-1 applied to the other half of the declaration. End-to-end on the field's own shape: `verify-edit` went from *2 public API changes* (one of them the phantom `#run`) to **1**, and that one is the `throws` removal it used to miss entirely, cited from and to. Fleet A/B: symbol population BroadleafCommerce 30 427 → 30 694, killbill 18 513 → 18 887, spring-petclinic 267 → 269, mall 15 752 → 15 757 — the growth is the anonymous types themselves; their members are **re-attributed, not added** — and the endpoint population is byte-identical on all five (268 / 285 / 17 / 240 / 0). Regression `tests/test_contract_evidence_e33.py`, 9 assertions covering both halves and the negative controls (the named type keeps its own members; a method that throws nothing says nothing) |
|
|
398
|
+
| E-34 | **A type that declares parameters of its own has no supertype anywhere in the model — not an edge, not a signature.** Two independent readers of the `extends`/`implements` clause, both written as if a declaration went straight from the type's name to its supertype. **(a)** The `extends`/`implements` **edge** builder (`repository_ir.py:3229-3232`) matches `(?:class\|interface)\s+(\w+)(?:\s+extends\s+…)?(?:\s+implements\s+…)?\s*\{`: after the name it requires whitespace, so `class Generic<T, V> extends Base {` matches **nothing at all** — not a partial match, no edge of either kind. **(b)** `SymbolRecord.signature` drops the clause the same way, so `class B<T, V> extends HashMap<String, V>` is recorded as `class B`, and `extends_map` — derived from that signature at `repository_ir.py:7043` and handed to `_build_route_surface` — inherits the hole. Minimal end-to-end witness: three classes, `Plain extends Base` and `Generic<T, V> extends Base`; `ask explain Base` publishes **`Used By: Plain`** and nothing else. Census over 8 repositories, counting only types whose declaration really does carry a clause: **254 of 8 182 (3,1 %)** lose it — open-banking-gateway 57 of 595 (**9,6 %**), keycloak 144 of 4 356, killbill 25 of 838, BroadleafCommerce 26 of 1 965 | 5.7.3 (found while closing E-29(c), 2026-08-16) | **High** — it is silent, it is repo-wide, and it removes the one relation the two commercial blockers are about to be fixed by. A generic abstract controller loses its inheritance projection (the E-3 family), a generic filter loses the supertype `container_wiring` classifies on (E-28), and any supertype **closure** built for E-27 will walk a chain with holes in it and report the result as complete | **closed (local)** — one root, one fix: the type's own parameter block is now matched as a **balanced** `<…>` span instead of "everything up to the next `>`". The unbalanced form stopped inside `<T, V>` and, to reach the body brace, had to swallow the whole `extends …` clause — which is why the same defect appeared twice from one cause, in `_CLASS_DECL_RE` (the signature) and in the edge builder (`repository_ir.py:3229`, where it matched nothing at all). Balancing is also what settles the bounded-parameter trap without a second rule: the `extends` inside `class E<T extends Map<String, X>>` belongs to the parameter, and the reader now walks past it to the type's own clause. Nesting is bounded at four levels, past which the old unbalanced branch is kept as a **fallback**: it loses the clause as it always did, and the type still matches — a declaration this product cannot fully read must not vanish from the model. ⚠ The signature half was weighed as the row asked and carries no churn: `_compute_stable_id` is built from `(package, class, kind, name)` and never from `signature`, and `contract_diff.extract_public_contract` reads signatures of **method** nodes only, so restoring a type's clause changes no identity and no contract diff. Two consumers that read the clause off `signature` (interface closure `:3688`, `Api`-interface controllers `:6737`) gain the generic types they were silently skipping. Measured A/B over **25 repositories** (61 k `.java`): **0 type declarations lost, 0 gained** — the population is byte-identical — and clause-carrying declarations recovered wherever the shape occurs: dubbo +207, alfresco +130, jenkins +104, keycloak +74, langchain4j +67, BroadleafCommerce +26, killbill +27; keycloak-config-cli and mall +0, which is the correct answer for repositories without the shape. Regression `tests/test_generic_supertype_clause_e34.py`, 15 assertions: the end-to-end witness (`explain Base` now names `Plain`, `Generic` **and** `Bounded`), the `extends` edge in the CIR, the signature, and the negative controls — record components, enums, an unparameterised type, and the past-depth declaration that must still match with no clause. The E-29(c) test that documented this hole from the other side was updated to assert both readers agree |
|
|
399
|
+
| E-35 | **A statement is parsed as a method declaration, and the method it invents is published with a call edge pointing at it.** `return unknownHelper("x");` matches the declaration shape read by `_METHOD_DECL_RE` — no modifiers, "return type" `return`, name `unknownHelper`, parameter list `"x"` — so the symbol table gained `com.a.BasicCtl#unknownHelper` with `signature: ("x")->return`, an `in_degree` of 1 and a `calls` edge from the real method that contains the statement. Census over 5 repositories: **BroadleafCommerce 1 047 of 23 913 methods (4,4 %)**, killbill 846, keycloak-config-cli 76, mall 11, spring-petclinic 6 | 5.7.3 (found while writing the E-27 negative controls, 2026-08-16) | Medium — it inflates every method population and every degree derived from it, and a phantom callee is the shape a reader cannot check: the symbol does not exist to be looked up | **closed (local)** — the discriminator is the return-type slot, because nothing declares a method whose type is `return`. `_STATEMENT_HEADS` lists the keywords that start a statement and can stand where a type is read (`return`, `throw`, `new`, `else`, `assert`, `yield`, `case`, `do`, `instanceof`, `break`, `continue`, `catch`, `finally`); **`void` is deliberately absent — it is a return type**. Measured A/B: phantoms **1 047 → 0** on Broadleaf, 846 → 0 on killbill, 76 → 0 on keycloak-config-cli, and the method population falls by **exactly** that count on each repository, so no real declaration was lost; the endpoint population is byte-identical on all five. Regression `tests/test_statement_is_not_a_declaration_e35.py`, 10 assertions, of which 7 are negative controls — every real declaration in the same file, `void` kept as a type, and a method that returns a constructed object |
|
|
354
400
|
|
|
355
401
|
## Class P — Product and procurement (not analysis defects)
|
|
356
402
|
|
|
@@ -404,6 +450,7 @@ provenance: E-3 was found from outside, on the surface where silence is loudest.
|
|
|
404
450
|
| CL-28 | *(eval #26 — what carries over rather than opening a row, the CL-20/CL-22/CL-26/CL-27 precedent)* | **B18 is C3-106, closed in 5.1.0 and certified from the outside** — *"corregido casi literalmente como lo propuse"*, with `reason`/`detail` the report did not ask for. **B16 stays F-AV** (4.17.0): the round did not truncate, so `total_defects` was correctly unrenamed; latent for the fourth round. **B8 stays informational** for the third round, by the field's own measurement. **B19/B21/B22 stay C3-102/C3-103/C3-107** — re-reported with a round of ageing, not new facts — and **B20 splits**: the stdout half stays closed (CL-26) and the `-o` half is new and is C3-111. **B17's remainder is C3-109 plus the process half, which is C3-110** — one task, as the report says | Recorded, not re-derived |
|
|
405
451
|
| CL-29 | *(eval #27 — what carries over rather than opening a row)* | **Three closures certified from the outside, one of them with the implementation the report says it did not ask for**: B17 is C3-109 (*"ya no miente; admite"* — the anchor names its build and that this one was not re-measured), B20 is C3-111 (`OUTPUT_TOO_LARGE` before the build, with a band whose basis states that its own top end under-states), B22 is C3-107 (*"base_cir_hash: null, totales por lado null nunca 0 — la optimización correcta, no la perezosa"*). **B18 stays closed** (C3-106, second consecutive certification). **B16 stays F-AV** (4.17.0), latent for the fifth round: 5.2.0 finishes `spring-audit` in 17,0 s, so nothing truncated and `total_defects` was correctly unrenamed — the rename is conditional on `partial` by design, and a round that never truncates cannot observe it. **B8 stays informational** for the fourth round, by the field's own measurement and their own conclusion that the 280× came from the serial path rather than from workers. **B21 stays C3-103** — re-measured, not re-derived. **B19 does *not* carry over**: the fix it names shipped and reproduces here, and what the field measures is a different hole, **C3-112**. **B10's mechanism shipped as C3-110 and its evidence did not, which is C3-113** | Recorded, not re-derived |
|
|
406
452
|
| CL-20 | *(non-reproducers from eval #20, recorded so they are not raised a third time — the CL-17/CL-18 precedent)* | **`export .` "requires a mode flag" is the documented behaviour and the error already carries the cure**: verified at HEAD, the rejection publishes `hint: "Pass --c4 for the full architecture export, or one of --by-directory / --module-graph / --integrations for a section."` and `expected: "--c4 \| --by-directory \| --module-graph \| --integrations"`, and `ask export --help` lists all four with a paragraph each. The report's *"el flag no está en la ayuda del comando raíz"* is about `ask --help`, which lists commands rather than each command's flags, by design. · **`trend` and `baseline trend` failing identically is correct**: they are **one command published under two names** (`cli.py` — `"baseline trend" # same command under the group name`), so two different messages would be the defect | **no action** — both verified at 4.16.0. What *is* actionable from the same section of the report is C4-22 (`schema`), which is a real gap and is filed |
|
|
453
|
+
| CL-30 | **Four sentences the product's own positioning invites, and that E-27/E-28 make false today.** From the Spring-migration validation on BroadleafCommerce: *"ASK demuestra qué **no** hay que revisar"*; *"N componentes quedan estructuralmente fuera del alcance"*; *"cierre transitivo completo"*; and — the dangerous one, because it is where a customer would believe it — *"un `risk: low` con 0 callers significa cambio aislado"* | **88 % of the default exclusions were falsified by hand on one repository** (209 of 236 endpoints), and `AdminSecurityFilter` publishes `low` / `0.0` / `confidence: high` over the admin filter chain. What survives the same scrutiny, unchanged: the file surface was **15/15 exact**; every `symbol → component → file → line → commit` chain held; the unit of every figure is published; `MIG-010`/`MIG-031` are true positives with a measurable delta (171 → 170); *"78 components, 1 test"* is a first-order QA conclusion; the evidence is reproducible against a commit; and a **build-boundary** exclusion does hold — the `integration` module is a leaf of the Maven graph, verified over all 10 poms | **reverted (local)** — E-27 and E-28 are closed and the reversal condition this row set was measured, not argued: `ask impact ExploitProtectionService .` on BroadleafCommerce goes from 38 to **276** endpoints affected and from 34 to **247** security declarations — the honest census the evaluator computed by hand — and `ask impact AdminSecurityFilter .` no longer publishes `low` with `confidence: high` over the admin filter chain but `high` with `risk_score: null` and the container-wiring evidence beside it. Two things stay true and are part of the reversal rather than exceptions to it: an exclusion claim is only as good as the closure behind it, so the **field revalidation on the evaluator's repository is still owed** before the differential promise goes into commercial material — what is measured here is the acceptance test this ledger defined, not a second audit; and the positive half needs no rehabilitation, having been 15/15 exact throughout. Original note: until E-27 and E-28 close, the sellable answer is *positive map + uncertainty inventory + retest gap*, with no exclusion claim beyond a verifiable build boundary. The evaluator's own one-minute framing is the defensible one: *"la migración no toca transacciones, toca 5 subsistemas y 78 componentes, y sólo hay 1 test cubriéndolos."* Reversing this row is the acceptance test of E-27/E-28, not a copy decision |
|
|
407
454
|
|
|
408
455
|
---
|
|
409
456
|
|
sourcecode/_docs/USER_GUIDE.md
CHANGED
sourcecode/archetype.py
CHANGED
|
@@ -434,9 +434,16 @@ class ArchetypeClassifier:
|
|
|
434
434
|
http_strength = (
|
|
435
435
|
max(0.0, min(1.0, (density - 0.5) / 1.5)) if f.endpoint_total else 0.0
|
|
436
436
|
)
|
|
437
|
+
# The clause is the CONCLUSION this signal supports, so it is only
|
|
438
|
+
# printed when the measurement supports it. At strength 0 the same
|
|
439
|
+
# sentence read "0 endpoints … the repository's primary surface is
|
|
440
|
+
# HTTP" — a claim contradicted by the number in front of it (F-1).
|
|
437
441
|
add("application", "http_surface_density",
|
|
438
442
|
f"{f.endpoint_total} endpoints over {f.total_files} files "
|
|
439
|
-
f"({density:.2f}/100 files)
|
|
443
|
+
f"({density:.2f}/100 files)"
|
|
444
|
+
+ (" — the repository's primary surface is HTTP"
|
|
445
|
+
if http_strength > 0 else
|
|
446
|
+
" — too thin a surface to make HTTP this repository's product"),
|
|
440
447
|
2.5, http_strength, 1.0)
|
|
441
448
|
# And the same fact as NAMED negative evidence for `engine`, the way
|
|
442
449
|
# the library de-bias below does it: a codebase whose product is a
|
sourcecode/cache_model.py
CHANGED
|
@@ -220,8 +220,10 @@ LAYERS: tuple[Layer, ...] = (
|
|
|
220
220
|
invalidated_by=(
|
|
221
221
|
"the file's own bytes and the extractor's source — content-addressed, "
|
|
222
222
|
"so it is never stale and never needs invalidating. Bounded by "
|
|
223
|
-
"`ASK_PARSE_CACHE_MAX_MB` (default 512 MB, least-recently-used, "
|
|
224
|
-
"
|
|
223
|
+
"`ASK_PARSE_CACHE_MAX_MB` (default 512 MB, least-recently-used, swept "
|
|
224
|
+
"every 32 MB written rather than on every write, so the size can sit "
|
|
225
|
+
"one sweep interval per running process above the budget) and reported "
|
|
226
|
+
"in `cache status` (F-AR)"
|
|
225
227
|
),
|
|
226
228
|
warmed="filled for every Java file the warm parses",
|
|
227
229
|
),
|
sourcecode/canonical_ir.py
CHANGED
|
@@ -23,6 +23,7 @@ from typing import Any, Callable, Optional
|
|
|
23
23
|
|
|
24
24
|
from sourcecode.cir_graphs import ImplementationGraph, InjectionGraph
|
|
25
25
|
from sourcecode.repository_ir import (
|
|
26
|
+
NON_SERVED_ROUTE_SCOPES,
|
|
26
27
|
build_repo_ir,
|
|
27
28
|
)
|
|
28
29
|
from sourcecode.repository_ir import (
|
|
@@ -450,10 +451,14 @@ def ir_dict_to_canonical(
|
|
|
450
451
|
# surface feeds impact-chain, pr-impact, review-pr and spring-audit, and test
|
|
451
452
|
# scaffolding is not production HTTP exposure. route_surface itself keeps them
|
|
452
453
|
# (raw evidence); only this canonical projection drops them.
|
|
454
|
+
# F-1 added a second such scope: a mapping declared on an interface nothing in
|
|
455
|
+
# the tree implements is not served here either. Both live in
|
|
456
|
+
# NON_SERVED_ROUTE_SCOPES so this projection and the `endpoints` command
|
|
457
|
+
# cannot drift apart on what counts as an endpoint of this repository.
|
|
453
458
|
_seen_ids: set[str] = set()
|
|
454
459
|
raw_endpoints: list[CanonicalEndpoint] = []
|
|
455
460
|
for r in route_surface:
|
|
456
|
-
if r.get("scope")
|
|
461
|
+
if r.get("scope") in NON_SERVED_ROUTE_SCOPES:
|
|
457
462
|
continue
|
|
458
463
|
if _route_has_reserved_handler(r):
|
|
459
464
|
continue
|
sourcecode/change_plan.py
CHANGED
|
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
from typing import TYPE_CHECKING
|
|
28
28
|
|
|
29
|
+
from sourcecode import non_coverage as _non_coverage
|
|
29
30
|
from sourcecode.canonical_ir import project_blast_radius
|
|
30
31
|
from sourcecode.repository_ir import _all_callers_from_rg
|
|
31
32
|
from sourcecode.security_posture import _graph_nodes
|
|
@@ -236,4 +237,10 @@ def build_change_plan(
|
|
|
236
237
|
"graph + endpoint surface + test-path membership. Lists what to review; "
|
|
237
238
|
"emits no code, no sequence beyond dependency depth, no recommendation."
|
|
238
239
|
),
|
|
240
|
+
# E-31: `plan` projects a blast radius, so it publishes the same bounds
|
|
241
|
+
# the other projections do. The field's case was a component invoked
|
|
242
|
+
# from two templates, absent from the 78 this command listed and absent
|
|
243
|
+
# from any blind-spot label — the miss is ordinary, the unlabelled
|
|
244
|
+
# silence is not.
|
|
245
|
+
"non_coverage": _non_coverage.block("blast_radius"),
|
|
239
246
|
}
|
sourcecode/cir_graphs.py
CHANGED
|
@@ -269,3 +269,70 @@ class InjectionGraph:
|
|
|
269
269
|
_dependents_of=dependents_of,
|
|
270
270
|
_injector_to_class=injector_to_class,
|
|
271
271
|
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# Supertype closure — E-27 / E-28
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def _edge_ends(edge) -> "tuple[str, str, str]":
|
|
279
|
+
"""(from, to, type) for either edge shape: the IR's `RelationEdge` objects or
|
|
280
|
+
the CIR's plain dicts. Both carry the same fact under two spellings."""
|
|
281
|
+
if isinstance(edge, dict):
|
|
282
|
+
return (
|
|
283
|
+
str(edge.get("from") or edge.get("from_symbol") or ""),
|
|
284
|
+
str(edge.get("to") or edge.get("to_symbol") or ""),
|
|
285
|
+
str(edge.get("type") or ""),
|
|
286
|
+
)
|
|
287
|
+
return (
|
|
288
|
+
str(getattr(edge, "from_symbol", "") or ""),
|
|
289
|
+
str(getattr(edge, "to_symbol", "") or ""),
|
|
290
|
+
str(getattr(edge, "type", "") or ""),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def supertype_closure(edges) -> "dict[str, list[str]]":
|
|
295
|
+
"""type FQN → its **transitive** supertypes, nearest first.
|
|
296
|
+
|
|
297
|
+
One authority for "what is above this type", derived from the `extends` /
|
|
298
|
+
`implements` edges the graph already carries — no new parse, and nothing
|
|
299
|
+
read from a name. Breadth-first, so a member declared two levels up is only
|
|
300
|
+
reached after every declaration one level up has been considered: that is
|
|
301
|
+
Java's own resolution order, and it is what lets a consumer stop at the
|
|
302
|
+
first supertype that declares the member it is looking for.
|
|
303
|
+
|
|
304
|
+
E-27/E-28: three passes needed this fact and each carried its own partial
|
|
305
|
+
version, or none at all — a call through an inherited field, a call to an
|
|
306
|
+
inherited method, and the `container_wired` classification, which compared
|
|
307
|
+
only the *immediate* `extends` clause and so read `X extends Y extends
|
|
308
|
+
OncePerRequestFilter` as an isolated class with `confidence: high`.
|
|
309
|
+
|
|
310
|
+
Cycle-safe: an inheritance cycle is not legal Java, but a half-resolved
|
|
311
|
+
graph can present one, and a walk that hangs is worse than a walk that
|
|
312
|
+
stops.
|
|
313
|
+
"""
|
|
314
|
+
direct: dict[str, list[str]] = {}
|
|
315
|
+
for edge in edges or ():
|
|
316
|
+
src, dst, etype = _edge_ends(edge)
|
|
317
|
+
if etype not in ("extends", "implements") or not src or not dst:
|
|
318
|
+
continue
|
|
319
|
+
bucket = direct.setdefault(src, [])
|
|
320
|
+
if dst not in bucket:
|
|
321
|
+
bucket.append(dst)
|
|
322
|
+
|
|
323
|
+
closure: dict[str, list[str]] = {}
|
|
324
|
+
for fqn, parents in direct.items():
|
|
325
|
+
order: list[str] = []
|
|
326
|
+
seen: set[str] = {fqn}
|
|
327
|
+
frontier = list(parents)
|
|
328
|
+
while frontier:
|
|
329
|
+
nxt: list[str] = []
|
|
330
|
+
for sup in frontier:
|
|
331
|
+
if sup in seen:
|
|
332
|
+
continue
|
|
333
|
+
seen.add(sup)
|
|
334
|
+
order.append(sup)
|
|
335
|
+
nxt.extend(direct.get(sup, ()))
|
|
336
|
+
frontier = nxt
|
|
337
|
+
closure[fqn] = order
|
|
338
|
+
return closure
|