sourcecode 5.5.6__py3-none-any.whl → 5.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sourcecode/__init__.py CHANGED
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "5.5.6"
7
+ __version__ = "5.7.2"
@@ -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
 
@@ -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
@@ -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
 
@@ -350,6 +361,17 @@ provenance: E-3 was found from outside, on the surface where silence is loudest.
350
361
  | 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
362
  | 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
363
  | 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 |
364
+ | 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` |
365
+ | 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 |
366
+ | 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 |
367
+ | 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 |
368
+ | 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 |
369
+ | 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") | open |
370
+ | 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 | open |
371
+ | 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 |
372
+ | 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 |
373
+ | 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 |
374
+ | 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 |
353
375
 
354
376
 
355
377
  ## Class P — Product and procurement (not analysis defects)
@@ -0,0 +1,47 @@
1
+ ASK Engine Proprietary License
2
+ Version 1.0, effective 2026-08-13
3
+
4
+ Copyright (c) 2026 Dominique Haroun. All rights reserved.
5
+
6
+ 1. Scope
7
+
8
+ This license applies to ASK Engine version 5.6.1 and later, including its source
9
+ code, documentation, and distributed artifacts (the "Software"). It does not
10
+ alter the license of versions 5.5.6 and earlier, which were distributed under
11
+ the Apache License, Version 2.0. Grants made for those earlier versions remain
12
+ governed by their published license terms.
13
+
14
+ 2. No Implied Grant
15
+
16
+ Except under a separate written agreement signed by Dominique Haroun, no right
17
+ or license is granted to use, copy, modify, distribute, sublicense, sell,
18
+ reverse engineer, or create derivative works of the Software. Possession of a
19
+ copy, access to a repository, or access to a distributed artifact does not
20
+ create an implied license.
21
+
22
+ 3. Third-Party Materials
23
+
24
+ Third-party dependencies and materials included with the Software remain subject
25
+ to their own license terms. This license does not supersede those terms.
26
+
27
+ 4. Trademarks
28
+
29
+ "ASK Engine", "ASK", and associated marks are identifiers of the Software.
30
+ No trademark license is granted except as required to identify an unmodified,
31
+ lawfully obtained copy of the Software.
32
+
33
+ 5. No Warranty
34
+
35
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
36
+ IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE,
37
+ AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE COPYRIGHT
38
+ HOLDER SHALL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING
39
+ FROM OR IN CONNECTION WITH THE SOFTWARE.
40
+
41
+ 6. Contact
42
+
43
+ Commercial, evaluation, redistribution, and contribution terms require a
44
+ written agreement with Dominique Haroun.
45
+
46
+ This license text is an operational policy draft and must be reviewed by
47
+ qualified counsel before distribution to third parties.
@@ -0,0 +1,8 @@
1
+ ASK Engine
2
+ Copyright (c) 2026 Dominique Haroun. All rights reserved.
3
+
4
+ ASK Engine and its source code were originally created and developed by
5
+ Dominique Haroun.
6
+
7
+ This NOTICE applies to ASK Engine 5.6.1 and later. Earlier releases may carry
8
+ different license terms; see the license distributed with the applicable release.
@@ -136,7 +136,7 @@ pipx install sourcecode # isolated install, no venv needed
136
136
 
137
137
  # Verify
138
138
  ask version
139
- # ask 5.5.6
139
+ # ask 5.7.2
140
140
  ```
141
141
 
142
142
  Requires Python 3.9+.
@@ -157,7 +157,10 @@ On success:
157
157
  {"status": "activated", "plan": "pro", "features": ["impact", "review-pr", "fix-bug", "modernize", "generate-tests"]}
158
158
  ```
159
159
 
160
- License is cached in `~/.sourcecode/license.json` and re-validated every 24 hours. Works offline after first activation — network errors keep the cached state.
160
+ License is cached in `~/.ask/license.json` and re-validated every 24 hours.
161
+ The legacy `~/.sourcecode/license.json` location remains readable for existing
162
+ installations. Works offline after first activation — network errors keep the
163
+ cached state.
161
164
 
162
165
  ---
163
166
 
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
- "enforced on write) and reported in `cache status` (F-AR)"
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
  ),