Repository navigation
Epic: detect project-local idiom wrappers as a fact channel (follow-up to #3003) #3313
Description
Follow-up to #3003, which documents the limitation in docs/known_blind_spots.md (Blind spot 1).
Problem
Literal-vocabulary rules count calls to a primitive by name. When a project wraps the primitive in its own helper, the rule goes blind to every call site of the helper:
- curl 8.17 → 8.18: summed
state_memory_allocfalls 1197 → 171 (−86%) across thecurlxallocator refactor, while LOC grows (temporal-crucible walk panel, 2026-09-13). - CPython, reproduced in the pinned crucible (v1.4.0): all 7 CPython C files record
Manual Memory Allocation = 0, yet they make 45 calls throughPyMem_*/PyObject_*.
In both cases the wrapper is defined in a different file from its callers, so no same-file heuristic can see it.
Design (decided on #3003)
Wrappers are a fact, not a count. The literal signal keeps its syntactic, cross-language meaning, and wrapper knowledge rides alongside it as a named relation, the same signal/fact split the fact channels use (#3200, #3201, #3246, #3250). An opt-in per-project alias list was considered and rejected: it goes unfilled, goes stale, and makes two scans of the same code disagree.
Priority and scope (after steps 0-2; see the comments below)
Medium priority, and memory_alloc is in. The crucible census (step 0) found the in-sample gap concentrated in error and logging helpers (debug_prints, panics_and_aborts). The full-clone census (step 2) found the allocator gap is large and detectable:
- curl: its allocator layer is function-like
#definealiases. Literal calls fall 82% across 8.17 → 8.18; literal plus aliases moves +4%. - CPython: its allocator API is function wrappers, with about 10× more wrapper calls than literal ones.
Sub-issues (to file as each starts)
-
Census: how common are thin wrappers?Done. Results are in the comment below. -
Detection heuristic plus a measured precision bar.Done (#3313 step 1: thin-wrapper detection heuristic with a measured precision bar (debug_prints, panics_and_aborts) #3315, PR wrapper_probe: thin-wrapper detection heuristic with a measured precision bar (#3315) #3316). The result is ≤8 LOC with an aligned span and no branches, fordebug_prints/panics_and_aborts. The original plan for this step follows.- Define a thin wrapper: a function whose body is short (bounded statement/LOC count) and whose substantive content is a call that hits a literal-vocabulary rule.
- Hand-check precision on the census's real cases: fortran/wrf
wrf_error_fatal/wrf_message, numpyoutmess/errmess, sqliteErrorMsg. - Measure false attribution: the go/core
lockcase, where a same-named function outside the sample receives the calls. Call-site credit needs a resolution rule: same file first, then a unique repo-wide definition, and never a qualifiedx.f(/x::f(call. - Decide whether test-suite helpers (Lua
checkmessage/RUN) count, and settle the thick-wrapper cutoff.
-
Full-clone allocator census.Done (#3313 step 2: full-clone allocator census (CPython, curl 8.17/8.18) -- does memory_alloc need wrapper detection? #3324, PR wrapper_probe: --repo mode and #define alias census; full-clone allocator census (#3324) #3326). The gap is material and detectable, with macro aliases. -
Fact channel
wrapper_data(global/resolved shape, like Mainframe call graph: no edges for COBOL CALL, CICS LINK/XCTL or JCL EXEC PGM=, and unresolved CALL targets are recorded nowhere #3200's call graph). Detection, as measured in steps 1 and 2:- Function wrappers: a per-rule filter, with no-branch for
debug_prints/panics_and_abortsbut not formemory_alloc, since allocator wrappers guard size and NULL. - Function-like
#definealiases in C-preprocessor languages. - A bounded closure across both kinds.
- Resolution: same-file first, then a unique definition; never a method or a qualified call. For macros, any
#ifdefdefinition counts.
Record
wrapper name → kind (function | macro) → primitive or via → rule → defining file, followinggitgalaxy/core/how_to_add_a_fact_channel.mdand theadd-fact-channelskill. - Function wrappers: a per-rule filter, with no-branch for
-
Derived wrapper-aware count, reported beside the literal signal and never folded into it. Example:
debug_prints160 + 278 calls through 2 detected wrappers (the fortran/wrf case).- Needs a contract entry stating it is derived, not a structural signal.
- Decide whether any score may read it; the default is no.
-
Validation.
- fortran/wrf's
debug_prints/panics_and_abortsin the crucible. - The curl 8.17 → 8.18 walk: the literal signal still drops, and the derived figure should show the surface moving behind
curlx_*rather than vanishing.
- fortran/wrf's
Out of scope (stays in known_blind_spots.md)
- Macro wrappers (
#define xmalloc malloc). - Thick wrappers.
- Library delegation (curl's
def_auth: the primitive never appears in the repo).
Activity
squid-protocol commentedon Sep 23, 2026
Step 1 evidence: thin-wrapper census over language-crucible v1.4.0 (2026-09-22)
A read-only census, with no engine change: for every crucible repo group, find thin wrappers and count their call sites against the rule's literal hits.
memory_alloc,debug_prints,telemetry,io,panics_and_aborts,sync_locks,high_risk_execution,regex_execution,serialization_parsing,concurrency,time_date_logic,thread_sleeps,ipc_rpc_bridges,auth_middleware.x.f(,x::f(,x->f() don't count, and names are ≥3 characters. A first pass without these filters was dominated by name collisions (Lua testf/foo,constructor,__init__, RustX::new().How common
The material cases, each read by hand
wrf_error_fatalisprint *,stringfollowed bystop, with 166 call sites;wrf_messagehas 112. Itsdebug_printsgo 278 through wrappers vs 160 direct;panics_and_aborts169 vs 47.outmess/errmesswrapsysstream writes.iois 99 wrapped vs 55 direct.ErrorMsgwrapsfprintf, 64 vs 297 direct.checkmessageandRUNare test-suite helpers wrappingstring.findandos.execute. Whether assertion-helper calls should count as regex or exec use is debatable.lock(calls do not reachconnReader.lock()(a method, always calledcr.lock()). They go to otherlockfunctions defined outside the sample.Findings
obmalloc.cand curl'scurlxare exactly that shape. Sizing allocator wrapping needs full clones.Scope below is updated accordingly.
squid-protocol commentedon Sep 23, 2026
Step 1 filed as #3315 (probe + hand-labelled precision set; no engine change).
squid-protocol commentedon Sep 23, 2026
Step 1 result (#3315, PR #3316)
The heuristic is measured against 65 hand-labelled crucible candidates (
tests/tools/wrapper_labels.json). The decision re-scores in CI from the committed labels.Detection rule for step 3:
Resolution rule:
calls_out_to(unqualifiedname(only).self/cls, qualified header) never receives bare calls. go/core's falselockattribution goes 209 → 0.Test helpers are counted, for consistency with the literal rules, which count test code too.
Residual false positives at the cutoff (recorded in the labels):
sprintfmatched bydebug_prints, a rule quirk.catch @panic(...)error path inside an expression.printpassed as a value.Next: step 2, the full-clone allocator census (CPython, curl), which gates
memory_allocsupport. Step 3, thewrapper_datafact channel fordebug_prints/panics_and_aborts, can start in parallel since it doesn't depend on step 2.Docs: known blind spots -- project-local idiom wrappers (#3003) (#3314)
wrapper_probe: thin-wrapper detection heuristic with a measured preci…
squid-protocol commentedon Sep 23, 2026
Step 2 result (#3324, PR #3326):
memory_allocis material and detectable, with macrosFull clones: curl at
curl-8_17_0/curl-8_18_0and CPythonv3.14.0.#definealiasescurlx_malloc/curlx_free/... are#define curlx_malloc(size) malloc(size)and friends. Literal calls fall 82%; literal plus aliases moves +4%. Function-wrapper detection alone finds 14 sites in 8.18._PyMem.malloc(ctx, size), which the C rule already matches):PyMem_Freehas 1,001 sites andPyMem_Malloc289. Macros such asPyMem_Newchain through them. Wrapper calls outnumber literal calls about 10×.Consequences for step 3, now in the scope below:
memory_allocis in scope.#definealiases are in scope for the C-preprocessor languages.known_blind_spots.md's "macro wrappers stay blind" is corrected for this shape.debug_prints/panics_and_aborts, but not for allocators. Allocator wrappers guard size and NULL, and the no-branch filter would drop 507 CPython sites.free()tocurlx_free.squid-protocol commentedon Sep 23, 2026
Correction to the step-2 comment above: #3316 merged just before step 2 was pushed to its branch, so step 2 is in its own PR, #3326. The
known_blind_spots.mdcorrection likewise landed after #3314 merged and is in #3325.squid-protocol commentedon Sep 23, 2026
Step 3 is up as PR #3334: the
wrapper_datafact channel within the measured scope, plus thememory_alloclabel pass (92% of recorded call sites are real allocator aliases, 99% counting destructors). No signal moves (0 differing cells on a full CPython scan); the golden masters gain only the new report block on 24 files.known_blind_spots: function-like #define aliases are detectable (epic #…
wrapper_probe: --repo mode and #define alias census; full-clone alloc…
Engine: project-local idiom wrappers as a fact channel -- wrapper_data (
squid-protocol commentedon Sep 23, 2026
Step 4 is up as PR #3337:
wrapped_<rule>beside the literal rules (contract indocs/wrapper_aware_count_contract.md; no score reads it). Acceptance: curl, C family, 8.17 → 8.18: literalmemory_alloc1,197 → 171 (−86%, the exact #3003 finding), literal + wrapped 2,629 → 2,719 (+3.4%). The surface moved behindcurlx_*; it did not vanish.Engine: wrapper-aware counts beside the literal rules -- wrapped_<rul…
squid-protocol commentedon Sep 23, 2026
Closing: acceptance met
docs/known_blind_spots.md(#3314, #3325)wrapper_datafact channel within the measured scope;memory_alloclabels at 92% of call sites (99% with destructors)wrapped_<rule>beside the literal rules, contract doc, no score reads itAcceptance (curl, C family, 8.17 → 8.18): literal
memory_allocgoes 1,197 → 171 (−86%, the #3003 finding exactly), while literal + wrapped goes 2,629 → 2,719 (+3.4%). The surface moved behindcurlx_*; it did not vanish.Defects and gaps found along the way, filed separately: #3338 (batch has no
func_start), #3339 (misaligned engine spans), #3340 (perldebug_printscountssprintf), #3341 (contract sheet render not gated), #3342 (stale carddemo differential baseline), #3343 (two-label cancelled gate check).Not filed, deliberately. These are optional and should come with their own evidence if picked up:
new/ Gomakeare a different vocabulary);wrapped_<rule>(ascore-contract-auditchange, per D1).perl: drop sprintf from debug_prints -- it formats, prints nothing (#…
perl: drop sprintf from debug_prints -- it formats, prints nothing (#…
perl: drop sprintf from debug_prints -- it formats, prints nothing (#…
Engine: built-ins are calls -- calls_out ignore sets hold keywords on…