contributor-scan 2.11. libpng emitted 198 sections, 163 of them asserting that `memcpy` and `strcpy` are PRESENT in a library that uses them correctly throughout. That is the noise developers have learned to skip in every other tool, and shipping it costs the findings beside it their credibility. **Filtering on rung was the obvious rule and it was wrong.** The obligations checker emits at `suspicion` too, so hiding that rung would have hidden an entire detector -- six tests failed and they were right to. The discriminator is evidence_level: on libpng 163 of 198 rows are `static_corroboration`, on vampi 3 of 60 are, and the model layer and the obligations checker carry `suspicion` there instead. That is exactly the distinction the ladder exists to make -- an undecided CLAIM about a site is not an observation that an API exists. So a pattern-only rule is grouped once it has more than three sites, and keeps its full sections under that. One `python-flask-debug` is a finding a contributor can act on and must not be summarised away; fifty-four `c-unsafe-copy` are a property of the codebase. libpng 198 findings, 198 sections -> 39, 161 grouped into three lines vampi 60 findings, 61 sections -> 61, nothing grouped Counted, never dropped: findings.json stays complete because it is the record a baseline is compared against, and the grouped section says the model reached none of these and that its silence means no fact table covered the site rather than that the site is safe. What it does not fix, recorded rather than glossed: vampi still emits 61 sections for a 520-line application, 51 of them reasoned claims this rule does not touch. It fixes pattern noise, not volume. Also records what group 2 established about the three corpus asks, so none is started on a wrong assumption. Joern ships php2cpg but our fact tables have zero PHP families, so WordPress would parse and produce nothing -- fact tables first, corpus second. An older vulnerable libpng will not help for a reason no version can fix. Vibe-code repositories are the best fit and the cheapest route to a second true-positive checkout, because Python and JavaScript are the two languages whose fact tables are complete. And Group 8, for per-repository state a developer owns: a committed dismissal file the tool reads and never writes, dismissals as a split evaluation set, and GEPA/SIMBA-style evolution scoped to the QUESTION -- the judge and residual prompts -- never to the fact tables. That boundary is the whole design: a prompt is how we ask, a fact table is what we claim, and the closed-loop leak measured on 2026-09-05 is why the distinction is not negotiable. Gates byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
38 KiB
Implementation Plan: contributor-scan
Guidance for the implementing agent
- The predecessor's lesson, in one line. Five bugs in
model-grounded-detectionshared one shape: a taint-specific assumption applied to every family, each caught by a measurement rather than a check. Before you finish any change to a routing rule, a matching rule, or a per-family question, grep for its siblings and fix them in the same commit. The most expensive instance cost a 19-minute run that returned an identical number, and only the identity revealed the bug. - Run the whole CI list before pushing, not the parts you remember:
ruff format --check .,ruff check .,mypy src/openultrasast,pytest, the three gates,compileall src tests, andpytest -m semantic. CI was red for most of a session becauseruff checkpassing was taken as evidence thatruff format --checkwould. - The gate baseline is the safety net.
benchmarks/measurements/2026-09-06-gate-baseline.txt. Run all three gates before and after every task. The model layer is additive: if a gate moves, you changed something that was never meant to change. - RED first, one commit per task, selective
git add. - Zero-dependency core. Copy the capability-probe pattern in
semantic/extra.pyandcpg/capability.py. - Do not modify
model/pipeline,taint,dominance,config_value,specsorcandidates. They are built and measured. If a change looks necessary, it belongs in its own commit with its own measurement.
Group 1 — Wire it (gate-protected)
-
1.1 Regions from entry points
model/regions.py:ScanRegion(path, function, families, rank, source)andregions_for(entries, targets, *, taxonomy, language_of). Rank from whatEntryPointRecordalready carries (access_level,trust_boundary) — do not invent a new signal. File-level fallback when a target has no entry point, withsource="file_fallback". Families from region shape and language, via the existingtaint_specs/dominance_specs/config_specs.- Sibling audit (Req 8.5): family routing now exists in three places —
ceiling.py's harness,pipeline._arbitrate, and here. Check all three agree before finishing. - Observable: a region is produced per entry point; a target with no entry point yields a file region marked as such; no region carries a family whose spec is absent for its language.
- Requirements: 2.1, 2.2, 2.3, 2.4, 8.5
-
1.2 The repository driver
model/scan.py:ScanBudget,ModelScanResult,scan_repository(...). One CPG per run, reused across regions. Regions in rank order; budget decremented per model call and checked before each; the remainder counted intoregions_unjudged. Findings ordered by rung, then rank.- Observable: one
backend.buildcall for many regions; a budget of N yields at most N model calls and a non-zeroregions_unjudged; ordering putsmodel_entailedabovesuspicion; a failed CPG build yields an empty result withcpg_build_failed, never an exception. - Redaction (Req 8.3) is inherited from
model/pipeline, which redacts every prompt; a test asserts the driver adds no unredacted path of its own. - Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 8.3
- Depends: 1.1
-
1.3 The MAP stage
- Wire
scan_repositoryintocli.pyinsideStage.MAP, mirroring_check_obligations: takesentry_pointsandtargets, returns findings merged additively beside the pattern, overlay and obligation findings. A[model]config block (enabled, budget, max regions), zero-dep, conservative defaults. Payload into the manifest withby_rung, cost and elapsed. - Observable: a scan on a repo with Joern present reports findings carrying rungs; the same scan with
OPENULTRASAST_JOERN_PROBE=offis identical to today's output plus acpg_unavailabledegradation; the three gates are byte-identical. - The
[model]block is capability-detected like every other optional plane (Req 8.4): core staysdependencies = [], and the engine, endpoint and execution tier each degrade to a recorded reason. - Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 8.4
- Depends: 1.2
- Wire
-
1.4 Batch the queries — one Joern invocation per scan, not one per region and family
- Why this is in group 1 rather than group 2. Wiring 1.3 exposed that the cost model in the design was
half right. One CPG per scan was the right call, but every query is its own
joern --scriptJVM launch of roughly 30 seconds, and the driver issues one per region per family. A single ten-line Python file admits six families and therefore takes six launches and over three minutes. At 100 regions that is ~5 hours; at 1000, ~50. Task 2.2's go/no-go would not have been a measurement, only arithmetic. - Extend
cpg/backend.pywith a batched call: one invocation carrying many (region, family, spec) requests and returning rows keyed back to each. The.scscripts already take parameter lists; they take a list of requests instead.model/scan.pycollects the work first, issues one call, then arbitrates from the rows. - Keep the seam. The subprocess boundary stays the only Joern touch-point (predecessor Req 4.5). Server mode would keep a JVM warm but adds a process model and weakens that promise; batching needs neither.
- The arbiters do not change.
taint,dominance,config_valueandpipelinetake rows and are already measured; if this task needs to edit one, that is a signal the batching is leaking into them. - Observable: a scan over N regions issues one
joern --scriptinvocation, not N×families; the same fixture produces the same findings and rungs as the unbatched path, asserted against a recorded baseline; the ten-line scan drops from minutes to seconds. - Requirements: 3.5, 4.2, 4.3
- Depends: 1.3
- Why this is in group 1 rather than group 2. Wiring 1.3 exposed that the cost model in the design was
half right. One CPG per scan was the right call, but every query is its own
Group 2 — Scale it (the phase that can reshape the rest)
-
2.1 A pinned known-vulnerable checkout
benchmarks/repos/<name>.toml: url, commit, the known CVE, the file and function it lives in, licence. Start with one small enough to iterate on. Fetch is explicit and offline-by-default, like the pointer pairs.- Observable: the recipe resolves to a checkout; CI never fetches it.
- Requirements: 4.1
- Depends: 1.3
-
2.2 The first repository measurement
- Run the full scan against that checkout and commit the artifact: CPG build time, query time and total
scan wall-clock as three separate numbers, peak memory, findings by rung, cost,
regions_unjudged, and whether the known CVE was found and at which rung. The build/query split is separated because 2.3 is decided on that ratio and cannot be decided on a single wall-clock figure. If the repository cannot be processed in its envelope, record the stage it failed at. - This is the go/no-go for the shape of the rest: if one CPG per repository is infeasible, the region and budget design changes and phases 3–6 should not be built on it first.
- Observable: the artifact exists with every field; the reading states go or no-go and why.
- Requirements: 4.2, 4.3, 4.4, 4.5
- Depends: 2.1
- Run the full scan against that checkout and commit the artifact: CPG build time, query time and total
scan wall-clock as three separate numbers, peak memory, findings by rung, cost,
-
2.3 Decide on Joern server mode, on 2.2's numbers
- Joern offers
--serverwith--server-host,--server-portand basic auth: one warm JVM with the CPG loaded once, and queries in milliseconds after that. It would remove the three-to-four JVM starts (~100s) that remain per scan now that 1.4 has batched the queries. - Deliberately sequenced after the measurement, because deciding it now would be optimising before measuring. Batching already moved query cost from O(regions) to O(1) per scan; whether the remaining floor matters is a ratio 2.2 produces and nothing before it does. If a real CPG build takes ten minutes, a 100s query floor is noise and server mode buys nothing. If the build is thirty seconds, the floor is 3x the build and server mode is the obvious next move.
- Costs to weigh against that ratio: process lifecycle (start, health, shutdown, orphan reaping -- stray
JVMs had to be killed by hand twice while building the batching); the seam weakening from "the subprocess
is the only Joern touch-point" to "subprocess or HTTP client", which is the promise that has kept the
backend replaceable; and that a CPGQL endpoint evaluates Scala, so it is arbitrary code execution on the
analysis host -- acceptable inside the shipped container, which runs
network_mode: none, and much less so as a port listening on a contributor's laptop. - Observable: a recorded decision that cites 2.2's build-time-to-query-time split. A preference does not close this task; a ratio does.
- Decided: no. On libpng (89k lines of C) the query phase splits taint 243.3s / config 10.7s over two
invocations, and fixed overhead measured on that CPG is ~13s per invocation. So ~26s of 250s is JVM
startup and ~224s is CPGQL evaluation: a warm server removes 6% of a 446s scan and none of the part that
costs. Recorded in
benchmarks/measurements/2026-09-09-repo-libpng-envelope.json. - The small repository pointed the other way -- vampi's 34.3s over three kinds is roughly three startups and almost no evaluation -- and the ratio inverts with size. That inversion is precisely why this task was sequenced after a real repository instead of being decided on a 520-line file.
- Requirements: 4.2
- Depends: 2.2
- Joern offers
-
2.4 A region that spans the flow, not just a function
- 2.2's finding, and the one that reshapes the phase. VAmPI-SQLI is a request parameter reaching
get_by_username(username), passed toUser.get_user(username)in another module, interpolated into a query and executed. Source and sink are in different functions, regions are per-function, and a repository scan does not treat parameters as untrusted -- so neither region sees both ends and the engine is silent on the most ordinary shape a real injection bug has. - The CPG already has the call graph;
reachableByFlowsis not function-bounded. What is bounded is the QUESTION the driver asks. So the work is in the region model and the query scope, not in the arbiter: a region should be able to name an entry point and admit sinks reachable from it, with the file scope from 2.2 kept so the answer is still attributable. - Watch the cost: an unbounded interprocedural question over a large repository is how this gets slow again. Measure it on vampi first, then on the envelope checkout.
- Observable: VAmPI-SQLI entailed or corroborated at
models/user_model.py:get_user, with the scan's wall-clock and the entailed-finding count recorded against 2.2's numbers. - Requirements: 4.4, 8.1
- Depends: 2.2
- 2.2's finding, and the one that reshapes the phase. VAmPI-SQLI is a request parameter reaching
-
2.5 Module-level regions the engine can actually ask about
- A setting in
if __name__ == '__main__':belongs to a region the mapper callsapp.py:__main__, but Joern names that method<module>. The function filter matches nothing, so module-level configuration is invisible. Before 2.2's file scoping it was worse, not better: the literal was reported eight times in eight files that do not contain it. - Observable:
app.py:17'shost='0.0.0.0'reported exactly once, at that location. - Requirements: 4.4
- Depends: 2.2
- A setting in
-
2.6 Access control that does not entail every public endpoint
- 7 of 9 VAmPI handlers were entailed, including
register_userandget_all_users, which are meant to be unauthenticated. The dominance rule -- unguarded, with guarded siblings -- was calibrated on pairs where a guarded twin always existed. At repository scale every public endpoint has guarded siblings. - This is a precision question and needs a precision measurement: on a real repository, what fraction of entailed access-control findings are endpoints that genuinely require authorisation? Do not tune it against VAmPI alone; that is how the +16% overfitting gap in Req 8 happened.
- Observable: a recorded precision figure before and after, on more than one checkout.
- Requirements: 8.1, 8.2
- Depends: 2.2
- 7 of 9 VAmPI handlers were entailed, including
-
2.7 A dominance witness that names its line
- Access-control sites read
api_views/users.py:?:update_password. Taint and config witnesses carry(line N); dominance does not, so its findings have no line and a contributor cannot open them. - Observable: every entailed finding in a repository scan has an integer line.
- Requirements: 5.1
- Depends: 2.2
- Access-control sites read
-
2.8 An endpoint that declares itself public carries no obligation
- 2.6's remainder. Both false positives left on VAmPI are
register_userandlogin_user, which its OpenAPI spec declares public with nosecurityblock. The arbiter cannot see that declaration, so it reports a missing guard on an endpoint whose contract says there is nothing to miss. - The mapper's
access_levelis NOT a safe substitute on its own. For an OpenAPI route it comes from the declaredsecurityblock and is trustworthy; for a decorator-based framework "public" means only that no@login_requiredwas found, which is precisely the bug this family exists to report. Suppressing on that would silence the primary detection case for Flask and Express. - So the work is to carry the PROVENANCE of the access level, not just its value: a declared contract
removes the obligation, an absent decorator does not.
EntryPointRecord.provenancecurrently only echoes the level, so this needs a real signal rather than a string match on evidence text. - Observable:
register_userandlogin_userno longer entailed on vampi, with the pair-corpus leak rate unchanged at 0% on the non-VAmPI pairs -- the check that this suppressed a declaration and not a family. - Requirements: 8.1, 8.2
- Depends: 2.6
- 2.6's remainder. Both false positives left on VAmPI are
-
2.9 One defect is one finding, however many regions reach it
- libpng's scan produced 26 entailed findings that are all the identical site,
contrib/gregbook/wpng.c:335:main. Many regions reach one shared sink and each emits its own finding; nothing dedupes by site. A contributor would be shown the same defect twenty-six times. - Invisible at vampi scale, where regions rarely share a sink, and it appeared the moment task 2.4 let a region's question follow the call graph.
- Dedupe on the reported site, keeping the strongest rung and recording how many regions reached it -- "reached from 26 entry points" is useful information, twenty-six rows are not.
- Observable: libpng's entailed count reflects distinct sites; the artifact records both numbers.
- Requirements: 5.1, 8.1
- Depends: 2.4
- libpng's scan produced 26 entailed findings that are all the identical site,
-
2.11 The suspicion band has to be worth reading
- libpng's scan produced 198 rows: 26 entailed (all one false positive, since fixed), 113
c-unsafe-*pattern matches that assert an API is PRESENT rather than misused, 50 overlay-coverage annotations that are not findings at all, and 9 model suspicions. Zero validated defects. - The rung is honest -- these say "needs review" and mean it -- but a contributor handed 163 rows about a
library that uses
memcpycorrectly throughout will not read the one that matters. 82 of the 113 are incontrib/, so task 2.10 removes most of it; what remains needs a policy, not a filter invented here. - Observable: a stated rule for what reaches a contributor's report at
suspicion, and libpng's row count under it, measured. - Requirements: 5.1, 5.2
- Depends: 2.10
- libpng's scan produced 198 rows: 26 entailed (all one false positive, since fixed), 113
-
2.10 Do not spend the scan on example code
- 22 of libpng's 30 highest-ranked regions are under
contrib/-- the sample programs shipped with the library rather than the library. The budget and the 243s taint query go there first. - The rank already comes from the entry-point mapper; what is missing is that a path can be evidence too. Vendored, generated, example and test trees are not what a contributor is changing. This is also the cheapest available reduction of the 243s.
- Do not hardcode
contrib/: derive it, record what was excluded and why, and measure the scan with and without so the exclusion is a number rather than a preference. - Observable: a recorded before/after on libpng -- regions, query seconds, and entailed sites.
- Requirements: 4.2, 8.1
- Depends: 2.2
- 22 of libpng's 30 highest-ranked regions are under
-
2.12 A C flow that a bounds check governs is not a finding
c/injectionandc/memorycarry ZERO sanitizers, so no C flow can ever be discharged. Everyargv -> strcpyentails however carefully it is bounded, which is why libpng reportsstrcpy(outname+len, ".png")at wpng.c:374 -- guarded four dozen lines earlier byif ((len = strlen(inname)) > 250)against achar[256].- A sanitizer list will not fix this: the discharge is not a call, it is a GUARD that dominates the sink.
That abstraction already exists and already works -- it is what
dominance.scdoes for access control -- and it is wired to one family only. The work is to let a taint family name a guard-shaped discharge and route it through the arbiter that can decide one. - Observable: wpng.c:374 not entailed, with the pair corpus and both repositories measured either side. Do
not do this by adding
strlento a sanitizer list; that would discharge the unguarded case too. - Requirements: 8.1, 8.2
- Depends: 2.9
-
2.13 A destination allocated to size is bounded too
- 2.12's remainder, and libpng's last entailed finding:
strcpy(output + len, ".icc")at contrib/examples/iccfrompng.c:124, whereoutput = malloc(len + 5)and a five-byte copy lands at offsetlen. It fits exactly. There is no comparison anywhere, so the bound rule cannot see it -- the destination is ALLOCATED to size rather than bounds-checked. - Deliberately not done inside 2.12. Both discharge shapes were found on the same repository, and extending a rule until one checkout is clean is the overfitting Req 8 exists to prevent. This wants a second C checkout with a genuine CWE-121 first, so the change can be measured for what it silences as well as what it clears.
- Observable: iccfrompng.c:124 not entailed, AND a real overflow on another checkout still entailed. Without the second half this task should not be closed.
- Requirements: 8.1, 8.2
- Depends: 2.12, 5.3
- 2.12's remainder, and libpng's last entailed finding:
-
2.14 Control dependence is not populated, and two arbiters want it
- Measured, not assumed: 0 of 13,184 calls in the libpng CPG and 0 of 1,152 in the vampi CPG have a
controlling control structure.
controlledByreturns nothing in either, so the PDG's control half is simply absent from the graphs we build. The data half is present and load-bearing --reachableByFlowsis the whole taint arbiter, andjoern-parsereportsdataflowOssapplied. - Everything guard-shaped we do therefore uses CFG dominance (
dominatedBy) or syntactic conditions (method.controlStructure.condition) instead. That is why 2.12's first attempt failed: libpng's bound reaches its sink through anerrorflag, which is a control-dependence fact and nothing else. - Find out whether a CDG overlay can be enabled on
joern-parseor in the script, what it costs on the envelope checkout, and whether it makes the flag-mediated guard decidable. If it cannot be enabled, record that -- it bounds what guard reasoning this engine can ever do. - Observable: a recorded answer with the build-time and query-time cost, and wpng.c:374 decided or a stated reason it cannot be.
- Requirements: 4.2, 8.1
- Depends: 2.12
- Measured, not assumed: 0 of 13,184 calls in the libpng CPG and 0 of 1,152 in the vampi CPG have a
controlling control structure.
-
2.15 A C checkout whose CVE goes through a sink we model
- libpng cannot demonstrate a C true positive, and pinning an older vulnerable commit would not change
that. Every one of the twelve files using
strcpy/strcat/sprintf/getsis undercontrib/; the library itself uses none of them. Its overflows are array and pointer arithmetic andmemcpysizing, and five buffer-overflow fixes checked (png_set_quantize,png_image_finish_read,png_init_read_transformations,png_do_quantize,png_write_image_8bit) touch no modelled sink at all. - So there are two ways forward and they are different sizes.
- Cheap: pin a C project whose CVE genuinely runs through a string function. That gives the memory family its first true positive and tells us whether the family works at all. It does not make libpng decidable.
- Expensive, and a different engine: model how C actually overflows. A
memcpywhose size is computed, an index that is not bounded. That is size arithmetic, not sink matching -- "can this expression exceed this allocation" rather than "does tainted data reach this call". It is a constraint problem, which is what SMT is for and what this project deliberately did not adopt: the architecture decision was abstract interpretation as arbiter, taken because a model layer can be built from code where execution cannot be assumed. That decision was right for the flow families and it does not reach this one. - The maintainer has attacked libpng this way directly -- SMT to lay out memory, then guiding a fuzzer to
produce the PNGs that reach it -- which is evidence about the shape of the problem, not a suggestion to
reimplement it. It says the honest routes to C memory safety are constraint solving or execution, and
the ladder already has a rung for the second (
execution_confirmed, built and unreachable per the module audit). - Observable: a pinned checkout with an in-scope CVE found at a rung, OR a recorded decision that
c/memoryis scoped to string-function misuse and makes no claim about arithmetic overflows. Do not leave it implying it covers a class it cannot see. - Requirements: 4.1, 4.4
- Depends: 2.12
- libpng cannot demonstrate a C true positive, and pinning an older vulnerable commit would not change
that. Every one of the twelve files using
-
2.16 Say what
c/memoryactually covers, or retire it- Its score so far is zero true positives and every finding a false one: 26 from a substring match, 3 from a bound it could not see, 1 from an allocation sized to fit. Each was a real defect in the engine and each is now fixed, but the family has never once been right, and 2.15 explains why -- it models string functions in a codebase class that overflows through arithmetic.
- This spec's predecessor threw away parts that did not carry their weight, and the same question applies
here. The answer may well be "keep it, scoped and labelled" --
strcpymisuse is real in plenty of C -- but it should be a recorded decision with a number behind it, not an assumption. - Observable: either a measured true positive on the 2.15 checkout, or the family's declared scope narrowed in the facts and the docs so no reader takes a silent C scan for a clean bill of health.
- Requirements: 3.1, 8.2
- Depends: 2.15
Group 3 — Ship it
-
3.1 Contributor output
model/report.py: a scan summary — counts per rung, the top findings with witness andpath:line, and a plain statement of whatsuspicionmeans (the model could not confirm it). Findings to the writable output only.- Observable: a scan produces a summary readable without opening JSON; every finding shows rung, witness and location; nothing is written into the analysed tree.
- Keeps the disciplines already built into
model/report.py(Req 8.1, 8.2): per-slice rows with the real-world slice deciding, a refused bare aggregate, and the overfitting gap published beside any headline — absent rather than zero when a slice is missing. - Requirements: 5.1, 5.2, 5.3, 5.4, 8.1, 8.2
- Depends: 1.3
-
3.2 Build and verify the image
docker compose build, then run a real scan through it and through the shell wrapper. The Dockerfile and compose exist and are syntax-valid; the image has never been built, so nothing about it is proven.- Observable: the image builds;
TARGET=<repo> docker compose run --rm ousast scan /targetproduces the same findings as a native run; the target mount is read-only; the network is off by default. - Requirements: 6.1, 6.2, 6.3, 6.4, 6.5
- Depends: 3.1
Group 4 — Close the deferred decisions
-
4.1 The memory-family decision
- Either reach
memorythrough an adopted execution tier, or record explicitly that C memory-safety is out of scope. 173 of the 176 real-world CVE pairs sit idle behind a stub; the decision gates Req 9.2. - Observable: the decision and its reason are recorded where a later reader finds them; if out of scope, the corpus says so per slice.
- Requirements: 7.1, 7.3
- Either reach
-
4.2 Retire
evidence_level- It duplicates
rungin intent. Retire it, or record why both are kept. Note the legacy valuestatic_corroborationcomes from the flat-IR overlay, which Req 4.4 of the predecessor demotes to the suspicion band — so a straight rename would overstate it. - Observable: one vocabulary, or a recorded reason for two; SARIF and JSON consumers considered.
- Requirements: 7.2, 7.3
- It duplicates
Group 5 — Grow the corpus
What group 2 established about each of these, so none is started on a wrong assumption:
- PHP/WordPress: Joern ships
php2cpg, so the engine can parse it. The blocker is ours --taint_specs(language="php")returns zero families, as do ruby and go. WordPress would parse and produce nothing, which is the quiet failure that reads as a clean bill of health. The work is fact tables first, corpus second. - A known-vulnerable old libpng: measured and it will not work, for a reason the version cannot fix.
Every file using
strcpy/strcat/sprintf/getsis undercontrib/; the library uses none of them and overflows through arithmetic instead. See 2.15 and 2.16. - Vibe-code repos: the best fit available. Python and JavaScript both have full fact tables, the pair corpus already carries 70 vibe-py and 17 vfc-js pairs, and vampi -- the one repository where the engine finds 3 of 3 -- is exactly this shape. A whole-repository vibe-code checkout is the cheapest next measurement that could produce a true positive.
-
5.1 PHP/WordPress slice
- Plugin and core weaknesses.
php2cpgneeds a PHP interpreter, which the image provides. Validate on a sample that the model can read the pairs before vendoring — the OWASP harvest found 238 and vendored 40 for exactly this reason. - Observable: a sample measurement precedes vendoring; only categories the model can arbitrate are vendored; the slice reports as its own row; every pair states a licence.
- Requirements: 9.1, 9.4, 9.5, 9.6, 9.7
- Depends: 3.2
- Plugin and core weaknesses.
-
5.2 More vibe-code pairs, and at least one as a whole repository
- The pair corpus has 70 vibe-py and 17 vfc-js pairs, all excerpts. Group 2 showed that excerpts cannot see the defects a repository shows: nine engine bugs this session, none of them visible to the corpus, the gates byte-identical through every fix.
- So the higher-value half of this task is a pinned vibe-code repository, not more pairs. It is the cheapest measurement that could produce a second true-positive checkout, because Python and JavaScript are the two languages whose fact tables are complete.
- Observable: a
benchmarks/repos/recipe for a vibe-code checkout with in-scope known vulnerabilities, and a measurement saying which were found at which rung. - Requirements: 9.1, 9.2, 4.1
- The slice where the stated audience works, currently 35 pairs. Same read-check discipline.
- Requirements: 9.3, 9.4, 9.5, 9.6, 9.7
-
5.3 Real-world C library CVEs
- libpng and the image libraries, OpenSSH, libpam, kernel modules. Gated on 4.1: until the memory decision closes these would join 173 idle pairs.
- Requirements: 9.2, 9.4, 9.5, 9.6
- Depends: 4.1
Group 6 — Regression baselines
- 6.1 Repository baseline and delta
- A committed baseline per pinned checkout; a delta report on fact-table or query changes showing findings gained, lost and rung movements. An unexplained loss of a previously-entailed finding is a regression to investigate, not a number to accept. The baseline records the engine version, because a CPG engine change moves every verdict.
- Requirements: 10.1, 10.2, 10.3, 10.4
- Depends: 2.2
Task-plan review notes
- Group 1 must land and pass review before group 2: measuring an unwired pipeline is what produced a session's worth of harness-only numbers.
- Task 1.4 was added after 1.3 was wired, when a ten-line scan took three minutes. It belongs in group 1 because group 1's promise is a working scan, and a scan that cannot finish on a real repository is not one. Finding it here rather than in 2.2 is the argument for wiring before measuring.
- Task 2.2 is a go/no-go. If one CPG per repository is infeasible, stop and redesign the region/budget model rather than building groups 3–6 on an assumption.
- Group 5 is deliberately last. The corpus is not the binding constraint today — reachability is — and 198 of 238 OWASP pairs were left unvendored precisely because volume without readability makes every number worse.
Group 7 — Stop repeating the same false positive
Three false-positive classes were found by hand today, and two of them were the same defect in a different
matcher: resolve matched inside resolveUrl, user inside users, gets inside fgets. Each was fixed
locally, and each next one survived because nobody carried the fix across. That is the thing to automate --
not the judgement, the propagation.
The constraint that shapes every task here: no LLM-authored patterns or policy. A model may propose a hypothesis and draft a change; only a deterministic measurement may adopt one. And any loop that learns from the corpus must respect the train/holdout split, because the last one did not: five of eleven candidate shapes were taught by the holdout pairs they then "recovered", turning a -0.059 Youden into +0.059.
-
7.1 One matcher, asserted to be the only one
- The three bugs above existed because three modules each grew their own token-matching rule. There is now
one bounded matcher in
taint.scand one indominance.sc, still separate copies of the same logic. - Make it one definition the queries share, and add a test that fails when a second appears -- a grep-level invariant is fine and is what would have caught the third instance.
- Observable: a test that fails if any query matches a spec token with a bare
containsorstartsWith. - Requirements: 8.1
- The three bugs above existed because three modules each grew their own token-matching rule. There is now
one bounded matcher in
-
7.2 A counter-example ledger
- When a finding is contradicted -- by a maintainer, by the judge with coverage, or by a declared contract
like an OpenAPI
securityblock -- record it: the site, the rung, the witness, the spec tokens that produced it, and the commit. That artifact is the durable part; today's three fixes left no trace a future regression could be checked against. - Each entry becomes a regression case: this site, at this commit, must not be reported at that rung.
fgets(buf, 256, f)and VAmPI'sregister_userare the first two. - Observable: the ledger is committed, the cases run in CI offline, and re-introducing the substring match fails a test rather than a repository scan four weeks later.
- Requirements: 8.1, 10.3
- Depends: 2.9
- When a finding is contradicted -- by a maintainer, by the judge with coverage, or by a declared contract
like an OpenAPI
-
7.3 Group the ledger by cause, and let a model propose the cause
- Twenty-six identical findings were one defect; three separate bugs were one bug class. A deterministic pass should group counter-examples by what they share -- same spec token, same matcher clause, same family, same witness shape -- and report the groups by size.
- This is where the model earns its place, and only here. Given a group, ask it for a hypothesis: what do these share, and what change would remove all of them? It drafts; it does not decide. The output is a proposed fact-table or matcher edit with a rationale, written to a review queue -- never to the ruleset, and never to the declared policy file.
- Observable: run it against today's three fixes with the fixes reverted, and record how many it groups correctly and what it proposes. A model that cannot rediscover a known cause should not be trusted with an unknown one.
- Requirements: 8.1, 8.4
- Depends: 7.2
-
7.4 Adopt only what a split-respecting measurement confirms
- A proposed edit is adopted only if it removes its counter-examples AND does not lose pair-corpus recall, measured on the train split with the holdout untouched. The predecessor's improve lever admitted a shape because it "recovers a currently missed holdout pair", which is how a -0.059 Youden was reported as +0.059.
- The gates already exist and must stay byte-identical across an adoption, or the adoption is a regression wearing a fix's clothes.
- Observable: an adoption record per accepted edit -- counter-examples removed, train recall before and after, holdout untouched and unread, gates identical.
- Requirements: 8.1, 8.2, 10.2, 10.3
- Depends: 7.3
-
7.5 Report what the loop cost as well as what it bought
- A false-positive reducer that quietly trades recall is worse than none, and the trade is invisible unless it is measured on both sides. Today's 2.6 removed three leaks and lost no correct detection; that is the shape of an acceptable trade and it was only knowable because both numbers were taken.
- Observable: every adoption records precision and recall on both repositories and the pair corpus, and a net-negative adoption is reverted automatically rather than argued about.
- Requirements: 8.2, 10.3
- Depends: 7.4
Group 8 — Per-repository state a developer owns
"an evolution (gepa, simba) style prompting with a declarative and persistent approach when evaluating findings per target repo? so that devs can commit state and evolve the harness / dismiss findings"
Yes, and Group 7 is half of it already -- 7.2's counter-example ledger is the persistent state, 7.3 is a model proposing from it, 7.4 is a split-respecting adoption. What this adds is the part that makes it a developer's rather than a maintainer's: the state lives in the target repository, committed beside the code, and it is declarative.
The boundary that decides the design. A reflective optimiser may evolve a prompt, because a prompt is
how we ask a question. It may not author patterns or policy -- the fact tables and the rulesets stay
human-authored, and the tool never writes the declared policy file. So GEPA-style evolution has exactly one
legitimate target here: the judge and residual questions in model/pipeline.py, which are already the
LLM-facing surface. Evolving those against a repository's own dismissals is sound. Evolving sink tables from
them is the thing this project has refused from the start, and the closed-loop leak is why it is right to.
-
8.1 A dismissal file the developer owns
.openultrasast/dismissals.tomlin the TARGET repository: finding id or site, the rung it was dismissed at, a reason, and who. Committed with the code, reviewed like code, and diffable -- a dismissal without a reason is how a suppression file becomes a graveyard nobody can audit.- The tool READS it and never writes it, which is the same rule the declared policy file already follows.
ousast scanreports dismissed findings as dismissed with their reason rather than hiding them, so a stale dismissal is visible rather than silent. - Observable: a scan of vampi with two dismissals reports 3 findings and 2 dismissed-with-reason, and the file is untouched by the run.
- Requirements: 5.1, 5.2
-
8.2 Dismissals are an evaluation set, per repository
- A dismissal is a labelled example: this site, at this rung, is not a finding here. Together with the
known vulnerabilities a
benchmarks/repos/recipe already declares, that is a per-repository eval set with both classes in it -- which is exactly what an optimiser needs and what a suppression list normally throws away. - Split it the way the pair corpus is split. A dismissal that teaches the prompt that recovers it is the leak measured on 2026-09-05, where five of eleven candidate shapes were taught by the holdout pairs they then recovered and a -0.059 Youden was reported as +0.059.
- Observable: the eval set builds from a recipe plus a dismissal file, with a declared train/holdout split and a test that the holdout is never read during optimisation.
- Requirements: 8.2, 10.2
- Depends: 8.1
- A dismissal is a labelled example: this site, at this rung, is not a finding here. Together with the
known vulnerabilities a
-
8.3 Evolve the QUESTION, never the facts
- GEPA/SIMBA-style reflective evolution over
CANDIDATE_QUESTIONandresidual_question, scored on 8.2's train split: fewer dismissed sites affirmed, no known vulnerability lost. - Hard boundaries, and they are the point of putting this in its own group: the optimiser proposes prompt text only; it never edits a fact table, a ruleset or the declared policy file; an evolved prompt is committed by a human like any other change; and the three gates stay byte-identical or the evolution is a regression wearing a fix's clothes.
- Observable: an evolution record per accepted prompt -- train score before and after, holdout untouched, known vulnerabilities still found, gates identical, and the diff of the prompt.
- Requirements: 8.1, 8.2, 10.3
- Depends: 8.2, 7.4
- GEPA/SIMBA-style reflective evolution over
-
8.4 Say what the harness learned, in the repository it learned it in
- Per-repository state is only useful if a developer can see what it did. A committed record of which dismissals shaped the current prompt, and what that cost on the shared corpus, is what keeps this from becoming a per-repo tuning that nobody can reason about.
- Observable:
ousast reposor the report shows, for a target with evolved state, what was learned and what it was measured against. - Requirements: 5.2, 10.4
- Depends: 8.3