`gc_root_dominance_check.py` reports the **verbatim pre-#7453 code** as clean in all three of its modes. The gate cannot see the bug class it was extended for. ## Reproduction (measured, local) Re-plant the exact code #7453 removed from `expr/url_main.rs`'s `Expr::UrlNew`: ```rust let url_ptr = ctx.block().call(I64, "js_url_coerce_string", &[(DOUBLE, &url_v)]); let base_v = lower_expr(ctx, base)?; // user code let base_ptr = ctx.block().call(I64, "js_url_coerce_string", &[(DOUBLE, &base_v)]); // allocates ctx.block().call(I64, "js_url_new_with_base", &[(I64, &url_ptr), (I64, &base_ptr)]); ``` Emitted IR (`test_compat_url_date_math`, `PERRY_RS4GC=0 PERRY_GC_MOVING_LOOP_POLLS=1 PERRY_INLINE_SHADOW_SLOT=0`): ```llvm %r883 = call i64 @js_url_coerce_string(double %r882) %r884 = load double, ptr @…_.str.17.handle %r885 = call i64 @js_url_coerce_string(double %r884) ; allocates %r886 = call i64 @js_url_new_with_base(i64 %r883, i64 %r885) ; %r883 is stale ``` Over the 18 `test-files/` sources that exercise the URL lowerings: | checker mode | sabotaged arm | clean arm | |---|--:|--:| | `--moving-only` (dominance) | **0 violations** | 0 | | `--unrooted-allocas --moving-only` | **0 violations** | 0 | | `--stale-registers --moving-only` | **3** | 3 | | `--stale-registers` (no `--moving-only`) | **14**, of which **11 `sink=js_url_new_with_base`** | 3 | The two gated modes are structurally blind: dominance asks *"is the root store late"* and there is no root store; `--unrooted-allocas` asks *"does this alloca have a root store"* and the value is in an SSA register, not an alloca. `--stale-registers` is the mode whose help text does cover it — *"report every register holding a GC value that is USED below a collection point without being re-read from a root (#7154)"* — and it does see it, but only with `--moving-only` off. ## Cause `js_url_coerce_string` is in `ALLOC_RE` but **not in `POLL_CAPABLE_RUNTIME`**. `ALLOC_RE` decides whether a register *has* a heap-value source; `POLL_CAPABLE_RUNTIME` decides whether the *window* around it is MOVING, which is what `--moving-only` gates on. #7453's own fix added `url_coerce_string` to `ALLOC_RE` — the code comment there says in as many words *"That gap is why the checker did not flag #7453"* — and stopped one list short, so the addition never made the shape catchable under the mode CI runs. It is not an exotic callee: `js_url_coerce_string` (`crates/perry-runtime/src/url/mod.rs:116`) is a five-line wrapper whose body calls `js_jsvalue_to_string`, which **is** in `POLL_CAPABLE_RUNTIME`. This is the dual of the hazard `--audit-poll-capable` already gates. That auditor catches an entry that names *no* symbol; nothing catches a real allocating symbol that is *missing* from the list, and a missing entry suppresses findings just as silently. ## Measured effect of the one-line fix Adding `"js_url_coerce_string"` to `POLL_CAPABLE_RUNTIME`: - `--audit-poll-capable`: still clean (55 entries, all real). - curated corpus (129 sources, 149 modules), `--moving-only` dominance: **0 violations**, unchanged. - curated corpus, `--stale-registers --moving-only`: **23 before, 23 after** — no change at all, because `scripts/gc_root_dominance_corpus.sh`'s `PATTERNS` list has no URL prefix and the curated corpus therefore never compiles a `expr/url_main.rs` lowering. That is a second, smaller gap: the gate corpus does not exercise the module this bug lives in. - the sabotaged URL arm under `--stale-registers --moving-only`: 3 → **14**, i.e. the bug becomes visible. **Not measured locally: the dependency-scale (`zod`) arm and its `--max-stale 118` budget.** That is the only reason this is a separate issue rather than a line in the Layer 1 template PR — widening a gate is its own change, and it has to measure both corpora before it lands. ## Suggested scope 1. Add `js_url_coerce_string`, then re-measure both corpora and re-pin `--max-stale` if it moves. 2. Sweep `ALLOC_RE`'s coercion family for the same one-list-short mistake (`js_string_coerce*`, `js_jsvalue_to_string*` and `js_number_coerce` are already in both; the question is which other `ALLOC_RE` alternatives are absent from `POLL_CAPABLE_RUNTIME`). 3. Consider a **third auditor**: for every callee that both `ALLOC_RE` matches and the runtime shows re-entering JS or allocating, require it to be in `POLL_CAPABLE_RUNTIME`. That closes the direction `--audit-poll-capable` does not. Found while migrating `expr/url_main.rs` onto the Layer 1 rooting API; the sabotage arm exists precisely to answer "does anything catch this".