Author SHA1 Message Date
Marius Ciepluch d242ebd59c ci(release): make alpha checkpoint reproducible 2026-11-06 14:34:19 +01:00
Marius Ciepluch c3bf1eece1 style: satisfy release lint 2026-11-06 14:13:29 +01:00
Marius Ciepluch 228f3104b9 style: format release candidate 2026-11-06 14:12:56 +01:00
Marius Ciepluch 7deab25093 chore(release): prepare v2.1.0-alpha.1 checkpoint 2026-11-06 14:12:07 +01:00
Marius Ciepluch 501a214b25 docs(specs): approve scoped evaluation admission design 2026-11-06 14:11:42 +01:00
Marius Ciepluch 3c2f86bbc0 test(detection-core): verify bounded validation cost 2026-11-04 15:09:42 +01:00
Marius Ciepluch aa7c581b25 test(detection-core): verify stage failure visibility 2026-11-04 12:08:32 +01:00
Marius Ciepluch 62b1d90734 test(detection-core): validate end-to-end readiness paths 2026-11-04 10:53:09 +01:00
Marius Ciepluch 586543d799 feat(detection-core): complete single-writer v2 cutover 2026-11-04 09:54:47 +01:00
Marius Ciepluch 10e93e09b7 feat(detection-core): migrate test and operational consumers 2026-11-04 08:53:21 +01:00
Marius Ciepluch 11059c4cd4 feat(detection-core): migrate benchmark readiness and admission consumers 2026-11-03 18:37:44 +01:00
Marius Ciepluch e98d264f62 feat(detection-core): close production legacy consumers 2026-11-02 13:41:35 +01:00
Marius Ciepluch d9a819c853 feat(detection-core): add syntax-only migration boundary 2026-11-02 13:16:00 +01:00
Marius Ciepluch ff8912a2a2 feat(detection-core): isolate legacy compatibility shims 2026-11-02 12:04:13 +01:00
Marius Ciepluch 856312c36e feat(detection-core): isolate legacy v1 ownership 2026-11-02 11:23:18 +01:00
Marius Ciepluch e86fb07856 feat(detection-core): integrate guarded admission custody 2026-11-02 11:03:36 +01:00
Marius Ciepluch 522e5a627f fix(detection-core): keep readiness test on v2 boundary 2026-11-02 10:49:38 +01:00
Marius Ciepluch 362e9548c4 feat(detection-core): add benchmark readiness adapter 2026-11-02 10:40:04 +01:00
Marius Ciepluch a6990e25af feat(detection-core): freeze v1 migration foundation 2026-11-02 10:23:58 +01:00
Marius Ciepluch 1b9018644e docs(detection-core): approve deletion-backed migration plan 2026-11-02 09:38:45 +01:00
Marius Ciepluch 7c0745247e feat(access-control-prover): integrate reviewed entry facts 2026-11-01 18:48:25 +01:00
Marius Ciepluch 2140f5a3ab test(detection-core): accept downstream fact handoff 2026-11-01 18:32:24 +01:00
Marius Ciepluch 29030b38fc feat(access-control-prover): publish fact conformance handoff 2026-11-01 18:16:07 +01:00
Marius Ciepluch ec6c8f7f3e feat(access-control-prover): match reviewed entry facts 2026-11-01 17:42:05 +01:00
Marius Ciepluch 9b61e492e6 feat(access-control-prover): persist canonical fact matches 2026-11-01 17:24:50 +01:00
Marius Ciepluch f739b39ead feat(access-control-prover): migrate reviewed entry packs 2026-11-01 17:17:05 +01:00
Marius Ciepluch 56fb129b7c feat(access-control-prover): record authority review ceremony 2026-11-01 17:01:44 +01:00
Marius Ciepluch a5ebc69a6e feat(access-control-prover): decode installed fact authority 2026-11-01 16:56:26 +01:00
Marius Ciepluch a2ffbacd05 docs(access-control-prover): approve installed authority ceremony 2026-11-01 16:45:39 +01:00
Marius Ciepluch 83e3088bf5 docs(access-control-prover): block self-authorized migration 2026-11-01 15:19:11 +01:00
Marius Ciepluch 18dc25b718 feat(access-control-prover): authenticate fact authority 2026-11-01 15:09:28 +01:00
Marius Ciepluch fd3d8ac1e6 feat(access-control-prover): define production family contract 2026-11-01 15:02:04 +01:00
Marius Ciepluch 30df6b36ba docs(access-control-prover): locate family contract boundary 2026-11-01 14:58:53 +01:00
Marius Ciepluch 8991f44ee3 docs(access-control-prover): own reviewed fact authority 2026-11-01 14:58:22 +01:00
Marius Ciepluch 24724e6da4 docs(detection-core): block unauthenticated fact-pack migration 2026-11-01 12:43:43 +01:00
Marius Ciepluch 5379b8ae39 feat(detection-core): adapt demonstrated proof evidence 2026-11-01 12:38:49 +01:00
Marius Ciepluch 64e44341ee feat(detection-core): bound model execution handoff 2026-11-01 12:18:30 +01:00
Marius Ciepluch 292d4b5557 feat(detection-core): adapt authorization traces 2026-11-01 12:13:20 +01:00
Marius Ciepluch 57802dac7a feat(detection-core): authorize fitted components 2026-11-01 12:03:51 +01:00
Marius Ciepluch e351aca041 feat(detection-core): replay admission evidence 2026-11-01 11:34:23 +01:00
Marius Ciepluch 6a1bc2023a docs(detection-core): add authoritative admission replay 2026-11-01 11:16:38 +01:00
Marius Ciepluch c8ce1c9e63 feat(detection-core): derive authenticated admission metrics 2026-11-01 10:37:37 +01:00
Marius Ciepluch 44516cf29f feat(detection-core): authenticate post-spend case release 2026-11-01 10:18:02 +01:00
Marius Ciepluch 007dd7aa77 docs(detection-core): authenticate post-spend case release 2026-11-01 10:03:58 +01:00
Marius Ciepluch b28045810e feat(detection-core): validate pre-label population spend 2026-11-01 09:20:02 +01:00
Marius Ciepluch 67b3ae62a9 feat(detection-core): validate threshold feasibility 2026-11-01 09:03:44 +01:00
Marius Ciepluch f3adbfe333 feat(detection-core): validate exact admission context 2026-10-31 19:38:01 +01:00
Marius Ciepluch 361c216402 feat(detection-core): validate reviewed admission payloads 2026-10-31 19:08:14 +01:00
Marius Ciepluch a3cea1096c feat(detection-core): add complete measurement records 2026-10-31 18:45:19 +01:00
Marius Ciepluch 9d266b2f11 feat(detection-core): define fixed measurement algebra 2026-10-31 18:26:24 +01:00
Marius Ciepluch 9488a57a1a feat(detection-core): integrate bounded consumer edge 2026-10-31 18:09:35 +01:00
Marius Ciepluch d4fcde615b feat(detection-core): validate differential proof attachment 2026-10-31 17:52:25 +01:00
Marius Ciepluch 2a44ad938c feat(detection-core): derive bounded verdict outputs 2026-10-31 17:38:30 +01:00
Marius Ciepluch a2788d11b0 feat(detection-core): replay model derivation in validation 2026-10-31 17:30:40 +01:00
Marius Ciepluch 8e1ebd4700 feat(detection-core): add closed evidence validation context 2026-10-31 17:23:38 +01:00
Marius Ciepluch d7785ff5d7 feat(detection-core): add semantic shortcut probes 2026-10-31 17:11:07 +01:00
Marius Ciepluch a1800c0c74 feat(detection-core): apply bounded model corroboration 2026-10-31 16:57:11 +01:00
Marius Ciepluch d28998b262 feat(detection-core): preserve model handoff records 2026-10-31 16:49:20 +01:00
Marius Ciepluch 2796647259 feat(detection-core): replay fact-derived relations 2026-10-31 16:33:53 +01:00
Marius Ciepluch 117b5b4bf7 feat(detection-core): apply reviewed fact effects 2026-10-31 16:08:05 +01:00
Marius Ciepluch 087dcc723f feat(detection-core): validate structural matches and matcher lineage 2026-10-31 15:44:48 +01:00
Marius Ciepluch 08b0eeaac2 feat(detection-core): validate reviewed fact packs 2026-10-31 15:28:14 +01:00
Marius Ciepluch 47ecada02d feat(detection-core): stop promotion on stale readiness 2026-10-31 15:11:01 +01:00
Marius Ciepluch f74069a182 feat(detection-core): record ordered readiness evidence 2026-10-31 15:05:37 +01:00
Marius Ciepluch 3e4951fd30 feat(detection-core): derive measured budgets and enforce scope 2026-10-31 15:00:39 +01:00
Marius Ciepluch 85e2a62ed6 feat(detection-core): account for instrument-safe coverage 2026-10-31 14:46:32 +01:00
Marius Ciepluch 4be22046a9 feat(detection-core): add unlike reference family conformance 2026-10-31 14:39:16 +01:00
Marius Ciepluch 741338f207 feat(detection-core): build exact family catalog resolution 2026-10-31 14:32:37 +01:00
Marius Ciepluch 93404c5e1e feat(detection-core): validate closed family declarations 2026-10-31 14:27:18 +01:00
Marius Ciepluch 93aade237e feat(detection-core): bind evidence packets to neutral decisions 2026-10-31 14:18:51 +01:00
Marius Ciepluch 62c87b39ab feat(detection-core): bind lineage to exact influenced artifacts 2026-10-31 14:13:39 +01:00
Marius Ciepluch edd8a7ec87 feat(detection-core): enforce append-only population custody 2026-10-31 14:08:35 +01:00
Marius Ciepluch 7b9c01319b feat(detection-core): validate complete input provenance 2026-10-31 14:00:47 +01:00
Marius Ciepluch 0d606031e6 feat(detection-core): represent family-neutral semantic evidence 2026-10-31 13:55:12 +01:00
Marius Ciepluch 931697ea37 feat(detection-core): build the strict canonical artifact boundary 2026-10-31 13:39:51 +01:00
Marius Ciepluch 1eb9ef11b5 docs(detection-core): approve v2 implementation plan 2026-10-31 12:35:42 +01:00
Marius Ciepluch 2c594b2c27 docs(benchmarks): 8.2 final gated round - dev funnel 11.9->19.2 complete, admission NO-GO #3, arm parked
Lever: unique-declaration member-call binding (dc28325). Measurement 1: zero movement
on in-family sites (0 unique-name member calls exist there; unresolved mass is framework
callee names). Screen: all 264 admission records byte-identical at dc28325. Measurement 2
(dev pool, single-lever isolation): complete-share 11.9->19.2 (+360478, exact conservation),
recall/location/FA identical, 5 ordinary none->indeterminate. Measurement 3 (ac-admission-3,
fresh freeze, criteria byte-identical): NO-GO #3, same four gates, 0/12 pairs, 264/264
fresh dc28325-stamped records. Stop condition fired: access-control arm parked permanently
(rule-11 fallback). Remaining gap: entry recognition (4/12 zero-recognition; coverage
8/12) and framework operation facts, not call resolution.
2026-10-31 09:02:59 +01:00
Marius Ciepluch dcdf04bfbc docs(benchmarks): express-pack admission provenance note for the 17 in-flight records 2026-10-31 08:48:25 +01:00
Marius Ciepluch dc2832564a feat(detection-core): unique-declaration member-call binding (8.2 final gated round step 1-2)
x.method() with a variable receiver binds complete iff exactly one same-name
method declaration exists repository-wide; collisions stay a finite candidate
set (receiver_type_unknown) and module-level functions never bind. Authorized
by maintainer strategy-review 2026-10-31; gated by dev-pool FA non-regression.
2026-10-31 07:10:36 +01:00
Marius Ciepluch 1481bb33d7 docs(steering): record maintainer strategy-review outcome - one final gated resolution round 2026-10-31 06:50:46 +01:00
Marius Ciepluch 62c8d21667 docs(benchmarks): 8.2 admission gate - recognition doubled, NO-GO #2, stop condition fired 2026-10-30 20:55:38 +01:00
Marius Ciepluch 6024152c48 fix(benchmarks): fact_manifest_ref digests entry-pack JSON symmetrically with disk 2026-10-30 20:45:39 +01:00
Marius Ciepluch a8a28f81f3 feat(detection-core): JS handler extraction for bind(this)/arrow/class-method routes (8.2 part 3); fold in load_pack_file repairing 51971cd import 2026-10-30 20:10:22 +01:00
Marius Ciepluch 51971cda7e feat(detection-core): package-rooted entry packs wired into JS recognition; fact digest covers packs (8.2 part 2) 2026-10-30 19:25:30 +01:00
Marius Ciepluch 1d5a509424 feat(detection-core): recognition target table and first express fact pack (8.2 part 1) 2026-10-30 18:46:19 +01:00
Marius Ciepluch 4a1167dfdd feat(detection-core): 8.1 prover native conformance to core trace/coverage contracts 2026-10-30 18:37:04 +01:00
Marius Ciepluch b5c26f52e8 docs(detection-core): bind integration phase into roadmap and task plan 2026-10-30 18:00:59 +01:00
Marius Ciepluch 1cd6a39aaf docs(detection-core): mark all task groups complete 2026-10-30 17:12:43 +01:00
Marius Ciepluch af400c6be4 feat(detection-core): runner migration onto core contracts, schema validation, E2E 2026-10-30 17:12:33 +01:00
Marius Ciepluch c924a69514 feat(detection-core): fact-pack admission and push-consumer contracts 2026-10-30 17:05:24 +01:00
Marius Ciepluch 43fd59f960 feat(detection-core): anti-shortcut probes and fitted-component admission gate 2026-10-30 16:27:27 +01:00
Marius Ciepluch 46b4579d01 feat(detection-core): generalized admission contract with family-scope reports 2026-10-30 16:22:40 +01:00
Marius Ciepluch eb9fe856f9 feat(detection-core): prover trace/coverage conformance adapters to the core contract 2026-10-30 16:18:22 +01:00
Marius Ciepluch 92fb372498 feat(detection-core): measured per-class budget replacing the static 120s gate 2026-10-30 16:11:37 +01:00
Marius Ciepluch 0b56ad5514 feat(detection-core): foundation contract package (trace, verdict, coverage, provenance) 2026-10-30 16:04:10 +01:00
Marius Ciepluch 839d58e3d3 docs(steering): detection-quality rules and stop conditions after admission NO-GO 2026-10-30 14:48:40 +01:00
Marius Ciepluch 1edc4790af feat(access-control-prover): 13.4 admission measurement 2026-10-30 13:25:34 +01:00
Marius Ciepluch d063167b28 feat(access-control-prover): 13.3 four-arm development regression 2026-10-30 12:37:19 +01:00
Marius Ciepluch 9c95f842f7 feat(access-control-prover): 12.6 paired scoring and admission report 2026-10-30 10:25:37 +01:00
Marius Ciepluch 494c29182d feat(access-control-prover): 12.5 admission manifest consumption 2026-10-30 10:13:09 +01:00
Marius Ciepluch ea33222bf6 feat(access-control-prover): 12.4 four-arm development comparison 2026-10-30 09:44:27 +01:00
Marius Ciepluch 282a085f77 feat(access-control-prover): 12.3 instrument and hard runtime gates 2026-10-30 08:53:05 +01:00
Marius Ciepluch eb30d0386c feat(access-control-prover): 12.2 record provenance, stage funnel and judgment transitions 2026-10-30 08:38:58 +01:00
Marius Ciepluch 733e17ed3e feat(access-control-prover): 12.1 analyzer bundles 2026-10-30 08:38:09 +01:00
Marius Ciepluch 9e0a901ccc feat(access-control-prover): 13.2 cross-component validation scenarios 2026-10-30 08:19:25 +01:00
Marius Ciepluch f8e134a159 feat(access-control-prover): 13.1 abstract-domain and metamorphic law validation 2026-10-30 08:09:02 +01:00
Marius Ciepluch 85aa3b1484 feat(access-control-prover): 11.7 first label-blind development acquisition 2026-10-29 21:04:51 +01:00
Marius Ciepluch acadf04109 feat(access-control-prover): 11.6 migrate the stapler recogniser through the registry 2026-10-29 20:59:37 +01:00
Marius Ciepluch 3f241bc595 feat(access-control-prover): 11.5 acquisition provenance and label-isolated mode 2026-10-29 20:49:39 +01:00
Marius Ciepluch 09c3ffd621 feat(access-control-prover): 11.4 verify and promote proposed target knowledge 2026-10-29 20:34:52 +01:00
Marius Ciepluch c0e1360149 feat(access-control-prover): 11.3 extract existing target knowledge into candidate records 2026-10-29 20:29:15 +01:00
Marius Ciepluch c279513def feat(access-control-prover): 11.2 generic call-site registration strategy 2026-10-29 20:23:50 +01:00
Marius Ciepluch 86709daf2e feat(access-control-prover): 11.1 entry-recognition strategy registry 2026-10-29 20:19:31 +01:00
Marius Ciepluch 82e4a1ad97 feat(access-control-prover): 10.6 three-way judgments in the prover 2026-10-29 19:34:44 +01:00
Marius Ciepluch 95dfddb018 feat(access-control-prover): 10.5 multi-path aggregation in the opt-in abstract mode 2026-10-29 19:20:24 +01:00
Marius Ciepluch f700b99637 feat(access-control-prover): 10.4 one judgment per path 2026-10-29 19:17:37 +01:00
Marius Ciepluch 7eb491dc82 feat(access-control-prover): 10.3 authority matching through promoted implications 2026-10-29 19:11:30 +01:00
Marius Ciepluch cf60e02804 feat(access-control-prover): 10.2 witness kind, scope and resource matching 2026-10-29 19:05:51 +01:00
Marius Ciepluch 8ad304ffeb feat(access-control-prover): 10.1 abstract dominance relations without prover fallback 2026-10-29 19:01:25 +01:00
Marius Ciepluch f23d3f986d feat(access-control-prover): 9.4 exact evidence paths from summaries 2026-10-29 18:46:41 +01:00
Marius Ciepluch 06d5363ce2 feat(access-control-prover): 9.3 summary caches and explicit exploration cutoffs 2026-10-29 18:37:06 +01:00
Marius Ciepluch c237626f7a feat(access-control-prover): 9.2 monotone summaries over recursive call components 2026-10-29 18:30:18 +01:00
Marius Ciepluch fffa3e0002 feat(access-control-prover): 9.1 local function summaries 2026-10-29 18:25:23 +01:00
Marius Ciepluch 2466670ba3 feat(access-control-prover): 8.4 inheritance and base-declaration attachment through repo-defined descendants 2026-10-29 16:10:44 +01:00
Marius Ciepluch 8ef2629e0b feat(access-control-prover): 8.3 exact, finite-candidate and unresolved call targets 2026-10-29 16:04:51 +01:00
Marius Ciepluch 6f2154fae0 feat(access-control-prover): 8.2 stable declaration identities and per-change symbol index 2026-10-29 15:45:29 +01:00
Marius Ciepluch 9eee4d7f35 feat(access-control-prover): 8.1 capture call receiver and arity evidence in the semantic IR 2026-10-29 15:33:18 +01:00
Marius Ciepluch 03807f4ddd feat(access-control-prover): task 7.4 - judgment, trace and coverage contracts with abstract state 2026-10-29 15:23:43 +01:00
Marius Ciepluch 2f97fb2693 feat(access-control-prover): 7.3 authority abstract values and promoted implication facts 2026-10-29 15:06:53 +01:00
Marius Ciepluch 351583a04f feat(access-control-prover): 7.2 resource abstract values and operation-fact fields 2026-10-29 14:48:53 +01:00
Marius Ciepluch 5697a7fe9e feat(access-control-prover): 7.1 evidence-relation algebra and completeness value 2026-10-29 14:11:13 +01:00
Marius Ciepluch 0d2b4cf15d docs(spec) access-control-prover: abstract-interpretation amendment -- evidence algebra, stable symbols, label-blind acquisition, admission discipline 2026-10-29 13:31:40 +01:00
Marius Ciepluch 152f405bcf docs(spec,steering): lift access-control-prover to capability level; engine/target split, pattern discipline, complexity caps, breadth map 2026-10-29 12:27:44 +01:00
Marius Ciepluch f09dc66592 docs(spec) access-control-prover: design amendment -- resolution, resource-bound guards, programmatic registration 2026-10-29 12:10:37 +01:00
Marius Ciepluch 3de96c88f6 docs(steering): terminology rule -- name the behavior, not the virtue 2026-10-29 11:57:51 +01:00
Marius Ciepluch bc896d2779 docs(spec) access-control-prover: post-measurement requirements amendment 2026-10-29 11:57:15 +01:00
Marius Ciepluch 37b0a593a7 docs(roadmap): gate 5 decision -- fix trace resolution first, injection spec deferred 2026-10-28 20:12:37 +01:00
Marius Ciepluch efdb66bc73 measure(ablation) gate 3: miss and false-alarm trace-component taxonomy 2026-10-28 20:06:40 +01:00
Marius Ciepluch 0b1d41d01b measure(judge) gate 2: zero-shot judge over prover findings 2026-10-28 19:43:59 +01:00
Marius Ciepluch b247da9e82 chore: add copilot instructions 2026-10-28 18:59:01 +01:00
Marius Ciepluch a60e9bda9d measure(search): 2026-10-07 shiftleft sql-vuln pilot records 2026-10-28 18:59:01 +01:00
Marius Ciepluch fcf05f7755 bench(unseen): p1 pool, draft and bisect freeze artifacts 2026-10-28 18:59:01 +01:00
Marius Ciepluch c5341d79a2 measure(prover): gate 1 on the 23-repo training pool: recall 65.2% (Wilson 44.9-81.2), 15.0 FA/100, 0 instrument failures; 4/5 on the hunt overlap incl. trigger.dev 2026-10-28 18:59:01 +01:00
Marius CiepluchandClaude Fable 5.1 03d925fffa spec/steering: the decision strategy corrected: software features do not decide safety; the trace does, the pool measures and never trains
Maintainer's finding of 2026-10-14 recorded in hunter-learning-loop (a dated section that overrides the shape
annotations as the model's input), its design (amendment pointing to the prover's trace) and the steering
overview's current state.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 19:40:12 +02:00
Marius CiepluchandClaude Fable 5.1 03b114cbeb feat(prover): delivery D: control flow decides dominance; the trace carries each guard's relation, effect and the operation's control context
Opus agent delivery under spec access-control-prover, requirement 10. `semantic/cfg.py` builds a control-flow
graph per function from the syntax tree (if/else, switch arms, loops, try/catch/finally, return, throw, break,
continue, exit, assert) with dominators and a per-line control context, and leaves a function without a graph
rather than a wrong one. `obligations/cfg_dominance.py::CfgDominance` discharges a statement guard only when it
lies on every path to the operation and the operation is unreachable from its failing arm; a predicate whose
result nothing consumes is `value_only` and never discharges; router, decorator and earlier-hop witnesses behave
as before; without a graph the order rule applies and `cfg_unavailable` is recorded. The trace's guards carry
`relation` and `effect`, the trace carries `operation_context`; Java parameter lists carry names only; a
handler-named operation's arguments are the handler's parameters. The checker's default stays order-based.

Smoke on the 23 fix changes: 15 caught, 55 of 93 locations, precision 15 of 15, no failure, no change over
120 s (unchanged from delivery B). Gate: 3575 tests pass; the reserved-repository guard passes on the export.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 19:01:07 +02:00
Marius CiepluchandClaude Fable 5.1 6c3d4a7d72 spec(access-control-prover): a guard whose passing branch alone holds the operation discharges it (effect branches)
Raised by the implementing agent: the design named only exits on the failing arm; if (can(x)) { op(); } is the
common shape and the operation is unreachable from the failing arm there too.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 18:28:10 +02:00
Marius CiepluchandClaude Fable 5.1 2f628b921b feat(prover): delivery C: the ordered trace with argument provenance for every examined pair
Codex delivery under spec access-control-prover, requirement 9. `semantic/obligations/trace.py` serialises each
(entry, operation) pair the prover examined, discharged or unguarded: the entry with its boundary, access and
parameters; each hop in order with its call line, the guards before the call and the values forwarded; the
operation with the provenance of each argument traced through the function's binds; the guards after it; the
verdict. One canonical text form and one JSON form, no language, framework, repository, size or count fields.
The prover result, the arm record and each unguarded finding carry the traces. On the ec2-plugin fix: 32
traces, 4 unguarded. Gate: 3534 tests pass; the reserved-repository guard passes on the tracked export.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 18:22:45 +02:00
Marius CiepluchandClaude Fable 5.1 52ecbb76ab spec(access-control-prover): control flow decides dominance; the trace carries each guard's relation, effect and the operation's control context
Requirement 10, the structural control-flow graph per function and CfgDominance behind the existing protocol,
tasks 4.4 and 4.5. Textual order stays the checker's default; the prover selects control flow.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 18:07:09 +02:00
Marius CiepluchandClaude Fable 5.1 12d09faeed spec(access-control-prover): the ordered trace with argument provenance is the prover's output for every examined pair
The maintainer's correction: counts over a function, language or framework carry nothing about safety; the
object a decision model may see is the path from an attacker-reachable entry to a protected operation, with the
checks on it in control-flow order and the provenance of the operation's arguments. Requirement 9, the trace
contract and tasks 4.2 and 4.3.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 17:56:50 +02:00
Marius CiepluchandClaude Fable 5.1 17e07b36c4 feat(prover): delivery B: handler-named operations, Symfony, Livewire, Remix and Tornado entries, enforcement witnesses, memoised path walk
Codex delivery under spec access-control-prover plus one fix of mine. Operation rows may now name a handler by
name pattern and framework (Jenkins form validation and list filling, Tornado request methods, Remix actions);
the mapper recognises Symfony, Livewire and Laravel controllers, Remix exports and Tornado handlers;
`obligations/enforcement.py` turns Laravel route middleware, Symfony attributes and security.yaml rules and
Tornado's authenticated decorator into router and decorator witnesses. The path walk looks each node's callers
up once per change and does not walk a tree with no recognised entry: one seatunnel change fell from 670 s to
35 s. One name Codex could not source (Tornado get_current_user) is left out and said so in the facts header.

Smoke on the 23 fix changes: 15 pushes caught (65 percent, Wilson 45 to 81), 55 of 93 locations, precision
15 of 15 flagged, no instrument failure, no change over 120 s. Delivery A caught 7.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 17:31:07 +02:00
Marius CiepluchandClaude Fable 5.1 7f1b820d5a style(prover-arm): format
The previous commit was gated through a pipe whose exit status was tail's, not ruff's; CI caught the
unformatted file.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 17:03:35 +02:00
Marius CiepluchandClaude Fable 5.1 5980bf8377 fix(prover-arm): the byte table holds only files that were read, so a zero-byte read stays on the record and the summary refuses it
CI failed test_unchanged_file_empty_read_refuses: the previous fix skipped failed records in the zero-byte rule.
Now the arm records a file's size only after reading it (a git-empty file is counted under empty_files and
not listed), a real zero-byte read keeps its zero and the summary refuses it on every record as the test says.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 17:00:13 +02:00
Marius CiepluchandClaude Fable 5.1 beb272a5ec feat(prover): access-control prover, delivery A: path records, Stapler and Jetty entries, Java operation rows, the prover and its scoring arm
Codex delivery under spec access-control-prover. `semantic/obligations/paths.py` builds entry-to-function path
records from the cached caller index; `mapping.py` names Stapler web methods and Jetty handlers as entries;
`check_obligations` gains the opt-in premise `assume_entries_protected` with the label `unguarded_path`;
`hunt/prover.py` proves a change with no model call and states its coverage; `benchmarks/valid/prover_arm.py`
scores it with the valid-benchmark metrics. Two fixes after the first smoke run: a product file git holds empty
is read, not unread (four changes failed on `__init__.py` and empty assets), and absent-input reasons are
counted by name.

Smoke on the 23 fix changes of the training pool: 7 pushes caught (30 percent, Wilson 16 to 51), 28 of 93
locations, precision 7 of 8 flagged, no instrument failure; three changes over the 120 second bound. Twelve of
the 17 Jenkins fixes reach the fixed method but check no operation because the method is the operation;
the spec now carries handler-named operations for delivery B.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 16:50:04 +02:00
Marius CiepluchandClaude Fable 5.1 e514551cb6 spec(access-control-prover): the checker gains an opt-in premise and the unguarded_path label
Codex stopped on a real conflict: with path records and no clause or sibling norm the checker returns no finding
by design. The prover's premise (a recognised entry is protected unless declared public) is now an opt-in keyword
with its own label and evidence, default off, existing behaviour and tests unchanged.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 15:54:29 +02:00
Marius CiepluchandClaude Fable 5.1 74a529825d spec: access-control prover (requirements, gap analysis, design, tasks)
A deterministic check that a protected operation is reachable from an entry point without a check dominating
the path, per framework, built on the existing obligations checker, caller index and entry mapper; scored as a
rule arm on the access-control training pool. Follows the maintainer's finding that the hunter's features
describe shape and cannot decide unsafety.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 15:51:25 +02:00
Marius CiepluchandClaude Fable 5.1 9feb8f1e33 fix(hunt): a row's entry points are the ones in its file or a caller's file, not the whole repository's list
The DSPy arm over the second pool failed with LMInvalidRequestError on every change with more than two rows, at no
cost: `source.facts_for` attached every entry point of the product tree to every row, 8190 records and 1.5 MB per
row on apache/seatunnel (the first pool's arm lost 11 changes the same way on trigger.dev and shopper), and the
rendered prompt was refused. The screen table of the second pool weighed 814 MB for the same reason.

A row now carries the entry points declared in its own file or in a file that calls the function, which is what the
input has described all along ("in-repository entry points that reach the file"). `source.entry_point_count`
follows that definition.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 14:47:08 +02:00
Marius CiepluchandClaude Fable 5.1 81ae206978 fix(hunt): build output gets no candidate rows, a minified line is not scanned once per function, the screen checkpoints every change
The second access-control screen spun for 2h20m on one ordinary mage-ai change: a Next.js build commit whose 3 MB
one-line bundles hold 12,355 functions each. `source.ranges_from_ir` ran two regex scans of the whole line once per
function, and the screen kept its 422 finished changes in memory only, so stopping it lost them.

- `source.ranges_from_ir`: a line over MINIFIED_LINE_CHARS (2000) is not checked per function; JavaScript names on it
  stay anonymous, other languages keep the parser's name.
- `source.generated_reason`: why a changed file is build output (a directory the product walk leaves out, a minified
  name, over MAX_FILE_BYTES, a line over the cap). `hunt.producers._candidates` leaves such files out and records the
  reason on the context; the fix site of a vulnerability is always kept. The DSPy arm shares the enumeration.
- The screen writes rows.jsonl and log.jsonl per change (rows first, entry last) and resumes by skipping logged ids.

Tests: the minified line never reaches the per-function scan; generated_reason on the four shapes and two source files;
build output in an ordinary change yields no rows while a vulnerability path in a one-line file is kept.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 13:24:45 +02:00
Marius CiepluchandClaude Fable 5.1 4cf9c85d22 perf(hunt): the caller search prepares each product file once per change
call_sites rescanned every line of every product file for declarations on each call; with one name per
candidate row that was the whole cost of the source producer. code_index prepares a file once (the names each
line declares), call_sites takes it, and callers_of keeps a cache of prepared files the producer context owns.
Same calls, tested against the unprepared scan. One Laravel change of 33 rows: 249 seconds before this pair of
commits, 20 now.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 09:46:46 +02:00
Marius CiepluchandClaude Fable 5.1 86d70831c8 perf(hunt): the source producer walks and reads the product tree once per change, not once per row
facts_for walked the whole product tree and re-read every product file for each candidate row; on a Laravel
application one change of 33 rows took four minutes, 96 percent of it there. The producer context already
holds the product file list and the texts it read for entry points, so facts_for and callers_of now accept
them. Same facts, tested by a call whose tree walk raises and whose file on disk is emptied after reading.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 09:35:16 +02:00
Marius CiepluchandClaude Fable 5.1 4063fd1562 feat(semantic): the hunter's semantic producer covers Java and PHP
Java parsed already, but the producer failed every Java row because no obligation facts existed for the
language and that failure took roles and def_use down with it. PHP had no grammar. Now: tree-sitter-php in the
semantic extra with PHP node kinds in the CST reader; obligation facts for Jenkins/Stapler, Spring Security and
Servlet code (java.toml) and for Laravel, WordPress and Symfony (php.toml), each header naming the training
clones and references the names were checked against; a language with an IR but no obligation facts keeps
roles and def_use and records obligations as unchecked (None), which the engineered features treat as missing,
never as zero. Proven on the committed pool: 6 of 6 Java rows and 33 of 33 PHP rows of one fix change each now
carry semantic features. Implemented by Codex; gated here with ruff, mypy and the full suite.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 09:35:16 +02:00
Marius CiepluchandClaude Fable 5.1 6fe46c3062 data: access-control training pool refrozen (e6a67487): 23 fix changes, 93 accepted locations, 23 repositories, 20 ordinary pushes per fix, floor 20, walk cap 1500; 11 repositories left out for short history
Batch 1 and the spot-checked batch 2 combined. The walk and freeze record are in the training work directory.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 09:31:56 +02:00
Marius CiepluchandClaude Fable 5.1 30ad197d3f fix(bench): an unparsable file in one ordinary commit no longer ends a repository's history walk
The combined access-control freeze stopped at its 31st repository: one ordinary commit of a PHP tree touched a
Python file that does not parse, functions() raised Rejected, and ordinary_history let it end the walk. The
file is now marked parse_failed on its row and such a commit is rejected as an ordinary push (reason
function_parse), which keeps the draw conservative without losing the repository.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 08:42:25 +02:00
Marius CiepluchandClaude Fable 5.1 351d73079a ci: the host job keeps the hunt and semantic extras; a benchmark main no longer leaks GIT_NO_LAZY_FETCH into later tests
The workflow runs pytest twice: in the dagger container (now one failure) and on the runner host, whose job
synced the dev group only, so dspy and tree-sitter were missing there (25 failures). The one failure shared by
both runs was the partial-clone extraction test: engine_trace.main sets GIT_NO_LAZY_FETCH in the process
environment, the variable outlived its tests, and git 2.44 and newer refuse the lazy fetch with "lazy fetching
disabled". The local git ignores the variable, so the leak was invisible here; the test module now restores
the variable after each test (leak checked with a session-end probe: present before, absent after).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 08:08:32 +02:00
Marius CiepluchandClaude Fable 5.1 17194b3df9 ci: pytest runs with the hunt and semantic extras kept; sandbox tests skip by cause where no bwrap exists
CI's first full pytest run since 2836b78 showed 65 failures in three classes. uv run re-synced the environment
to the lockfile's default extras before pytest, removing dspy again (UV_NO_SYNC=1 now). The learn slice, pair
corpus and hunt producer tests need tree-sitter (semantic extra installed). Thirty-two tests need bubblewrap
isolation, which CI's container cannot provide: they carry a sandbox marker and skip where the isolation
preflight fails, with the cause's type in the reason; proven locally by hiding bwrap (7 skipped, 23 passed).
The partial-clone extraction test now probes one lazy fetch through git itself first, so git's own stderr
reaches the next CI log instead of git_exit_128.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 07:54:09 +02:00
Marius CiepluchandClaude Fable 5.1 b35137ed1b ci: install the hunt extra so pytest exercises the hunter tests
CI's pytest stage had not run since 2836b78 (ruff and mypy failed first). Once it ran, the hunt tests reached
dspy at run time and CI never installs the optional extra. Installing it mirrors the local environment the
suite and mypy are gated in.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 07:36:16 +02:00
Marius CiepluchandClaude Fable 5.1 436e83de5e data: access-control batch 2 spot-checked; all 69 passing locations accepted (27 changes, reviewer mc)
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 07:36:16 +02:00
Marius CiepluchandClaude Fable 5.1 ae837a6aa4 fix(types): numpy, pydantic and litellm are absent in CI's type-check environment
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 03:05:12 +02:00
Marius CiepluchandClaude Fable 5.1 c8f7610313 fix(types): mypy clean again over src; stub-less libraries and the benchmark imports handled in pyproject
66 errors had accumulated in src since the last green run (the hunt modules of groups 4-6, stats, candidates,
runner). Codex typed them without behaviour change; the 17 inline import-untyped ignores it added are replaced by
one override for dspy, pandas, statsmodels and scikit-learn, and the hunt package's lazy imports of benchmarks/
(never type-checked, not in files) are followed silently so src stays strict against their signatures.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 03:03:12 +02:00
Marius CiepluchandClaude Fable 5.1 0d7d3f0dc9 chore: format the whole tree and lint the scratch measurement scripts; CI has been red since 25ae4b4
CI runs ruff format --check and ruff check over the whole repository; 29 files had drifted from the formatter and
four measurement scripts carried lint findings, so every push since 25ae4b4 failed before pytest ran.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 02:49:44 +02:00
Marius CiepluchandClaude Fable 5.1 fbbc03f3ea measure: access-control feature table and separation screen (952 rows, 74 positive rows, 5 repositories); first ablation manifest registered
Three builds: the first two read 150 rows as empty (a producer bug, fcb43fd), the third is clean. The diff
producer was blind to removed lines (537b61e). Screen: deletion_count 0.89 is the reversed-fix construction;
on rows whose file lost lines entry_point_count 0.83 and caller_count 0.73 are the class signal; semantic covers
262 rows (TypeScript only). Repository folds assigned, leak audit recorded, manifest ac-ablation-1 registered
for decision (five arms, about 35 dollars).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 02:45:35 +02:00
Marius CiepluchandClaude Fable 5.1 dee23c23f5 measure: both arms over the access-control training pool (2 of 5 pushes, 4 of 48 locations each); runner books real cost
DSPy baseline: 2/5 pushes, 4/48 locations, 14 of 90 ordinary pushes flagged, 25 suspicions (3 true), 11 invalid-request
failures in shopper and trigger.dev, $6.13. Pilot hunter: 2/5 pushes, 4/48 locations, 99 of 100 ordinary pushes
flagged, 594 findings (3 true). Different pushes caught; nothing in trigger.dev.

The runner booked usd 0.0 per change so --budget-usd never stopped a run; it now prices every LM call of the change
at the registered prices and writes usd into the record.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 01:14:46 +02:00
Marius CiepluchandClaude Fable 5.1 fcb43fdd47 fix(hunt): an empty product file no longer fails the source producer on every row of a change
The 150 unread rows were not a checkout failure: the entry-point scan read every product file and one empty
file raised empty_source, which escaped the cached property and failed the source producer on every row of
the change (and re-read the whole tree per row). Empty product files are skipped; the row's own file still
has to be non-empty.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-14 00:53:37 +02:00
Marius CiepluchandClaude Fable 5.1 c01e61c4dd fix(bench): the pilot hunter arm undoes an inherited sparse checkout before it reads a hotspot
The training clones are sparse-checkout and a worktree inherits it; without this the hunter arm raises
unreadable_hotspot on every change of the access-control pool. Same helper and checkout proof as the hunt runner.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 23:18:44 +02:00
Marius CiepluchandClaude Fable 5.1 537b61e8d9 fix(hunt): diff producer records where a push removed lines; worktree checkout is proven complete
Two instrument findings from the first access-control feature table (952 rows, 74 positive rows):

1. A fix that adds a guard is, replayed base-to-head, a push that removes it. The diff producer only kept
   hunks with added lines at head, so 38 of 74 positive rows had no hunk in their file at all and 69 of 74
   were "not on a changed line", while 60% of ordinary rows were. The diff annotation separated the labels
   by construction and was blind to exactly the access-control shape. The producer (version 2) now records
   per file the head lines after which the push removed code; distance and on_changed_line count them, the
   model's hunks input renders them as removed_after_line, and diff.deletion_count is an engineered feature.

2. 150 rows (16%, every positive of one repository) read empty files from the worktree although git holds
   the blobs; the build ran while another process checked out from the same clone. _full_checkout now
   proves the checkout: git status must be clean, a dirty tree is restored once, then InstrumentFailure
   checkout_incomplete. The table script cross-checks every unread row against the blob size and refuses
   to screen a partly unread tree, and records the diff-scoped recall ceiling (positive rows a label-blind
   enumeration would reach).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 23:17:15 +02:00
Marius CiepluchandClaude Fable 5.1 60cbefec9b data: access-control batch verified, awaiting spot-check: 57 of 78 cloned (6 GB disk guard), 148 catalog sites, 69 pass all three checks in 27 changes
Java 63 of 69 sites. verify: 148 records, 701 deepseek-flash calls, no provider errors. The 21
uncloned sources are in sources_access_control_dropped.json for a later clone pass.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 22:07:39 +02:00
Marius CiepluchandClaude Fable 5.1 6a9e4093fe data: 78 access-control advisories sourced from the OSV dumps (maven 48, npm 12, pip 11, composer 7), 237 candidate repositories after exclusions
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 21:53:00 +02:00
Marius CiepluchandClaude Fable 5.1 3d08d692ff bench(train): class-targeted sourcing, --family, from the public OSV dumps
sources --family <family> keeps only advisories whose CWE the taxonomy maps to that family, writes
sources_<family>.json beside the training set, and excludes the pilot, every repository the
benchmarks already name, the strict used set and every earlier batch; --prefilter-only runs the
dump pass without GitHub. Implemented by Codex (gpt-6-astra), gated here: names normalize rejects
exclude nothing instead of aborting, and the strict used set is in the exclusions.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 21:46:45 +02:00
Marius CiepluchandClaude Fable 5.1 499f6c4474 fix(hunt): label every verified target of a change; undo an inherited sparse-checkout in the worktree
label_rows honoured only a change's primary site, so 43 of the 48 accepted access-control
locations were unlabelled; every entry of targets is now a positive with its own label_ref and
span. The training clones were emptied with sparse-checkout to save disk and a new worktree
inherits it: every file read failed and the source and semantic producers reported an unread tree
on all 599 rows. The runner now disables sparse-checkout in the worktree it adds.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 21:32:43 +02:00
Marius CiepluchandClaude Fable 5.1 30921a12b9 data: access-control training pool frozen (53f9f364): 5 fix changes, 48 accepted locations, 20 ordinary pushes per fix, floor 20, history limit 1500
mcp-documentation-server (8 ordinary commits) and line-desktop-mcp (13) are left out of this freeze;
their locations stay accepted for a later one.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 21:30:04 +02:00
Marius CiepluchandClaude Fable 5.1 1d1c87f63b bench(valid): a training freeze may cap the history walk at the newest N first-parent commits
ordinary_history(limit=) walks only the newest commits; assemble.freeze(history_limit=) and
--history-limit pass it through and the record carries it. The held-out benchmark passes none.
Reason: trigger.dev's 7,109-commit walk runs for hours at the per-commit cost of the function
and generated checks; the training freeze now walks its newest 1,500.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 20:47:23 +02:00
Marius CiepluchandClaude Fable 5.1 6f9613329e feat(hunt): candidates scored on folds through the runner; the pilot gain, once per accepted program (6.1)
rounds.py runs a candidate over compile or held-out folds through runner.run, so a tool-using
program has its tools bound to each change's checkout, and turns paired records into the
FoldResults accept.decide reads (closes the gap noted in bacd7a1). report.py runs an accepted
program once over the held-out pilot: a ledger line reserved before the run refuses a second run
per program id, contaminated or unchecked training runs are refused, the instrument is proved per
change and more than 5% failures makes the run invalid with no numbers quoted, the pilot's own
scorer and metrics give the after numbers, the recorded hunter numbers give the before, baseline
repeats pair with the single run, the proof arm's workload per 100 pushes is counted, and the gate
distance is read off the confidence curve. 124 hunt and stats tests pass. Implemented by Codex
(gpt-6-astra), gated here.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 20:44:18 +02:00
Marius CiepluchandClaude Fable 5.1 bacd7a1e3f feat(hunt): proposals and the acceptance rule (5.1, 5.2)
propose.py: one change per round (instruction, demonstrations or one mask), examples built from
compile folds only with a provenance record of the change ids used, a registry for GEPA, MIPROv2
and BootstrapFewShot (MIPROv2 without the data-aware proposer so labels are never serialised),
compile-fold scoring with Wilson and the repository-paired bootstrap, and a compile-only history
the next round may read. accept.py: the rule as independent clauses (both folds non-negative with
one strictly positive, per-family regression budget on held-out recall, tie tolerance, no
acceptance without an interval, instruction cap, one change only), the comparison recorded in the
candidate artifact's configurations, rejections logged without held-out numbers. 48 fixture tests.
Known gap for live rounds: the optimisers and propose._score call the program directly, so a
tool-using program must be scored through the runner with its tools bound per checkout (6.1).
Implemented by Codex (gpt-6-astra), gated here.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 20:22:22 +02:00
Marius CiepluchandClaude Fable 5.1 47d0f0fac1 bench(unseen): per-commit history cache for ordinary_history; assemble.freeze --history-cache
One append-only JSONL per clone (named by the sha256 of its top-level path), one line per commit
with the row as the walk built it and HISTORY_CACHE_VERSION, flushed after every commit so a
killed run keeps its progress. A cached row is reused only when its parents match the current log;
other versions and torn lines are recomputed. With no cache directory nothing changes. The
7,400-commit trigger.dev walk that took over an hour per freeze becomes a one-time cost.
Implemented by Codex (gpt-6-astra), gated here.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 19:40:34 +02:00
Marius CiepluchandClaude Fable 5.1 dbaaa86d5b feat(hunt): contamination check (4.5): runs that used a reported repository are marked and kept out of any gain
contamination.py reads the repositories named by the held-out pilot manifest and by every
benchmark report (accepted_repos.json and NOTES.md links under benchmarks/measurements), marks
each run directory clean or contaminated with the report digest and time, and refuses to count
an unchecked run toward a gain. ablation.analyse carries excluded_from_gain when its run is
contaminated. Implemented by Codex (gpt-6-astra), gated here; the benchmark import is lazy so
the package imports without the benchmarks tree.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 19:03:23 +02:00
Marius CiepluchandClaude Fable 5.1 6e5d060de6 feat(hunt): registered paired ablation, repository folds, leak audit, importance table and the keep/drop rule (4.2-4.4)
ablation.py registers the manifest (table digest, arms as masks, folds, metric, sample, budget,
rule, seed, program id) before any run and refuses a run or an analysis that does not match it.
Arms run over the same changes through runner.run, paired by change; effects on recall and on
suspicions per ordinary push are reported pooled and per family with the repository-clustered
bootstrap and exact McNemar, plus the recall-against-suspicions curve at every reported
confidence (never a tuned cut). The importance table and decisions read held-out effects only;
a neutral annotation survives only when a kept one needs it. 30 fixture tests with a scripted
model. Implemented by Codex (gpt-6-astra), gated here.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 18:49:58 +02:00
Marius CiepluchandClaude Fable 5.1 bbbb0f397e bench(valid): a training freeze may lower the ordinary floor to the ratio; the draw takes a size
The held-out benchmark keeps the 40-commit floor and the union of twelve-commit stratified draws.
A training freeze passes floor=20 (never below the ratio) and the record carries ordinary_floor.
When the union of twelve-commit draws cannot reach the count (small cells stay unallocated on
every draw), one stratified draw of the full count is used; with count equal to the eligible
commits that is every one of them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 18:33:10 +02:00
Marius CiepluchandClaude Opus 5.5 f29b3a3df2 data: maintainer spot-check, 51 access control locations accepted (7 changes)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 18:09:10 +02:00
Marius CiepluchandClaude Opus 5.5 caa56387a1 spec(hunter-learning-loop): tick groups 1-3, implemented and measured
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 17:42:02 +02:00
Marius CiepluchandClaude Opus 5.5 2b65680d5d data: hunt training-set candidates (task 4.1), not frozen: 99 verified locations in 28 changes, awaiting spot-check
Sourced apart from the 12 pilot repositories: 212 candidates (pool-fresh, pool-retry, OSV for PyPI,
npm, Maven and Packagist), 49 dropped because the benchmarks already name their repository, 413
catalog locations, 99 passing all three verifier checks in 28 repositories. Short of the 40-positive
target. catalog.py now reads .java and .php. Freeze waits on the maintainer's spot-check
(CANDIDATES.md, SOURCING.md).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 16:48:41 +02:00
Marius CiepluchandClaude Opus 5.5 25ae4b489c fix(p1,p0): restore the pre-P1 range contract for two callers; docs link and maintainer paths
The full suite after the replay found regressions from 062a7b7 and 94e96c6:
- catalog_gen and candidates.labeled_spans still unpacked 3-tuples and expected None on a parser
  rejection; source.ranges returns FunctionRange (4 fields, module entry first) and raises ValueError.
- ranges_from_ir passed the language to declarator_name, so the repo-facts declaration table named a
  callback call (exec(..., function () {) as a function; the pre-P1 call had no language.
- the catalog_gen fail-loud test blocked the old module name.
- docs: a renumbered examples anchor, and two .kiro paths in the public threat model.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 16:15:32 +02:00
Marius CiepluchandClaude Opus 5.5 a99579b547 measure: final push-hook arm on all 252 pilot changes (0/12, 2.1 false alarms per 100) and the combined whole-tool score
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 15:50:29 +02:00
Marius CiepluchandClaude Opus 5.5 ec40dc1c93 measure: loop parse fix recovers 2 of 7 lost hunts; DSPy baseline 20/60 with the fix
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 15:45:56 +02:00
Marius CiepluchandClaude Opus 5.5 455637bea5 fix(hunt): a malformed tool-loop step ends the loop and the answer is still extracted
DSPy 3.4.1 raises AdapterParseError for an unparseable ReAct step, which is not the ValueError its loop
catches, so one malformed step discarded the whole hunt (7 of 60 hunts in the repeats). The step now ends
the loop like an invalid tool choice. Records carry loop_parse_failed beside parse_failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 15:30:13 +02:00
Marius CiepluchandClaude Opus 5.5 e07ab2ac93 measure: 5 paired repeats, DSPy hunt baseline 30% vs pilot hunter 52% recall on the 12 positives
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 15:29:51 +02:00
Marius CiepluchandClaude Opus 5.5 26720ed0dc measure: pre-registered analysis for 5 paired repeats of the hunt baseline against the pilot hunter
Committed before the repeats finish. Unit is (change, repeat); paired bootstrap
over changes and exact McNemar from openultrasast.stats; failures stay in the
denominator. Dry run on the existing single runs reproduces 6/12 and 3/12.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:21:15 +02:00
Marius CiepluchandClaude Opus 5.5 3692c85d76 measure: hunt cache replay verified (zero tokens on rerun)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:16:31 +02:00
Marius CiepluchandClaude Opus 5.5 c2bfcef3df measure: hunt baseline on the 12 pilot positives
3 of 12 with the fixed answer check against the pilot hunter's 6 of 12, one
run each, not significant. The intended zero-cost replay was not a replay:
answers at temperature 0 differ between runs and the cache did not serve the
second run. Programs must be compared over repeated runs, paired by change.
About $1.10 for both runs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:15:22 +02:00
Marius CiepluchandClaude Opus 5.5 602ec23e03 fix(hunt): lines read through a tool count as shown; an unparseable answer is an empty answer
The answer check counted only the rendered function text as shown, so 7 of
8 baseline suspicions citing lines the model had read with read_file were
marked unknown (baseline 1/12 against the pilot hunter's 6/12). Tools now
record the lines they return. A DSPy parse failure scores as zero findings
with a flag instead of a broken run. The per-run response cache now takes
effect through dspy.configure_cache.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:10:40 +02:00
Marius CiepluchandClaude Opus 5.5 c5bf52f47d measure: Semgrep arm on the valid pilot, our rules and facts converted
0 of 12 positives; 12.6 false alarms per 100 ordinary pushes (2.1 on changed
lines only). The misses come from what the rules and facts express (access
control lives in obligations, which do not convert; sources cover request
objects only), not from the engine. Comparison arm only: a Semgrep miss is
never evidence against a hunter finding.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:10:19 +02:00
Marius CiepluchandClaude Opus 5.5 d08dea19b8 spec(hunter-learning-loop): a pattern engine's miss is never evidence
A Semgrep, quick-rule or engine match may enter the feature table as
positive evidence; its absence is never a feature, filter, demotion or
gate on a hunter suspicion, and no acceptance rule counts it. The Semgrep
arm is a comparison only and may at most replace the regex quick tier.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-13 13:01:32 +02:00
Marius CiepluchandClaude Fable 5.1 c95b9241d8 feat(stats): one statistics module on scipy, statsmodels and scikit-learn; engineered features tied to the annotations
openultrasast.stats: Wilson intervals (statsmodels), exact McNemar, a
repository-clustered percentile bootstrap and its paired form with one
quantile convention, grouped folds (GroupKFold, leave-one-group-out), and
per-column separation (ROC AUC) for the leak audit and the signal screen.
Checked against the benchmark scorer's Wilson and the learn harness's
McNemar. hunt/features.py turns the feature table into the engineered
frame (distances, counts, flags) and keeps the map from every engineered
feature to the annotation the model is shown, so an ablation mask drops
both, and screen() says which annotation carries signal before a model
call is spent. The [hunt] extra now carries the libraries; the core
install is unchanged. The learn harness's own copies stay until that line
is revived.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:43:02 +02:00
Marius CiepluchandClaude Fable 5.1 d9d6335ab3 feat(hunt): feature table, producers, labels, metric, artifact, the DSPy hunt program, runner and CLI
hunter-learning-loop groups 1-3 (first increment). The feature table is the
contract: one row per touched function with diff, source, semantic and
taxonomy columns, provenance per producer, labels by the benchmark's rule.
Producers read the repository at head only (a planted advisory id and
commit text never reach a feature). The hunt program is a DSPy ReAct loop
over a signature whose inputs are the unmasked columns in a fixed order;
answers outside the closed family list or citing unseen lines score as
unknown. The runner writes records the benchmark scorer reads, resumable
by change, program id and mask, and refuses the held-out pilot unless the
run is a report run. Research extra [hunt] = dspy, pandas; the core
install is unchanged. 43 tests on a scripted model, no network.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:33:58 +02:00
Marius CiepluchandClaude Fable 5.1 e55e44d981 research: Semgrep CE and Opengrep as the quick tier, measured
Engine LGPL-2.1, shelling out carries no obligation; the registry rules are
under the Semgrep Rules License since 2024-12-13 and cannot ship, only our
own converted rules can. Measured 4.7 s startup and 5 to 10 ms per file;
converted regex and taint rules fire for Python, PHP and JavaScript; C
propagation and Groovy do not; one sanitizer semantic we refuse on purpose
is accepted by Semgrep. Recommendation: a Semgrep arm on the valid pilot
first. Recorded in the hunter-learning-loop research note and breadth.toml.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:21:02 +02:00
Marius CiepluchandClaude Fable 5.1 108dc2e5f3 measure: lizard and tree-sitter-language-pack against our function-span engines
610 pair files, 570 labelled functions. lizard matches the parser engine on
C, Python and Java but loses JavaScript names and TypeScript end lines:
keep ours, take its cyclomatic complexity as a feature candidate.
tree-sitter-language-pack gives identical spans, is faster, replaces six
grammar wheels with one and adds Go, Ruby, PHP and Perl: adopt as grammar
packaging. Also found: .c files should try the c grammar before cpp.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:18:57 +02:00
Marius CiepluchandClaude Fable 5.1 5d1d6f61d0 spec(hunter-learning-loop): implementation plan
Six groups: table and producers; the hunter as a program with the search
metric and the run artifact; runner integration and a baseline on the 12
pilot positives; training set, folds, paired ablation and the importance
table; proposals and the acceptance rule; the pilot gain report.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:15:45 +02:00
Marius CiepluchandClaude Fable 5.1 dc2da3d83a spec(hunter-learning-loop): gap analysis and design
Feature engineering is the centre: a feature table with switchable columns
is the contract between producers (source.facts_for, semantic facts, entry
points, diff hunks; lizard and Semgrep only after their evaluations), the
ablation runner and the hunter program. DSPy runs the program (ReAct over a
HuntSignature; GEPA and MIPROv2 propose), our rule accepts on held-out
folds. The pilot stays held out; the hook, its client and the core install
are untouched. Requirements approved by the maintainer ('4 first').

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:14:05 +02:00
Marius CiepluchandClaude Fable 5.1 062a7b780e refactor(p1): one source-facts module owns function ranges, callers, declaration tables and language aliases
src/openultrasast/source.py replaces the copies in semantic/functions.py,
plane/tasks/repo_facts.py, learn/features.py, learn/excerpt.py and
plane/tasks/verify.py. Two engines share one sentinel (<module>), one
DECLARATION/KEYWORDS/RESERVED set and one alias map: the parser engine
(ranges, enclosing, bounds, call_sites, callers_of, facts_for) for the
scorer, the overlay and the hunter's annotations; the line-pattern engine
(declared_functions, enclosing_at, function_span, function_bounds, the brace
matcher) that labels, features, roles, excerpts and plane facts were built
on. learn no longer imports plane.tasks. Frozen records that store the old
sentinel are mapped on read (module_function).

Gate: labels rebuilt before and after are identical, 7,921 rows, after
sentinel normalisation (benchmarks/measurements/2026-10-13-source-module-
label-diff/record.json). 490 targeted tests pass; 3,312 collect. Net: 657
lines deleted, 212 added. benchmarks/unseen keeps <global> for its own
frozen manifests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 12:00:37 +02:00
Marius CiepluchandClaude Fable 5.1 f63703a604 chore(p0): drop never-enabled hunter tools, test-only functions and receiver delivery
Off every active line (PATHFINDER-2026-10-13/03-unified-proposal.md, cuts).
Deleted: the four hunter tools no caller enables (read_definition,
entry_points, flows, obligations) with their schemas and dispatch, the dead
mapping branch of find_refs; 20 functions whose only caller was one test
(mapping task records, compute_ranking_metrics, build_quick_hunter_prompt,
validate_evidence_transition, should_fuse, harness processors,
rank_targets_with_model, model/report slice rendering, taxonomy
hierarchical/related, config_value.verdict, taint.verdict, roles.load_roles,
features.language_of and build_for_scan, folds.leave_one_*,
features.recorded_case, search BatchExecutor and the three __main__ shims,
delivery.put_input); the receiver delivery mode of the plane. push_inputs.py
moved to benchmarks/push. memory.purge stays: the live-S3 test cleanup uses
it. Net: 2,730 lines deleted, 207 added. 473 targeted tests passed; 3,282
tests collect. breadth.toml records the removals.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 11:06:09 +02:00
Marius CiepluchandClaude Fable 5.1 f5a904b7f2 chore(p0): retire regress, the skill router, the module audit tool and vocabulary.sc
Off every active line (PATHFINDER-2026-10-13/03-unified-proposal.md, cuts).
Deleted: src/openultrasast/regress/ and the deep-mode REGRESS branch (deep
now records a regress_retired degradation and equals standard; proof is the
search-with-proof arm's job), the sandbox docker runner and probe that only
regress used, --fail-on worth-fixing, RegressConfig, the two ledger helpers
only regress called; skills.py and index.py with the hunter pool's skill
routing; model/audit.py; cpg/queries/vocabulary.sc with
benchmarks/push/ontology_gap.py. Docs and the scan skill describe the
retirement. Net: 3,895 lines deleted, 86 added. Targeted tests: 180 passed,
28 skipped; 3,331 tests collect.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 10:50:01 +02:00
Marius CiepluchandClaude Fable 5.1 94e96c6885 chore(p0): remove the improve lever and the plane improvement loop
Off every active line (PATHFINDER-2026-10-13/03-unified-proposal.md, cuts).
Deleted: src/openultrasast/improve/ (1,175 lines), the ousast improve
subcommand, plane/tasks/loop.py and loop.yaml with the --loop generation,
the memory-snapshot step in memory.seed, pairs.build_pair_signals, and the
four improve tests plus the loop cases in other tests. Docs no longer
describe the lever. Net: 3,073 lines deleted, 83 added. Targeted tests:
154 passed; 3,436 tests collect.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 10:34:57 +02:00
Marius CiepluchandClaude Fable 5.1 d2010d0693 docs: Pathfinder map of src, duplication report, unified proposal, and the breadth ledger
PATHFINDER-2026-10-13/: feature inventory, ten per-unit flowcharts with
file:line evidence, cross-feature and within-unit duplication reports, a
proposal that keeps every package on one of the four active lines, and
copy-ready plan prompts in order. .kiro/breadth.toml: every spec and
capability with a status and the deletion that closes it. No code changed.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 09:55:21 +02:00
Marius CiepluchandClaude Fable 5.1 e979fb0d95 chore: remove retired specs and dead code, state the 2026-10-13 focus
fossil-mcp dead-code over src found 72 candidates; 69 are referenced by tests,
__all__ or other modules, 3 are not and are removed (cli._first_source,
pairs._scorable, provider.openrouter._extract_message_content). Eight spec
directories that steering already called retired or superseded leave the tree
(history keeps them, commits named in overview.md): harnessx-removal,
harnessx-self-improving-rulesets, openrouter-sast-harness, learning-harness,
constrained-detector, three-stage-scan, real-world-vfc-slice,
unseen-repo-evaluation. overview.md gets a current-state section: the valid
benchmark numbers, what they say, and the four active lines in order.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 09:12:37 +02:00
Marius CiepluchandClaude Fable 5.1 3b8327ee51 spec(hunter-learning-loop): requirements
The hunter gets an annotation model (named, versioned facts about the push)
and statistical ablation decides which annotations stay. Labels from fix
sites and ordinary pushes, training set apart from the held-out pilot,
compiled replayable program, train-and-holdout acceptance rule, two stages
reported separately, no thresholds set in this spec.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 08:47:51 +02:00
Marius CiepluchandClaude Fable 5.1 ad20405fc2 docs(benchmark): where the hunter's false alarms come from
Sweep over the pilot's hunter records: most 'vulnerable' suspicions on ordinary
pushes sit far from the diff. Scope to the change, then confidence, then the
config_secrets family; 5/12 recall kept at 8.8 false alarms per 100 (in-sample).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 08:35:04 +02:00
Marius CiepluchandClaude Fable 5.1 23aeeada10 feat(benchmark): combine --keep-verdicts drops hunter suspicions the classifier rejected
Opt-in filter on the classifier verdict for hunter-sourced defects; the default
keeps everything. Each combined record notes the kept verdicts and the counts.
Live check on the pilot: (a)+(b) filtered to 'vulnerable' gives recall 5/12 and
54.4 false alarms per 100 ordinary pushes, matching the scratch numbers.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 08:21:59 +02:00
Marius CiepluchandClaude Fable 5.1 af7d3e49b1 docs(benchmark): pilot numbers, hunter arm final and push hook interim
Arm (b) hunter over all 252 changes: recall 6/12 raw, 5/12 with the classifier
filter; false alarms 92.5 and 54.4 per 100 ordinary pushes. Arm (a) push hook at
163/252: recall 0/12, 0.7 false alarms per 100. Per-positive table, classifier
stats, the one hook false alarm, and the disk fix for the replay output.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-13 08:13:02 +02:00
Marius CiepluchandClaude Fable 5.1 abd82c6e33 fix(benchmark): meter the provider client before building the classifier Caller
Live run: every classification failed with ValueError because Caller.ask
reads client.usage as a dict (the learn CLI always hands it a MeteredClient)
while a bare DeepSeekChatClient keeps usage as a list of rows. build_caller
now wraps a client that carries a usage list in MeteredClient; scripted and
already-metered clients pass through unchanged.

Regression test runs classify_family through the real DeepSeekChatClient
wrapper with a stub transport (the module fixture that stubs classify is
restored for it). The error field stays the exception type only, since a
message can carry a model key.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 20:47:12 +02:00
Marius CiepluchandClaude Fable 5.1 d37d9634c6 fix(benchmark): classify hunter findings into the taxonomy before scoring arm (b)
The tool hunter's family tag is the model's free text (a live run returned
family:sql-parser and family:readonly-enforcement), so every arm (b) defect
scored family "unknown" and score.catches() could never credit the hunter
even when it landed on the labelled file.

hunter_arm now classifies each finding into the closed taxonomy vocabulary
with the decision-engine classifier (learn.program.classify with
families=FAMILY_IDS minus unknown): a numbered, clamped excerpt around the
finding sets valid_lines, the canonical ANSWER_FORMAT is reused, an off-list
family parses to "none" and lands as "unknown". The benchmark label is never
a fallback. The defect keeps the raw hunter family, title and rationale plus
the classifier's verdict, confidence, rationale, cited lines and parse
failures for audit; the record counts classifier calls and families.
--no-classify keeps the old heuristic path.

14 new offline tests, 67 green with the benchmark tests.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 20:42:08 +02:00
Marius CiepluchandClaude Fable 5.1 9f7ca132ff feat(benchmark): hunter arm runner and combiner (group 6)
benchmarks/valid/hunter_arm.py runs the shipped deep-mode tool hunter
(tool_hunter.run_tool_hunter via resolve_hunter_client) over each frozen
change off-cluster: detached worktree at head with git hooks disabled,
hotspots scoped to the changed files, instrument rule on every hotspot
(bytes read printed, empty or unreadable fails the change loudly), model id
from the config resolver, family from the taxonomy, never from the label.
Records land in the exact shape score.py reads (push.admission.defects with
locations), resumable per change.

`combine` joins arm (a) replay records with arm (b) hunter records per change
id, dedupes defects by (path, line, family), keeps (a)'s instrument, records
which arms were present, and writes a file score.py loads directly.

9 offline tests (stub hunter, real scorer), 53 green with the benchmark
tests. No network, no key handling, no live run.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 20:29:03 +02:00
Marius CiepluchandClaude Fable 5.1 bd7b7db25d docs(benchmark): drop two em-dash headings in the brief
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 20:18:05 +02:00
Marius CiepluchandClaude Fable 5.1 98825db94a docs(benchmark): R6 holds the push path to the LLM tier; measure hook as shipped and hook + hunter
The pre-push hook has no LLM detection tier today: push/assistance.py only
selects a witness for already-admitted defects and cannot add a finding. The
hunter lives in scan --mode deep. The maintainer's call is that the LLM tier is
required on pre-push, so R6 now measures two arms over the same changes:
(a) the hook as shipped (quick rules + engine), (b) the hook plus the hunter
arm run off-cluster. (b) is the number the tool is held to; the gap is product
work in pre-push-safety-net, not a benchmark artefact.

Adds group 6 (hunter arm runner + combine + offline test) to tasks, aligns the
brief and design, and labels the overnight replay as arm (a).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 20:17:44 +02:00
Marius CiepluchandClaude Fable 5.1 7a636d60fb feat(benchmark): freeze the valid-benchmark pilot, 12 verified positives + 240 negatives
First live run of the valid detection benchmark. Labels come from the fix
site (fix^1), never SZZ.

- 32 held-out Python/JS advisories sourced from the public pool-p1 set,
  CWE + text from OSV. 133 candidate locations -> 125 after dropping
  tests/minified.
- 125 DeepSeek verification calls: 31 locations passed all three checks
  (vulnerable pre-fix, fix addresses it, reachable in-repo) across 12 repos.
- Maintainer spot-check accepted all 12.
- 20 ordinary negatives per positive change from the same repos, 240 total.
- Held-out guard: 0 dropped against the training-only used set.
- Frozen manifest at benchmarks/valid/pilot/ (252 changes), provenance and
  NOTES under benchmarks/measurements/2026-10-12-valid-benchmark-pilot/.

Three fixes found by the live run:
- catalog.py: skip a site the fix renamed or removed instead of aborting.
- assemble.py: negative ratio counts positive changes, not accepted
  functions (malla would have demanded 260 negatives). Test updated.
- held-out guard: run against training sources only; build_used() also
  scans measurements/populations, which flagged the 12 repos via my own
  SZZ-pool analysis outputs (circular).

Replay and metrics not yet run; see NOTES.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-12 19:42:45 +02:00
Marius CiepluchandClaude Opus 4.8 43ef254134 feat(benchmark): valid detection benchmark spec + offline modules (groups 1-4)
Builds the benchmark from .kiro/specs/valid-detection-benchmark/brief.md:
sound labels from the fix (no SZZ), app-shaped verified positives,
ordinary negatives from the same repos, whole tool on unseen repos,
standard metrics.

Spec: requirements.md, design.md, tasks.md. Reuses the existing sourcing,
fix-site labelling, negative drawing, replay, and Wilson scoring; drops
the SZZ layer.

New offline modules under benchmarks/valid/:
- catalog.py  positives labelled at the fix site (never introducing/last_touch)
- verify.py   one LLM call per case (vulnerable? fix addresses it? reachable
              in-repo?) + human spot-check; this is the app-shaped filter.
              Honors the instrument rule: bytes read per function, reject on
              an empty read.
- assemble.py >=20x ordinary negatives + a frozen, deterministic manifest with
              the held-out guard
- metrics.py  precision (PPV), false alarms per 100 pushes, exploit-verified
              rate, and the plain headline, layered on score.py

36 new offline tests, 495 regression tests green. No network, no harvest,
no model calls in tests. Group 5 (the live pilot) is not started.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 18:55:42 +02:00
Marius CiepluchandClaude Opus 4.8 3855945311 docs: rename benchmark metrics to standard security terms
Lead with the names a security engineer already knows instead of the
coined ones:
- catch rate      -> recall (true-positive rate)
- false-alarm rate -> false-positive rate (still reported as false
  alarms per 100 pushes)
- proved-catch / proof rate -> exploit-verified findings (a working
  proof of concept)
- precision stays precision (positive predictive value)

The plain-English question for each metric stays as the explanation.
Updated the valid-benchmark brief and the roadmap. No numbers changed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 18:24:13 +02:00
Marius CiepluchandClaude Opus 4.8 39a1873e5b docs: plain-English pass over the active specs
Ran the humanizer rules over 79 prose files in the active specs (brief,
design, requirements, research and narrative docs). Removed em dashes,
significance inflation, forced rule-of-three, bold-label list items and
filler. Skipped tasks.md checklists and the retired specs (harnessx-*,
openrouter-sast-harness, unseen-repo-evaluation).

Every number, path, command, hash and identifier was preserved: checked
each file against HEAD for drift in numbers, inline code and code fences,
zero drift across all 79 files.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 18:16:41 +02:00
Marius CiepluchandClaude Opus 4.8 b08f372022 docs: retire the SZZ-pool metric, add the valid-benchmark brief, plain-English pass
The frozen SZZ-labeled pool is not a valid benchmark. SZZ puts the vulnerability in
the wrong place about half the time, and ~76% of entries are library-internal (not
reachable in-repo), so its catch/false-alarm numbers (record 07's "3/92") are noise.
Retired as a metric and a gate across the roadmap and briefs; no pool-scored number
is quoted anywhere.

Added .kiro/specs/valid-detection-benchmark/brief.md: positives labelled from the
fix (not SZZ), kept only when app-shaped and reachable in-repo, each verified;
ordinary negatives from the same repos; unseen repos; the whole tool measured; plain
metrics (catch rate, false-alarms per 100 pushes, precision, proved-catch).

Ran a plain-English pass (the humanizer rules) on the roadmap, README, and the two
active briefs via Codex, then reviewed the diff: only these files changed, every
number/path/hash/command is unchanged, and the retired-metric decision is intact.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 16:38:18 +02:00
Marius CiepluchandClaude Opus 4.8 c3f610f9f4 test(infra): drop the Dockerfile-text assertions from the engine-queue test
Follow-up to a36f99e (the prior git-add aborted on the already-removed pathspec and
left this edit unstaged). Removes the change-detector image assertions and renames
the function to test_rustfs_engine_queue_policy; the least-privilege rustfs policy
assertions are what remains.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 16:03:14 +02:00
Marius CiepluchandClaude Opus 4.8 a36f99e15e test(infra): delete the change-detector Dockerfile tests, keep the real policy check
They pinned exact Dockerfile text / an engine-stage SHA, so every legitimate image
edit broke them and the "fix" was to paste the new value back — a change-detector,
not a contract. Deleted:
- tests/test_unseen_image.py (test_image_stages) entirely.
- the Dockerfile-text assertions in test_engine_queue's image test.

Kept as a genuine contract: the rustfs least-privilege queue policy assertions
(renamed test_rustfs_engine_queue_policy). The partial-clone extraction test and
its new hermetic-git-env fixture stay — that one verifies real behaviour.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 16:02:47 +02:00
Marius CiepluchandClaude Opus 4.8 e49c150316 test(infra): fix the 3 stale/flaky infra tests to the current contracts
- test_engine_image_and_rustfs_policy: root Dockerfile was refactored to the
  digest-pinned ousast-task-base (carries semantic+s3 deps) + a joern-build stage,
  installing the package --no-deps. Updated the stale `COPY benchmarks` (now
  --chown=1000) and `.[semantic,s3]` (now --no-deps + joern-cli copy + pinned base)
  assertions to the new contract.
- test_image_stages: plane/Dockerfile.engine-task refactored the same way; the
  split marker is now `FROM ${ENGINE_REF} AS replay`, base is ousast-task-base
  (not ax-task-runner:v0.3.1), and the replay stage no longer installs git (base
  provides it). Re-pinned the engine-stage SHA and updated the markers/assertions.
- test_partial_clone_lazy_fetch: passed alone but failed mid-suite — a leaked
  GIT_DIR/GIT_OBJECT_DIRECTORY/alternates from an earlier test changed the
  missing-blob count. Added an autouse fixture that neutralises inherited git
  control vars so every test in the file is self-contained (reproduced + verified).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 15:58:11 +02:00
Marius CiepluchandClaude Opus 4.8 6c789eac40 docs: correct G0 status (record 07 done on docker) + ax-lane is a G1 target
Roadmap "Where we are" table said Baseline (G0.2) = "Not yet, run on kube" -- stale.
Record 07 (G0 exit) was completed right after on the docker lane (e901640, 1300/1300,
catch 3.3% / FP 3.1%, regex/quick-rule floor, engine_covered=0). G0 is exited; the kube
`ax` lane was never needed for the baseline. Reframed the readiness plan: steps 1-4 done,
the `ax` lane moves to G1 (engine arm at scale, gated by spec tasks 4.4/5.4).

Also fixed the ax binary in the coordinator commands: use $HOME/go/bin/ax, NOT
$HOME/.local/ax-v0.3.1/ax (the latter hits the AX-lane dispatch blocker recorded in the
unseen spec tasks.md). README.md + benchmarks/unseen/README.md.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 15:30:11 +02:00
Marius CiepluchandClaude Opus 4.8 9a3250f6c5 fix(corpus): advisory-pair repo-overlap -- remove real pilot leaks, skip vendored trees
test_no_pair_repeats_a_repository_the_benchmarks_already_name failed: advisory-fix
catalog repos also appeared elsewhere under benchmarks/. Root cause split two ways:

1. Genuine leak: 16 catalog repos are also subjects in benchmarks/search/private/
   pilot.json (a real private pilot population). Removed those 16 pairs from the
   three catalogs and their verification.json rows (totals recomputed, catalog ==
   verification preserved).
2. Spurious: the overlap scan greped ~49k vendored files under
   benchmarks/search/private/runs/*/checkout/node_modules/, so any catalog repo a
   vendored dependency's README names collided. Added a narrow vendored-tree skip
   (node_modules/vendor) to the scan rather than deleting labelled pairs over a
   dependency mention.

Implemented from Codex's first pass (gpt-6.1-sol), then gated and corrected: Codex
removed 20 pairs; 4 of those (backstage/backstage, koajs/koa, beaugunderson/
ip-address, cakephp/cakephp) were driven only by vendored/non-population mentions
and are kept. Also excluded advisory-fixes-spread.json (catalog-derived report)
and research-2026-09-26.md (selection write-up) as non-population.

tests/test_advisory_fix_pairs.py + tests/test_learn_labels.py: 53 passed.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 15:22:55 +02:00
Marius CiepluchandClaude Opus 4.8 60ffff0168 fix(docs): add the two orphaned pages to mkdocs nav
`pre-push-for-developers.md` and `use-case-fix-and-reverify.md` (authored
2026-10-07) were in docs/ but absent from nav, so `mkdocs build --strict`
errored and test_docs_site::test_site_builds_strictly failed. File both under
"Using it", restoring the strict build.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 14:52:42 +02:00
Marius CiepluchandClaude Opus 4.8 5ca1db7b7c docs(bug): close the blast-radius question for the taint single-query skew
Call-graph trace: no shipped product path or in-repo benchmark/test reached the
real single-query JoernBackend.query("taint"). The push/scan/replay path is
double-shielded -- batch fetch (run_batch) in Phase 2, and a _prefetched replay
CpgResult in Phase 3 whose run ignores the query -- so scan_region's cpg.run
("taint") never reaches JoernBackend.query. Benchmarks use the batch path; tests
use fake canned-row results. Only out-of-product ad-hoc harnesses hit it.
Regression onset 562b670 (2026-09-25).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 14:27:31 +02:00
Marius CiepluchandClaude Opus 4.8 a024656da0 fix(cpg): single-query path drops params the script does not declare
taint.request_params emits guards/fixedOrigin/originAnchors/quotedSanitizers/
fieldParameterSources, which taint.sc reads from per-request JSON in its BATCH
branch but does NOT declare on `@main def exec` and does NOT consume in its
single-request branch (it defaults them). The single-query path passed them as
`--param guards=...` (disposable) or named args (session), so joern errored
`Unknown arguments` and every cpg.run("taint", ...) failed -- a silent zero of
the shape AGENTS.md warns about. scan_region->taint_verdict->_usable_flows and
the pair-path verdict()/verdicts() were dead; the push/scan batch route was
unaffected (it carries the extras as JSON the script parses itself).

Fix: parse the script's `@main def exec(...)` signature once (_declared_exec_params
+ memoised _declared_params) and, in query(), filter params to the declared set
before building either route. Faithful, not lossy -- the single-request branch
could never use those 5 fields. No-op for config/dominance (their request_params
emit only declared params; verified). Only taint sheds the 5 extras.

Verified end-to-end on banks (patched backend in the replay image): single-query
path now runs; baseline 0 findings, annotated search 2 path/model_entailed
findings (prompt->write_text). Regression test asserts no undeclared param
reaches the command line and the query parses, with a fake runner (no Joern).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 14:16:36 +02:00
Marius CiepluchandClaude Opus 4.8 8f7fc4ae83 measure(g2): banks fixed-search MVP -- "no wrong targets, only wrong searching" demonstrated
Same CPG, only the search changes. Baseline (shipped path spec) resolves 0
prompt->write_text flows on banks; injecting the two annotations an LLM steer
would supply (parameter_sources=True + write_text/read_text path sinks) makes the
engine resolve prompt -> path-join -> prompt_file.write_text(prompt.raw). First
measured instance of "#2 guided by #1" closing on an unseen target; fix needed
exactly the param-source + file-write-sink annotations.

Files the instrument bug the first MVP hit: single-query cpg.run("taint",
request_params(...)) is dead -- request_params emits 5 keys (guards/fixedOrigin/
originAnchors/quotedSanitizers/fieldParameterSources) absent from taint.sc's exec
signature; both single-query sub-routes reject them (silent None). Batch path
(run_batch->requestsFile JSON) unaffected; push/scan route uses batch.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 14:02:41 +02:00
Marius CiepluchandClaude Opus 4.8 7ab5c2ab2d measure(g2): proof-arm on the 3 fair hits -- push-time lift 0/3 (structural), proof is async
All 3 hits have a runtime oracle (search FAMILY_ORACLES: injection->sql/command, ssrf for
untrusted_destination -- corrects the earlier families.toml-based 'dcmcp unprovable'). But
search/verify.verify() needs an AUTHORED runnable demo + both head/base building OFFLINE + a
sandbox differential (3/3 affected vs 0/3 safe). The scan/push path reaches only suspicion/
static_corroboration; no code path from a scan suspicion to exploit_demonstrated, no Coordinator
caller from the push flow. So proof-arm lift AT PUSH TIME = 0/3 -- proving is a per-target,
offline-build, runtime, differential operation that can't run inside a pre-push hook (prior
per-pair demos were mostly inconclusive on this wiring).

Strategic consequence: proof/BLOCK is an ASYNC lane (CI/plane/triage with authored demos), not
pre-push. Honest pre-push product = advisory-with-conditions (bet #2); proof arm = async BLOCK
lane (bet #3). Local sandbox exists (userns).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 11:46:26 +02:00
Marius CiepluchandClaude Opus 4.8 04667c815e measure(g0): fair-target deep scan -- detection 3/5 on app-shaped+sound vs ~0 library-heavy
Ran ousast scan --mode deep on the 5 app-shaped + sound pool targets (the fair change-scoped
eval set from the shape classification). Detection 3/5: graphiti (hunter, Cypher injection),
mcp-remote (hunter, url-scheme injection), dcmcp (ENGINE overlay -- 'outbound.request reached
from parameter' on readFileFromUrl; parameter-as-source fired because an entry point is present).
2/5 missed: malla (DOM/template XSS) and dms (Jinja SSTI), both in families families.toml
documents as limited.

This validates the strategic pivot: the near-zero catch was substantially a TARGET MISMATCH --
on app-shaped repos the engine itself detects (0 -> 3/5). But all hits stop at suspicion/
static_corroboration; release-grade BLOCK still needs proof-arm elevation + family coverage +
the DSPy hint loop. Cost ~$0.007/repo. So bet #1 (fix eval target) is necessary and material
but not sufficient.

Next: recompute a floor on an app-shaped sound subset / real app corpus (VAmPI, NodeGoat,
DjanGoat, vibe-py); measure proof-arm suspicion->proven lift.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 11:16:35 +02:00
Marius CiepluchandClaude Opus 4.8 e15ed680a9 spec(g0): strategic pivot -- core problem is unprovable reachability; fix the eval target first
Named the core problem with the maintainer: the tool spots dangerous ops but cannot prove
attacker-reachability -- the trust boundary lives outside the changed library code. Chosen
strategic bet (of three): FIX THE EVALUATION TARGET FIRST.

LLM shape classification of the 49 public pool vulns ($0.033): reachability_in_repo only
24% (12/49); 76% are pure libraries. A legitimate change-scoped catch needs sound label AND
reachability in-repo -> just 5/49 (~10%): graphiti, desktopcommandermcp, malla,
document-merge-service, mcp-remote. Shape predicted the hunter outcome (graphiti app-shaped ->
caught; bedrock library -> missed). So record-07's 3/92 is scored against a ~90%-illegitimate
denominator -- the near-zero catch is largely a target mismatch.

Next: deep scan on the 5 fair targets for a per-tier catch rate; then an app-shaped eval set
from known-vulnerable apps. Artifacts: 2026-10-12-pool-p1-shape-classification/ (+ earlier
label-audit, hunter-sound-targets). Memory: evaluation-target-mismatch.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 10:49:56 +02:00
Marius CiepluchandClaude Opus 4.8 27a7c12ca1 measure(g2): model hunter on sound targets -- first LLM catch the static tiers missed (1/2)
ousast scan --mode deep (DeepSeek deepseek-flash, model layer + tool_hunter, 60-call bound) on
bedrock + graphiti, the two sound pilot targets the static engine missed:
- graphiti: HIT -- falkordb_driver.py:207, tag family:cypher-injection; rationale traces
  build_fulltext_query concatenating user input into a Cypher string at line 278 (inside the
  labelled function, location-matches). The static engine produced zero findings here.
- bedrock: miss -- 12 suspicions on adjacent concerns (network-PUBLIC, download_file), never
  named the install_packages -> pip command injection.
Cost is a non-issue: ~$0.007/repo, 64 model calls for a whole-repo deep scan. BUT everything
stopped at suspicion rung -- the proof arm did not elevate to proven; release-grade BLOCK needs
proven findings.

Direction (maintainer): prompt evolution is a core lever -- a DSPy-based loop (GEPA/MIPRO,
pluggable; evolving field) whose objective is to learn what hints/annotations steer the model
to the real sink, sourced from the framework knowledge already shipped (no new user
annotation), with LLM-proposed boundaries graduating into the facts.

Next: measure proof-arm lift on graphiti; stand up the DSPy hint loop + re-run bedrock; widen
the hunter to the 23 sound targets for a catch rate; then a change-scoped + proven run.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 10:37:10 +02:00
Marius CiepluchandClaude Opus 4.8 a43e6293f5 measure(g0/g2): LLM corpus-label audit -- only 47% of public pool labels are sound
Widened the banks label-mislocation finding across all 49 public pool vulns via DeepSeek
(advisory + head code + pool label -> sound/unsound), $0.038 total. Result: 23/49 (47%)
sound; 53% unsound (25 pre-exist in base, 16 sink outside the changed lines) -- a change-scoped
hook correctly stays quiet there. Systematic by family: injection 10/13 sound, but path 1/7,
output_encoding 1/8, config_secrets 1/4.

Implications: record-07's floor (catch 3/92) is scored against a corpus ~half unfair to a delta
hook; the honest catchable denominator is the sound subset (caveat added to record 07 NOTES).
Fair pilot re-score: only bedrock + graphiti (injection) of the 5 are sound and the engine
missed both = the real detection gaps for the LLM-guided hunter.

Also: GEPA-style reflective-retry (future spec) + GLM cyber model (future spec) noted; DeepSeek
is the working default. Artifact: benchmarks/measurements/2026-10-12-pool-p1-label-audit/.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 10:09:32 +02:00
Marius CiepluchandClaude Opus 4.8 53b1d31d82 measure(g2): banks LLM sizing -- '2 guided by 1' validated; corpus label mislocated
DeepSeek deepseek-flash reachable: ~$0.0002/call, ~1.1s (sub-cent/push). The LLM gives the
correct verdict WHEN aimed at the real sink + source context: asked about the pool's
SZZ-labelled site (_load) it correctly says not-vulnerable; asked about the real sink
(from_prompt_path line 44, prompt.name request-derived) it correctly says vulnerable. So '2
guided by 1' works as a mechanism.

Two blockers, both concrete: (1) it needs the agentic search-with-proof hunter to supply
caller/source context -- single-shot on the ranker's region is insufficient; (2) the pool's
banks label is mislocated (head_lines 30/104/138-141, function _load; real sink is line 44),
so even a correct finding wouldn't score as a catch and the sink pre-exists in base -- a
change-scoped hook correctly stays quiet. Part of the pilot engine 0/5 is SZZ mislocation, not
a detection failure. GLM/abliteration fallback errors HTTP422 on the thinking param (client
fix needed; DeepSeek works).

Next: validate the 5 pilot labels (change-located & introduced?) before detection runs; then
size the agentic hunter on the sound targets; feed confirmed source shapes back as guarded facts.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 10:02:18 +02:00
Marius CiepluchandClaude Opus 4.8 90e0098a94 spec(g2): root-cause engine 0/5 (library params not sources) + the '#2 guided by #1' workstream
Verified root cause: the pilot repos are libraries whose untrusted input is public-API
parameters, not HTTP requests. Python taint sources (ruleset/semantic/python.toml) are web/CLI
only; the parameter-as-source mode (_parameters_are_input) fires only for recognised entry
points, and the pilot vuln regions are entry:false -- so their params are deliberately not
sources. Widening that statically would reintroduce the WordPress/PMPro false-SQL-injection
flood the docstring documents. So the static engine cannot safely source library APIs on its
own -- which is exactly the seam for the maintainer's '2 guided by 1' directive: the LLM
adjudicates which library parameter is a trust boundary, the taint engine traces that
source->sink as proof (not LLM say-so), and repeated source shapes graduate to guarded static
facts. First increment (pending budget): LLM-guided-source mode on the 5 pilot regions,
measuring source naming, traced finding, catch lift vs regex 1/5, and false-alarm on ordinary
changes; staged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 09:24:06 +02:00
Marius CiepluchandClaude Opus 4.8 ae22136c08 measure(g2): CORRECT pilot diagnosis -- engine produced zero vuln-family findings; anchoring is a red herring
Inspecting head.findings (not ranker evidence) overturns the previous commit's read:
the dataflow/taint tier produced ZERO findings in the vuln families (path/injection/deser)
on all 5 repos -- only config_secrets (banks 1, socketio 2; launch-editor/graphiti/bedrock 0).
The tier1/score2.0 'sink' I cited was the RANKER's static candidate, not a confirmed taint
finding. taint_evidence_not_applicable = no source->sink flow was traced (banks: sink found,
no source). So ambiguous_line_correspondence is downstream and moot -- there are no
vuln-family findings to anchor, and the region-level witness fix (Option A) is refuted before
implementation. The LLM adjudication tier was disabled (model_calls=0).

Reprioritised: #1 LLM-tier pilot L1/L2 (now primary, staged, costs money), #2 taint
source/sink coverage + region ranking, #3 change-anchoring (only after tiers yield findings).
Instrument rule: caught the ranker-vs-finding misread before building the wrong fix.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 09:19:04 +02:00
Marius CiepluchandClaude Opus 4.8 a5a37374a7 measure(g2): engine-on pilot -- regex 1/5, engine 0/5 on clean host; budget hypothesis refuted
Five public pool repos, vuln change each, clean host, 300s. The shipped engine added NO
catches over the regex floor. 3/5 repos finished their region queue (deferred=0) and still
emitted no finding, so change-scoping is not the path to catch. The engine ranked the vuln
region in all 5 and found the right sink on 2 (score 2.0), but withholds any finding it
cannot anchor 1:1 to changed lines -- all 5 hit ambiguous_line_correspondence ->
witness_unresolved -> coverage incomplete, suppressing even the scored sinks. Confirmed not a
shallow-clone artifact (correspondence is computed from both blobs).

Reprioritised engine levers: #1 change-anchoring robustness (region-level witness fallback),
#2 family ranking/taint coverage, #3 the LLM adjudication tier (the pilot is the case for it).
Release read: the pre-push net is NOT releasable on detection yet.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 08:53:38 +02:00
Marius CiepluchandClaude Opus 4.8 4c4b7d375a measure(g2): E1/E2 clean-host results -- the engine 'query=0 wall' was contention, not intrinsic
On a clean host the shipped engine runs dataflow (query 0->35s, selected 0->356 regions).
The evidence/taint batch (~225s) is invariant to warm session and graph preload; importCpg
is cheap (~35s). The real barrier is taint.sc's ~160s per-batch flow work, so build+evidence
~255s of a 300s budget defers 1009/1365 regions. Lever = change-scope the family/file set the
taint batch walks, not session/preload. Corrects the prior ~200s graph-load wall diagnosis.

Next decisive experiment: engine-on unseen pilot vs record-07 regex floor (catch 3/92).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-12 08:20:16 +02:00
Marius CiepluchandClaude Opus 4.8 e901640f3f measure(g0): record 07 — slice-1 unseen-repo baseline (G0 exit)
First honest, measured unseen-repo baseline on 100 frozen never-seen repos
(1300 changes, DEFAULT arm, docker lane). This is the quick pattern-rule FLOOR
(no LLM: model=disabled, 0 calls; engine_covered=0, the ~200s wall) — the release
gate is now a measurement, not an assertion.

- catch 3/92 = 3.3% (Wilson95 1.1–9.2%); false_alarm 3.1% (cluster95 1.6–5.4%,
  icc 0.25); alerts_only 0%; instrument_failure 4.2% (<5%); engine_covered 0.
- by family: deser 1/4, injection 2/20, rest 0.
- baseline row appended to usage-p1.jsonl (freeze_digest 3371c67d) = G0 exit.

Caveats in NOTES.md: machine-label (SZZ, 100 unchecked), regex-only floor (NOT the
>=90%/<10% gate), docker lane, slice-1 sizing (~±10% overall, per-family too thin).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 22:30:58 +02:00
Marius CiepluchandClaude Opus 4.8 e716e3beab docs(g2): record-07 baseline is regex-only; prioritize semantic detection + barrier analysis
Verified on the records: the push replay arms hardcode model="disabled",
max_model_calls=0, cost_usd=0 (NO LLM) and engine_covered=0 (engine never ran,
~200s wall). So the measured catch~2.5% is the quick pattern-rule tier ALONE --
grep-with-structure. Pattern rules alone will never be enough.

Reprioritize G2 after record 07 to an efficiency barrier analysis over the two
tiers that carry detection and both sat out:
- engine (Joern dataflow): E1 split the ~150s evidence-fixed (JVM/graph-load/
  census); E2 does a warm session + graph preload at build kill it (decisive);
  E3 analysis per-pair; E4 build vs repo size.
- LLM adjudication: L1 model latency + calls per push; L2 recall lift over
  regex+dataflow on the pool.
Deliverable: a barrier table (budget x tier combo -> fits? measured recall/FP)
for a realistic pre-push window (<=120s target, 300s hard) -> minimal releasable
config. Change-scoping branch is a late add-on (trims only the per-pair tail),
not the lever. Also fixed a stray </content> tag in the brief.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 15:05:51 +02:00
Marius CiepluchandClaude Opus 4.8 df09fe4030 docs(g2): engine-arm wall diagnosed -- ~200s fixed overhead, change-scoping insufficient
Records the full G2 engine-detection finding on main (code attempt parked on
branch engine-change-scoped-evidence-pass, unmerged). The engine arm has ~200s
intrinsic fixed overhead -- ~48s CPG build + ~150s first-query graph-load+census
(measured uncontended: build 48s, evidence 243.79s over 1015 pairs, query 0) --
so a 300s budget cannot reach the flow queries. Pair-count change-scoping trims
only the ~86s per-pair tail; the warm-session env times out at startup contended
and spares only subsequent batches. Real fix is clean-host engine work (reliable
session startup + graph preload, or census-cost reduction). Parked to finish G0.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 13:31:55 +02:00
Marius CiepluchandClaude Opus 4.8 15992d3090 docs(g2): increment 2 measured -- the 236s is 99.9% the evidence-tier pass
Saved-artifact run: total=291s build=52s query=0s arbitrate=238.75s, and
query_seconds_by_kind[evidence]=238.54s. The evidence-tier pass is the whole
middle; affected_context + _scope_work are 0.21s. It tiered 1015 pairs at
~235ms/pair (contended) and exhausted the budget -> selected=0, deferred=1365
all deadline_exhausted. Secondary anomaly: ~50-100x the design's assumed
2.5ms/pair -- the evidence query is pathologically slow on a cold large CPG.

Increment 2's single target is the evidence pass (scan.py:338-359). Fork
recorded: (i) scope+bound the pass change-first, (ii) two-phase neighbourhood
pass, (iii) per-pair speedup. Must preserve reach-into-unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 12:11:57 +02:00
Marius CiepluchandClaude Opus 4.8 a0637f2f9d docs(g2): correct increment 2 -- preserve reach-into-unchanged, measure the 236s split first
A first scoping attempt (restrict analyzed regions to changed files) was reverted:
it breaks a tested design invariant. The engine deliberately selects an unchanged
region's question when a changed region reaches it
(test_changed_dependency_reaches_unchanged_ranked_question;
test_change_does_not_replace_ranker_selection). _changed_files feeds _evidence_pass
only as contextFilter/contextPaths (itemise vs summarise), not to shrink the tier
population. Any scope must keep changed regions PLUS the regions they reach.

Also: increment 1 over-claimed. Increment 0 measured build=67s/query=0s but NOT
the split of the ~236s middle (evidence-tier pass vs affected_context/context
resolution vs _scope_work enumeration). Increment 2 must capture per_kind /
other_seconds / affected_context timing and cut the dominant phase while
preserving reach-into-unchanged -- instrument before fixing.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 11:50:41 +02:00
Marius CiepluchandClaude Opus 4.8 9580ad1e30 docs(g2): increment 1 located -- regions are discovered whole-repo, never change-scoped
The 4095 candidates are not an in-scan artifact: push/runner.py:345 discovers
head_regions across the whole head tree, then runner.py:397 passes them wholesale
to scan_repository with change_context threaded in but unused for scoping. The
scan enumerates all regions x families (scan.py:167/364) and, with
order_by_evidence=True, evidence-tiers every pair before analysis -> the ~236s
from increment 0 -> deadline at query_seconds=0.

Fix locus (increment 2): scope the analyzed region set to context.changed_paths +
dataflow neighbourhood (regions.py:279 primitive exists; compare_targeted_base is
precedent). Keep the CPG whole-repo so taint can trace into the change from
unchanged code; only the question set is change-scoped.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 11:24:43 +02:00
Marius CiepluchandClaude Opus 4.8 7a1e47e0cc docs(g2): increment 0 confirmed -- CPG build cheap, candidate enumeration is the overspend
Timed live bedrock replay: build_seconds=66.69, query_seconds=0.0, total 303s,
4095 candidate questions all analysis_not_started=deadline_exhausted, selected=[],
regions_scanned=0. ~67s builds the CPG; ~236s enumerates/ranks/prepares 4095
repo-wide candidates for a 3-file change, then the deadline hits before any query
runs. The CPG build is not the bottleneck; the over-broad candidate set is ->
change-scoping is the confirmed lever (and compounds with the correspondence fix,
since per-candidate prep includes correspondence work).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 11:21:33 +02:00
Marius CiepluchandClaude Opus 4.8 dc23151022 Merge engine-change-scoped-workstream: open the G2 change-scoped-analysis workstream
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 11:10:26 +02:00
Marius CiepluchandClaude Opus 4.8 9f312177ce docs(g2): open the change-scoped engine-analysis workstream (brief + roadmap)
Track the engine-detection lever found while driving G0. Live diagnosis on the
frozen pool: the engine's "no finding" on unseen changes is usually
analysis-never-runs -- it ranks repo-wide candidates and defers them all
(deadline_exhausted) before analyzing the pushed change (bedrock injection,
303s, selected=[], ranked sites unrelated to the change). Lever: seed analysis
from the change's declarations + dataflow neighbourhood (rank.py/mapping.py) so
it runs within budget.

- .kiro/specs/engine-change-scoped-analysis/brief.md: problem (measured),
  hypothesis, anchors, and checkable increments (0 confirm build-vs-analysis
  cost split -> 1 locate selection/budget -> 2 seed from change -> 3 soundness
  + tests -> 4 live re-test -> 5 measure delta on the engine arm).
- roadmap.md G2: active workstreams (change-scoped analysis; the done-but-
  necessary-not-sufficient line-correspondence fix), for progress tracking.

Honest scope: G2, gated on the pool metric; not part of the G0 quick-rule
record-07. Coverage also needs the capability registry (Task 8.3); arm-scale
measurement needs the kube lane.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 11:10:26 +02:00
Marius CiepluchandClaude Opus 4.8 c39fbb8457 Merge push-correspondence-run-aware: run-aware base/head line-correspondence
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 10:12:03 +02:00
Marius CiepluchandClaude Opus 4.8 abb2058a54 push(snapshot): run-aware base/head line-correspondence (stop discarding intra-run anchors)
The correspondence mapped an equal-region base<->head line pair only when the
line's content was GLOBALLY unique in the file, so common lines (`}`, blanks,
imports) tripped `ambiguous_line_correspondence` and the finding's anchor was
discarded -> the operation came back `unmapped` -> novelty `unknown` ->
`comparison_unknown` -> the engine finding was suppressed (a direct cause of
"near-blind to fixes").

Replace the global-uniqueness gate with run-aware positional anchoring: segment
the equal-region pairs into contiguous runs (strictly +1/+1, as _equal_line_pairs
emits), and map a pair when its content occupies exactly one old run and one new
run. Duplicates WITHIN a contiguous run are positionally forced and now map;
duplicates ACROSS runs split by an edit are aligned arbitrarily by git and stay
withheld + flagged (soundness preserved -- the moved-repeated-operation guard
still passes, so no false `new`/identity claims).

Tests: update test_change_context_unknown_base_binary_and_ambiguous_lines_are_
unresolved (its two in-run sinks now anchor); keep the moved-operation soundness
test; add intra-run-maps, cross-run-withheld, and distinctive-line-anchors cases.
163 push tests pass, ruff clean.

Honest scope (design.md amendment): this removes one real cause but is
necessary-not-sufficient -- live-testing a messy 14-file Java change showed
coverage stays `incomplete` due to other, dominant blockers (capability_unavailable
until Task 8.3; the engine produced no actionable defect; eligibility/projection
gaps). Engine coverage on unseen diffs needs those too, not just correspondence.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 10:12:03 +02:00
Marius CiepluchandClaude Opus 4.8 c657c2219b Merge doc-g0-readiness-2026-10-11: G0 state, readiness plan, bucket-first docs
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 08:17:06 +02:00
Marius CiepluchandClaude Opus 4.8 d03590148f docs(g0): record the 2026-10-11 state and readiness plan; bucket-first + image provenance
Fold the night's learnings into the roadmap, steering and the
unseen-repo-evaluation spec so the process is reproducible and the
project is "ready now":

- roadmap.md: a 2026-10-11 state table (pool p1 frozen; harvest hardened
  + bucket-first; replay-image provenance pinned; baseline-on-kube next)
  and a 5-step readiness plan to record 07.
- storage-streaming.md: RustFS-over-local as the default (local disk is
  the scarce resource); the implemented ousast-corpus layout; replay
  outputs belong on RustFS; the AX store endpoint is operator-owned.
- design.md: an "Amendments (2026-10-11)" section (bucket-first
  persistence, ~2.2% rate-bound yield, machine-label record 07, pinned
  replay image provenance).
- tasks.md: a folded-in amendment block (pool p1 frozen + freeze recipe;
  bucket-first; CI replay image; machine-label baseline; kube lane +
  ax/store as operator-owned infra).

Facts stated are verified (measured yield, freeze digest, GHCR anon pull,
batch.py's endpoint check); infra capability/policy is left to the
operator, not assumed. The frozen pool artifacts themselves are not
committed here (a separate samples/licensing decision).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-11 08:17:06 +02:00
Marius CiepluchandClaude Opus 4.8 7b711a6150 Merge unseen-getrepo-cache: bucket-cache get_repo (one-time used-set canonicalization)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 17:10:19 +02:00
Marius CiepluchandClaude Opus 4.8 a3a38f1b9f unseen: bucket-cache get_repo (kill the repeated used-set canonicalization tax)
The real bottleneck was not enumeration or extraction: canonicalize_used
resolves the entire exclusion set (measured: 4,499 names) via serial
GitHub get_repo on every launch, before any harvesting -- ~30 min of
network-latency-bound work that also burns ~4,499 of GitHub's 5,000/hr
budget before real work starts (hence the low CPU and the "never
finishes"). The earlier resolution-cache only banked via
BankedGitHub.resolve, but canonicalize_used calls eligibility.resolve ->
get_repo directly, so it was never cached and re-ran every time.

Cache get_repo at the real choke point. CorpusBank gains cache_repo /
get_cached_repo storing one object per normalized name at
<prefix>/repos/<sha256(name)>.json -- NOT seed-scoped, since repo
metadata is seed-independent, so every seed/run shares it. 404s are
cached as None too (a found-but-None hit) so known-missing repos are not
re-fetched. BankedGitHub.get_repo now checks the local journal, then the
bucket, and only then GitHub (banking the result). canonicalize_used,
advisory_index and candidate resolution all funnel through get_repo, so
all of them are covered.

Effect: the first run pays the lookups once and banks them; every later
launch reads them from the bucket (no GitHub, no rate-limit spend), so
the rate budget goes to actual candidate harvesting and restarts are
cheap. Proven live against ousast-corpus (repos/ fills 1:1 with
resolutions). 206 unseen tests + new repo-cache test pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 17:10:19 +02:00
Marius CiepluchandClaude Opus 4.8 21aebe8538 Merge unseen-resolution-cache: cache enumeration + resolution to ousast-corpus
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 16:33:29 +02:00
Marius CiepluchandClaude Opus 4.8 06b8a40c46 unseen: cache enumeration + resolution to ousast-corpus (restarts skip the ~26-min prefix)
The decision bank made extraction durable, but every restart still
re-paid the ~26-minute advisory enumeration + per-advisory resolution,
since those lived only in the local journal. Mirror them into the bucket
so a restart reads them back and goes essentially straight to any
remaining extraction.

bank.py: bank_resolution/resolved at <prefix>/<seed>/resolved/<sha256(ghsa)>.json
(streamed one-by-one as each advisory is decided), cache_advisories/
cached_advisories at <prefix>/<seed>/advisories/list.json, and
bank_repository/repositories (caches the advisory_index repo-resolution
pass, preserving renames and 404s). All gated by RESOLUTION_CONTRACT="v1"
(extract.py) -- bumped only when enumeration/eligibility/source.candidate
output changes; recent fetch/parse/pool changes do not bump it.

draft.py run(): with --bank, advisories load from the cache (else
enumerate then cache); eligibility decisions load from the bucket
(contract-matched) to skip re-resolution; new resolutions stream to the
bucket before the local checkpoint write. Repository lookups reuse the
repositories/ cache. No --bank => local-journal behavior unchanged.

Net: advisories + resolutions + extraction all resume from ousast-corpus;
restarts no longer re-enumerate or re-resolve. Fresh and resumed runs
produce byte-identical manifests at 1 and 4 workers. Bank methods proven
live against ousast-corpus. 356 tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 16:33:29 +02:00
Marius CiepluchandClaude Opus 4.8 857263d975 Merge unseen-corpus-bank: durable decision bank in ousast-corpus (stream + resume)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 16:09:47 +02:00
Marius CiepluchandClaude Opus 4.8 4aa612253a unseen: durable decision bank in ousast-corpus (stream + resume, retires the local journal)
The draw lost all progress on any restart because the local journal
invalidates on a code change and is the only cross-run state. Make the
bucket the source of truth: stream every per-repo accept/reject to
ousast-corpus as it is decided, keyed by sha256(repository+"\n"+fix) so
writes are idempotent (content dedup at rest), and resume by listing the
bucket and skipping already-decided repos. No supervisor/watchdog needed
-- a crash or a code change just re-runs and resumes.

New benchmarks/unseen/bank.py: corpus_settings (CORPUS_S3_* from .env,
missing key named, value never printed), CorpusObjects (lazy boto3,
path-style + SigV4 verified live against ousast-corpus @ 10.0.0.54:9001),
CorpusBank.bank/decided with one-retry put/get, split_decided. Decisions
live at <prefix>/<seed>/decisions/<key>.json with repository, fix,
accept/reject, reason, entry, exclusions, counts, contract, seed, ts.

draft.py: --bank [prefix] opens the bank on the coordinator (never shipped
to fork workers); run() resumes from bank.decided() before extracting,
reusing accepted entries across contracts (deterministic from repo+commit)
and rejections only when contract == EXTRACTION_CONTRACT ("v1", in
extract.py); collect() streams each decision one-by-one. Seed-order
acceptance, fork/used-set dedup, and duplicate-advisory reuse preserved.
Without --bank, local-journal behavior is unchanged.

CorpusBank proven live end-to-end (bank accept+reject, idempotent re-bank,
decided/split roundtrip). 325 tests pass offline, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 16:09:47 +02:00
Marius CiepluchandClaude Opus 4.8 95bed7005c Merge unseen-fetch-bulk: full-objects clone, no per-blob lazy fetch
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 15:53:05 +02:00
Marius CiepluchandClaude Opus 4.8 00a1fdb30f unseen: full-objects clone instead of blob:none promisor (bulk pack, no lazy per-blob fetch)
The harvester cloned --filter=blob:none --no-checkout, then blame/log/diff
triggered the promisor to fetch each needed blob ON DEMAND -- hundreds of
round-trips per repo, the dominant cost that made the draw unable to
converge. The blobless clone was a premature optimization that backfired.

Drop --filter=blob:none from the clone and the two commit fetches. A
full-objects --no-checkout clone brings the whole commit graph AND every
blob in one pack, so blame/log/diff read locally; --no-checkout still
skips the working tree so disk stays bounded (plus the existing 500 MiB
clone_size_cap and the disk guard). With no promisor remote, a genuinely
missing object now errors loudly instead of silently round-tripping --
the instrument guarantee that the bulk fetch was complete.

Extraction semantics are unchanged (same SZZ introducing-commit positive,
same stratified ordinary negatives, byte-identical entries); only the
object-transfer strategy changes. Test inverted to assert no blob:none
filter on the clone; README updated. 145 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 15:53:05 +02:00
Marius CiepluchandClaude Opus 4.8 f712b2cffa Merge unseen-parse-deadline: wall-clock deadline for the Python parse phase
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 14:46:18 +02:00
Marius CiepluchandClaude Opus 4.8 a49b15b720 unseen: bound the per-repo Python parse phase (--parse-deadline)
Git subprocess calls were deadline-bounded (Git.command_timeout uses the
remaining per-repo budget), but a pure-Python parse between git calls
(repository_changes -> history/blame/diff on a giant file) had no
deadline: when it hung, no further git call fired the git deadline, so
the worker stalled indefinitely (observed >720s with zero git
processes). An external watchdog was papering over it at ~20 min/stall.

Add extract.process_deadline(seconds): a SIGALRM/setitimer wall-clock
alarm installed on the worker's main thread that raises
ExtractionTimeout("wallclock_timeout"), with no-op guards (None/<=0, off
the main thread, or no SIGALRM support) and strict timer-cancel +
handler-restore in finally. Wrap the whole extract_repository body in it
so the timeout propagates to the existing `except source.Rejected` and
journals as a clean rejection, not a worker failure.

Budget = --extract-deadline (git, 240s) + --parse-deadline (new, 120s)
= 360s total ceiling, so git-bound repos keep their full budget (yield
unchanged) while a hung parse is rejected well before any external
watchdog. Record parse_deadline_seconds in the report.

295 unseen tests pass (incl. timeout + handler-restore + no-op cases),
ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 14:46:18 +02:00
Marius CiepluchandClaude Opus 4.8 c4c08d1ca1 Merge unseen-pool-size: configurable pool size / slices
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 07:54:49 +02:00
Marius CiepluchandClaude Opus 4.8 5ea28ca9f8 unseen: configurable pool size and slice count (--pool-size / --slices)
The draw hardcoded 300 repos / 3 slices. At the measured lazy-fetch rate
the full 300 projects ~2.5 days; a 100-repo single slice (slice 1, all
the G0 baseline needs) is ~20h. Add --pool-size (default 300) and
--slices (default 3), validated (>=1, slices<=pool_size, evenly
divisible). Generalize assign_slices' stratified allocation from a
literal 3 to `slices`, the run() break / insufficient check / report
(changes = pool_size*13) to pool_size, and record both in the journal
inputs. Remove bisect's three-slice assumption; freeze reads the size
from the draft. Defaults (300/3) reproduce prior output exactly.

With --pool-size 100 --slices 1 the pool is the first 100 accepted repos
in seed order, all in slice 1 (deterministic, byte-identical across
worker counts). 280 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 07:54:49 +02:00
Marius CiepluchandClaude Opus 4.8 f0bcbe08b5 Merge unseen-revert-materialize: lazy blob fetch, drop broken prefetch
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 05:06:24 +02:00
Marius CiepluchandClaude Opus 4.8 0023f61ff8 unseen: revert the batched-blob materialize (it failed git fetch on ~half of repos)
Git.materialize enumerated every object reachable from HEAD and tried to
`git fetch --no-filter` them by SHA. Git rejects that for real repos
("did not send all necessary objects" / "bad revision"), exiting 1 ->
InstrumentFailure(git_exit_1) that failed the whole repository. Measured
in a live draw: 326/670 attempts, ~49% of all failures, which froze the
accepted pool at 44/300. Reproduced on pallets/flask (26221 reachable,
9287 missing, fetch exit 1).

Git cannot batch-fetch arbitrary tree/blob objects by SHA, so remove
materialize and restore the original lazy path: the blob:none clone keeps
the commit graph and the introducing/blame/ordinary-history reads fetch
their blobs on demand through Git's promisor. The per-fix tolerant fetch
loop, the process-pool parallelism, the 25-with-zero-entries guard, the
per-repo deadline and resume are unchanged. Extraction content is
byte-identical (only the prefetch is gone).

217 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-10 05:06:24 +02:00
Marius CiepluchandClaude Opus 4.8 40bfdb8e18 Merge unseen-guard-and-gitexit1: guard + tolerant fix-fetch
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 23:54:08 +02:00
Marius CiepluchandClaude Opus 4.8 44311ff2fa unseen: fix systemic-guard miscalibration + the batched-fetch yield regression
Two defects stalled the draw at 40/300:

1. The systemic guard used an absolute `instrument_failures > 200` cap.
   At ~10% extraction yield, reaching 300 entries legitimately produces
   far more than 200 per-candidate failures, and the counter is rebuilt
   from the journal on resume -- so once past 200 the guard tripped
   immediately on every restart (permanent stall; the supervisor's
   no-progress guard then stopped). Drop the absolute cap; keep only
   `>=25 failures with zero entries`, which still catches a genuinely
   broken instrument (it yields no entries) without aborting a working
   low-yield draw. Bounded anyway by the population / max-hours.

2. repository_changes fetched all fixes in one `git fetch origin *fixes`,
   so a single unfetchable secondary fix ref failed the whole repo
   (extract_git_exit_1 was 166 of ~200 extract_ failures -- the dominant
   loss). Fetch the extra advisory fixes per-fix, best-effort, skipping a
   git_exit_ failure; candidate.fix stays strict (already fetched in
   extract_repository). Only resolved fixes are materialized and
   fix-site-indexed.

218 unseen tests pass (incl. 1000 failures after a success not tripping
the guard, and fetch-error regressions), ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 23:54:08 +02:00
Marius CiepluchandClaude Opus 4.8 a9be42b5f8 Merge unseen-extract-ascompleted: saturate the extraction pool
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 20:15:54 +02:00
Marius CiepluchandClaude Opus 4.8 2cf4caa959 unseen: keep the extraction pool saturated (as-completed, not oldest-first)
The ProcessPool spawned its workers but ordered_extractions blocked the
main thread on the OLDEST future, so while one slow repo finished the
other workers sat idle: live profiling showed 1 of 4 cores busy, ~67%
idle, ~1 attempt/min.

Separate the seed-order acceptance cursor (`pending`: row+decision+key,
no futures) from the worker set (`in_flight`: future->key). Keep up to
`workers` jobs running; journal each result as it COMPLETES via
wait(FIRST_COMPLETED) in any order and refill freed slots, never waiting
on the oldest. A candidate is accepted/rejected only when its result is
in the journal, advanced strictly in seed order -- so the pool runs
out of order while the accepted pool stays byte-identical to a 1-worker
run. Fork-network dedup and the systemic guard still apply at the cursor.

New real-process test proves later extractions are journaled before an
earlier slow one completes (completion order != seed order) while
accepted entries stay in seed order; the old loop fails it. 210 unseen
tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 20:15:54 +02:00
Marius CiepluchandClaude Opus 4.8 192b2b7e7c steering: stream run artifacts to RustFS, one bucket per purpose
Local disk is the scarce resource (host near full; the draw stops at
<2G free). Record the direction to stream durable run artifacts to RustFS
instead of hoarding on local disk, split across purpose-specific buckets
(sast-memory durable, sast-pools durable archive, sast-runs transient
with aggressive lifecycle), each with a lifecycle tuned to its artifact
-- the lesson from the engine-queue versioning bloat. Extends the
existing S3Store pattern; new buckets are an operator admin action.
Planned as a follow-up spec, not retrofitted into an in-flight run.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 19:43:26 +02:00
Marius CiepluchandClaude Opus 4.8 1b0b2bdf31 Merge unseen-freeze: freeze p1 (bisect guard, freeze writer, protocol)
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 19:23:26 +02:00
Marius CiepluchandClaude Opus 4.8 197fe8a19c unseen: implement freeze p1 (group 3: bisect guard, freeze writer, protocol)
Group 3 was unimplemented, so there was no way to turn the draft into a
frozen pool. Add:

- bisect_guard.py: stages the cache draft and bisects it against the
  reserved-repository guard, run as a black box
  (pytest -k referenced, stdout/stderr discarded, exit code only; an exit
  other than 0/1 aborts as an instrument failure). Drops guard-tripping
  candidates and reports the dropped COUNT only -- never a name or URL.
  Guard runner is injectable for offline tests.
- freeze.py: writes canonical pool-p1.toml (+ private counterpart,
  reusing draft's public/private split and private_digest) and
  freeze-p1.json (used-set digest, per-source counts, guard result,
  dropped count, label-check counts, freeze digest = sha256 of the
  canonical public manifest). A matching retry is a no-op; any changed
  frozen artifact requires a new pool id (never an overwrite).
- protocol-p1.md: the frozen scoring/statistics protocol from design
  sections 3 and 4.

Offline only; the guard subprocess is the sole subprocess and is mocked
in tests. 42 new tests pass (incl the group-1.3 guard digest check on a
fixture pool); neighbouring unseen suites green; ruff clean. New files
only -- does not touch the journal-pinned draft/extract modules, so a
running draw is unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 19:23:26 +02:00
Marius CiepluchandClaude Opus 4.8 23ede07608 Merge unseen-extract-processes: process-pool extraction for true multicore
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 19:08:01 +02:00
Marius CiepluchandClaude Opus 4.8 79f04400f6 unseen: extract in processes, not threads (GIL was the ceiling)
Extraction is CPU-bound Python (git-output parsing, HUNK regex,
ast.parse of hundreds of source files per repo). A ThreadPoolExecutor
cannot parallelize it: live profiling showed one thread pegging a core
at 99.7% while three cores sat idle and the others waited on the GIL, so
six "workers" barely beat serial (~0.25 entries/min).

Switch ordered_extractions to a ProcessPoolExecutor with a fork context
(copy-on-write memory; lets the existing in-memory test fakes cross into
children without pickling). Effective workers are clamped to the core
count. The parent still owns the journal, REST (finish_license), decide,
counters, the fork-network dedup, strict seed-order acceptance, the
systemic guard, and the graceful drain. Children run extract_repository
with github=None on picklable args and return their own count deltas
(threads had shared a Counter; processes return counts in the result).
A broken pool is replaced; a collateral worker_crash is a skip outside
the guard, a task-specific extract_worker_failure counts toward it.

Determinism is unchanged and tested with REAL processes: workers=6 ==
workers=1 byte-identical (fork collisions, out-of-order completion,
resume). 208 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 19:08:01 +02:00
Marius CiepluchandClaude Opus 4.8 9f078fe7d0 Merge unseen-extract-parallel: concurrent extraction + batched blobs
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 17:37:05 +02:00
Marius CiepluchandClaude Opus 4.8 65714405eb unseen: parallelize extraction + batch per-repo blob fetch
Serial extraction at ~147s/repo made a 300-repo pool take days, and
~626 lazy per-file blob faults per clone pushed slow repos past the
240s deadline (a draw reached 21 entries from 347 attempts, 160 of them
extraction_timeout). Both fixed.

Parallel: a bounded ThreadPoolExecutor (--extract-workers, default 6)
runs clones concurrently, each into its own clone with a private counter.
The main thread alone owns the journal, REST (license via finish_license),
counters, and acceptance. Entries are accepted in STRICT SEED ORDER with
the fork-network dedup applied at acceptance, so the pool is byte-
identical to the serial draw (tested: workers=6 == workers=1, including
fork collisions, out-of-order completion, and resume). The systemic guard
and resilient skips are unchanged; a graceful stop drains started work to
the journal for resume.

Batched blobs: repository_changes fetches the fixes together and
Git.materialize fetches all missing blobs for the read revisions in one
`fetch --no-filter --stdin` (rev-list --missing=print), instead of per-
file promisor faults. Content is byte-identical (tested; >=12 fetches
collapsed to 1).

202 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 17:37:05 +02:00
Marius CiepluchandClaude Opus 4.8 4f7eedddf1 Merge search-executor-command-once: upload command once per task
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 14:03:30 +02:00
Marius CiepluchandClaude Opus 4.8 b9f7958414 search: upload the executor command once, not on every poll
ObjectStoreExecutor.submit re-PUT the identical command.json on every
0.2s result poll, so a task running minutes wrote ~1,500 versions of one
engine-queue/search-exec/<id>/command.json. On the versioned memory
bucket that was the dominant growth: 109,715 versions, 10.3 GB (91% of
the bucket). retry_mailbox already retries a lost PUT acknowledgement
internally, so one upload per task is the correct delivery guarantee.

Guard the PUT with an `uploaded` flag: upload once (still retried on
transient failure), then poll only the result. One command.json version
per task instead of ~1,500. All submit semantics preserved.

59 executor tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 14:03:30 +02:00
Marius CiepluchandClaude Opus 4.8 6773c63634 steering: operator Discord notifications for OpenUltraSAST process status
Record the policy: OpenUltraSAST notifies the system/AI operator on
Discord about its own processes and results, and especially when a
process is stuck or impaired (stall, insufficient result, abort,
instrument_failure, timeout, budget/resource limit). Operator-facing,
not developer debugging, and explicitly not k3s/ax infrastructure.

Credentials by name in gitignored .env (DISCORD_WEBHOOK_URL,
DISCORD_OPERATOR_ID), listed blank in .env.example; off when unset.
Messages carry status/counts/digests only, under the hardening and
measurement-record redaction (no payloads, secrets, or repo content).
Notifier implementation to be wired/specced; this states the behavior.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-09 11:58:10 +02:00
Marius CiepluchandClaude Opus 4.8 aae586de69 unseen: decide phase skips a per-candidate HTTP failure too
The resilient draw only covered extraction. A per-candidate transport
failure in the decide/eligibility phase -- get_commit on an unresolvable
fix ref raising ValueError("http_failure") (observed: ljharb/shell-quote
commit 1518179) -- still aborted the whole draft, because decide only
caught source.Rejected and eligibility.RepositoryNotFound.

decide now catches that http_failure and returns a "decide_http_failure"
skip, and the run loop feeds decide + extract per-candidate instrument
failures into one systemic guard (25 with zero successes, or >200), so a
single bad commit lookup skips while a broken instrument (e.g. a network
outage failing every lookup) still aborts loudly.

Full unseen draft suite passes (48), ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 18:54:46 +02:00
Marius CiepluchandClaude Opus 4.8 1f89f04fcf Merge unseen-resilient-draw: skip bad repos, abort on systemic failure
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 17:19:36 +02:00
Marius CiepluchandClaude Opus 4.8 b59dd651d8 unseen: resilient draw -- a bad repo skips, a broken instrument aborts
Individual repositories in the advisory population raise per-candidate
extract.InstrumentFailure during extraction (git_exit_128 on an
unfetchable fix commit, clone_size_cap, and others), and each one
aborted the whole 300-repo draft. The draw is healthy; one bad repo
should not reset it.

extract_repository now also catches extract.InstrumentFailure and
OSError and records them as "extract_<reason>" rejections, so the draw
skips that repo and continues (the reason, e.g. extract_git_exit_128,
shows in the rejections counter). ExtractionTimeout keeps its existing
extraction_timeout skip; any other exception still propagates as a real
abort, so genuine code bugs are not masked.

A systemic guard preserves the instrument rule: run() raises
InstrumentFailure("systemic_extraction_failure") if 25 extraction
attempts fail with zero successes (e.g. git missing, auth broken) or if
extraction failures exceed 200 overall. A handful of bad repos with many
successes never trips it.

190 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 17:19:36 +02:00
Marius CiepluchandClaude Opus 4.8 ec7bb64378 Merge unseen-extract-deadline: per-repo extraction time bound
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 13:22:23 +02:00
Marius CiepluchandClaude Opus 4.8 b074cf7f89 unseen: bound per-repo extraction time; a slow repo skips, never aborts
A pathologically slow repository (observed: tensorzero, 183s to clone,
then `git blame -C` past the 180s command limit) raised
subprocess.TimeoutExpired, which Git.run mislabeled as a fatal
InstrumentFailure("git_launch_failed") and aborted the whole 300-repo
draft. The same slowness stalled throughput (a 54-minute no-progress
gap).

Give each clone a per-repo wall-clock budget (--extract-deadline,
default 240s, plumbed Clones -> Git, counting the initial clone). A
timeout -- a command exceeding the remaining budget, or the budget
elapsing -- raises ExtractionTimeout(Rejected), which the existing
rejection handler turns into an "extraction_timeout" skip so the draw
moves to the next candidate. Genuine launch failures and non-zero git
exits stay fatal; timeouts and launch failures are now distinct. The
deadline is recorded on the report only, not the journal inputs, so
changing it does not refuse resume.

181 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 13:22:22 +02:00
Marius CiepluchandClaude Opus 4.8 23c8644a37 docs: README reflects the dynamic proof arm and unseen-repo release gate
Update the front door and Status to the current state: static
abstract-interpretation blocking is NO-GO by hand, so the work is a
dynamic proof arm that builds and runs the app and demonstrates a
finding by a differential (triggers on the vulnerable revision, not the
fixed one), earning a block by execution rather than calibration. It is
pilot, not shipped. The release gate is a measured catch/false-alarm
number on unseen repositories, now being produced. Bump v2.0.0 -> v2.0.1,
link the developer guides.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 12:51:35 +02:00
Marius CiepluchandClaude Opus 4.8 f4c7143525 unseen: make clone disk-size guard tolerate git's mid-walk churn
disk_bytes() summed the clone tree with rglob + stat() as a size cap
before every git command. On a blob:none clone, `git show` triggers an
on-demand blob fetch that writes and deletes transient
.git/objects/pack/*.rev files, so a path rglob had just listed could be
gone by the stat() call -> FileNotFoundError aborted the whole draft as
an instrument_failure. The race can hit any candidate, so it was a
non-deterministic draft-killer (observed extracting a real npm repo).

Skip a file that raises OSError mid-walk; the size cap is unaffected.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 11:16:13 +02:00
Marius CiepluchandClaude Opus 4.8 0a637e8989 Merge unseen-cursor-pagination: follow Link rel=next cursors
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 10:26:11 +02:00
Marius CiepluchandClaude Opus 4.8 5620165540 unseen: cursor pagination for advisory enumeration
The GitHub /advisories endpoint ignores the `page` query parameter: it
paginates with the Link header's rel="next" cursor. The page-number
pager re-fetched the same first 100 rows forever and never terminated,
so enumeration never completed and no draft converged (a run fetched
5137 "pip pages" holding only 100 unique advisories).

Surface the successful response headers from JSONTransport, and walk the
rel="next" cursor per ecosystem until it is absent. The ecosystem /
published>=2019 / bare-number cwes filters are preserved on the first
request. Journal keys stay ecosystem:n (now cursor-walk positions) with
the saved next URL for resume; journal_version 4 refuses older journals.

Live smoke via the production pager: 300 unique advisories pulled in 3
API calls, advancing past the first page. Offline: 170 unseen tests
pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 10:26:11 +02:00
Marius CiepluchandClaude Opus 4.8 2d8f4f9e22 Merge unseen-sourcing-filter: server-side advisory filtering
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 08:19:30 +02:00
Marius CiepluchandClaude Opus 4.8 90e2400e6f unseen: filter advisory enumeration server-side (ecosystem/date/cwes)
enumerate_advisories paged the entire global GitHub reviewed-advisory
database (every ecosystem, every year, every CWE) before extraction, so
a 300-repo draft never converged -- a live run reached 21,962 advisories
in 4.5h with extraction never started.

Push the existing candidate() rejections into the query: page per
ecosystem, published>=2019-01-01, and cwes restricted to the seven
families. The cwes filter uses bare CWE numbers (89), which is what the
GitHub advisories API matches -- "CWE-89" silently returns zero. The
whole cwes filter is omitted if the taxonomy ever holds a non-exact
token, so a selectable advisory is never dropped. Journal version 3
(ecosystem:page keys) refuses incompatible older journals.

Live page-1 smoke (pip, npm): 100/100 rows carry a family CWE, none
published before 2019. Offline: 166 unseen tests pass, ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-08 08:19:30 +02:00
Marius CiepluchandClaude Opus 4.8 d7bf0ba750 unseen: sourcing robustness -- retry transient HTTP/connection errors, shrink the advisory journal
A live G0 sourcing run failed on both: a transient connection/decode error aborted
the whole enumeration (the bare except raised http_failure on the first blip), and
the advisory journal stored full raw advisory JSON and reached 1.77 GB on a near-full
host. Fix: the JSON transport now retries URLError/timeout/ConnectionError/IncompleteRead/
JSONDecodeError through the same attempt-4 backoff as HTTP 5xx/429 (fatal only after the
cap or on a genuinely non-retryable error); enumerate_advisories journals only the fields
resolution/extraction consume via source.reduce_advisory (ghsa id, published/withdrawn,
type, CWE ids, package ecosystem/name for OSV, fix-commit links), with journal_version
bumped to 2 so an old full-format journal is rejected and resume-skip stays intact.

Offline: test_unseen_source/draft/extract 133 passed; ruff + format clean; mypy unchanged
(pre-existing verify.py lambda only).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 18:23:40 +02:00
Marius CiepluchandClaude Opus 4.8 56cde9f535 spec(proof-arm-opencode): OpenCode MCP is the endpoint for PoC AND fix [skip ci]
Maintainer direction: the same agentic endpoint authors the PoC, writes a candidate
fix against the repair direction + failing PoC, and hands it to the differential
verifier, which re-runs the PoC and confirms absence. Closes the no-auto-patch gap:
the proof is the acceptance test for the fix (a wrong fix is caught), the fix stays a
developer-reviewed proposal, and the green proof is the objective signal. Specced
alongside proof authoring, after qualification.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 17:34:52 +02:00
Marius CiepluchandClaude Opus 4.8 7f8a492857 docs: use-case reference -- catch, prove, fix, re-verify absence (+ GitHub Actions example) [skip ci]
The loop: catch a regression in the diff, prove it with a reproducible check,
the dev/agent fixes it (tool gives a repair direction + the failing proof, not an
auto-patch), then the same proof re-runs and no longer triggers = absence
demonstrated by execution; the proof becomes a committed regression test. Example
GitHub Actions workflow uses the real 'ousast pre-push . --base --head --deadline
--mode --artifact' entry; PR-comment formatter marked as a placeholder. Honest
status separates what exists (re-verify by construction, --base/--head) from
delivery work (PR formatter, auto-landing the regression test) and the open
large-repo latency.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 17:34:06 +02:00
Marius CiepluchandClaude Opus 4.8 dfc0956086 docs: frame the proof as prioritization evidence for busy devs [skip ci]
Revise the developer guide for engineers with full backlogs and no spare schedule:
a vague warning dies in the backlog, so the value of the demonstrated proof is the
prioritization conversation -- reproducible evidence (input, behavior, the one-line
fix difference) a dev takes to a product owner or the business to win capacity,
dropped into the ticket system as a location, repair direction, and reproduction.
mcp-shift-left-agent brief: the agent's return should be ticket-ready evidence.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 17:30:14 +02:00
Marius CiepluchandClaude Opus 4.8 c49f334651 docs: developer-facing pre-push security guide + mcp-shift-left-agent brief [skip ci]
docs/pre-push-for-developers.md: COBESY-structured guide for engineers who are
strong at dev but not security. Leads with the conclusion (zero-setup, no
annotations, diff-scoped, proof-backed), frames the agent-era case (a pre-push
check is how you oversee coding agents), states control (advisory vs blocking,
budget, lane, models), and is honest about limits with an explicit disproof
condition (coverage, large-repo latency, generalization being measured by G0).

.kiro/specs/mcp-shift-left-agent/: follow-up brief for an MCP agent + skills that
give devs and coding agents early shift-left feedback and a required pre-push
security step for agent oversight; builds on the existing ousast mcp server;
sequenced after the safety net is qualified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 17:28:49 +02:00
Marius CiepluchandClaude Opus 4.8 7bfe6fff7e merge(search-with-proof): verification-architecture doc + proof-arm-opencode brief [skip ci]
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 16:06:56 +02:00
Marius CiepluchandClaude Opus 4.8 60617a92ba docs(search-with-proof): verification architecture + proof-arm-opencode follow-up brief
verification-architecture.md documents the existing two-stage design: the Cairn-inspired
search (brain + model calls on the VM) and the DAST proof (gVisor AX tasks in kube-ax),
the trust boundary (secrets VM-only, cluster tasks secret-free with restricted egress),
and the three verify lanes (ax=cluster default, vm=local Bubblewrap for $0 proofs,
docker=replay). Clarifies the proof is dynamic, not abstract interpretation: AI triage
stays the static backbone, proof earns the block by execution.

proof-arm-opencode/ is a follow-up brief: drive proof-authoring with OpenCode MCP, an
abliterated/GLM proof model (DeepSeek stays the search model), DSPy-optimized from
OpenObserve traces, to beat the safety-classifier refusals and nondeterministic wiring.
Flags the trust-domain decision that must come first (A: scoped key + egress inside the
cluster; B: brain on VM, only DAST in gVisor — recommended to start). Credentials live in
.env by name only (ABLITERATION_*, OPENOBSERVE_*); no secret value is in any tracked file.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 16:06:56 +02:00
Marius CiepluchandClaude Opus 4.8 b1a050e16b merge(search-with-proof): reproducible npm builds via --before commit date [skip ci]
Lockless old npm projects resolve era-appropriate deps (fixes glance filed->mime@4 crash);
npm ci untouched; git-failure falls back to plain install.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 13:37:23 +02:00
Marius CiepluchandClaude Opus 4.8 c30dd9126a search-with-proof: reproducible npm builds for old lockless projects (--before commit date)
A lockless npm project (e.g. glance, 2015) resolved an unpinned transitive range to the
latest major ([email protected] -> mime@4, which dropped .lookup()), so the built app crashed
serving files -- a build-reproducibility defect, not an app or agent fault. The executor
now, for the npm-install (no-lock) case only and only when .git is present, resolves
dependencies as of the checkout's HEAD commit date via `npm install --before <date>`,
giving era-appropriate, compatible versions. npm ci (lockfile present) is untouched, and
any git/date failure falls back to plain install. demo.py also recognizes
npm-shrinkwrap.json as a lock for the ci-vs-install choice.

Tests: executor + smoke 75 passed, demo 107 passed; ruff clean; mypy clean on executor.py.
Executor-side change -> image rebuild required.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 13:37:23 +02:00
Marius CiepluchandClaude Opus 4.8 8de0c2732b spec+docs(search-with-proof): record first cluster demonstration + fixed-side negative [skip ci]
Feed the milestone back into the spec and steering. HANDOFF: the open "first demonstrated"
question is RESOLVED -- xiaomusic (path, prefix-check-bypass) demonstrated on the vulnerable
side (3/3 affected, 0/3 safe) and the blind fixed-side search produced 0 false proofs; pair
total $1.22. Records the six-fix chain that enabled it (parity, verify-time-only, fair-share
budget, served-root guidance, oracle prefix-bypass coverage, control-signal feedback) plus the
keystone. tasks.md: implementation note; pilot (6.2/6.3) in progress. spec.json: pilot status.
safety-net steering: proof-based BLOCK mechanism reached first execution-backed demonstration
(feasibility-pilot scope, not release readiness; pilot exit + 73-negative qualification open;
classifier stays triage). search README: status note. Payload-free; demos/boards stay gitignored.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 13:25:52 +02:00
Marius CiepluchandClaude Opus 4.8 ecd1372568 measurements: FIRST cluster demonstration (xiaomusic prefix-bypass path traversal) [skip ci]
search-with-proof produced its first demonstrated finding on a real CVE fix-pair end to
end on kube-ax: vuln observes, fixed does not, 0 false proofs, $0.37, 55 calls. Enabled
by the full session fix chain (parity, verify-time-only, fair-share budget, served-root
guidance, oracle prefix-bypass coverage, control-signal feedback). Payload-free record.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 12:41:01 +02:00
Marius CiepluchandClaude Opus 4.8 1ec4ee4c55 merge(search-with-proof): served-root control-signal feedback to the agent [skip ci]
Non-blocking advisory hint when the control shows the app didn't serve ${served_root},
so the agent self-corrects its served-directory wiring. Host-side; verdicts unchanged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 12:03:45 +02:00
Marius CiepluchandClaude Opus 4.8 20c5742d84 search-with-proof: feed the control signal back to the agent (served-root hint)
When a path demo does not serve ${served_root}, the keystone control fails
(SideRecord.control_observed False) but the search only reported a generic
inconclusive, so the agent kept making the same wiring mistake. The pilot now
surfaces the affected side's control_observed in the verify result, and the
coordinator appends one advisory, deduplicated "served-root" hint (visible in the
board snapshot the agent reads) telling it to set the served-directory config to the
${served_root} placeholder. Advisory only: verdicts, _end, the oracle, the differential
and the demo validator are unchanged; the hint fires only when control_observed is False.

Tests: coordinator + pilot_run suites 78 passed; ruff clean; mypy unchanged (pre-existing
benchmarks errors only). Host-side change, no image rebuild.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 12:03:45 +02:00
Marius CiepluchandClaude Opus 4.8 d1fe78987b measurements: xiaomusic vuln search -- oracle fix landed (agent targets prefix-sibling) but served-dir wiring missed this run [skip ci]
Oracle broadening works in the agent: demo now targets the prefix-sibling vector. Still
inconclusive (0 false proofs, $0.29): the agent set the served-dir env var to its own
value, not ${served_root}, so the sibling canary was outside the served tree. Served-root
wiring is nondeterministic (a prior run wired it). Last-mile agent-reliability gap.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 11:52:19 +02:00
Marius CiepluchandClaude Opus 4.8 84509fbdc0 merge(search-with-proof): path oracle covers prefix-check-bypass class [skip ci]
Plant a sibling canary so prefix-bypass path traversals (startswith without separator)
are demonstrable; parent canary + differential unchanged; glance still demonstrates.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 11:24:31 +02:00
Marius CiepluchandClaude Opus 4.8 6c7d6bb1d9 search-with-proof: path oracle covers the prefix-check-bypass class (sibling canary)
The path oracle only modelled classic parent traversal (a `canary` file in the parent of
${served_root}). A common real path-traversal class is the prefix-check bypass:
startswith(base) without a trailing separator, which the fix closes with base + os.sep;
it reaches a SIBLING whose path string-extends the served directory, not the parent.
PathOracle now also plants a sibling canary `public_leak` beside the served `public`
(same nonce, 0600), so a prefix-bypass demo leaks it on the vulnerable side and the fix
blocks it. ORACLE_GUIDE names the second target and tells the agent to pick the vector
its code shows. One shared nonce stays correct: a canary leaks only when the demo's own
request reaches it, and each demo targets one vector whose fix blocks it on the fixed side.

observe()/capture()/the differential/SideRecord are UNCHANGED. Local glance ceiling proof
still demonstrates (parent canary, 3/3 vuln, 0/3 fixed, control on both sides). Gate:
worker+control+verify suites 106 passed / 4 socket-skips; ruff clean; mypy clean on oracles.py.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 11:24:31 +02:00
Marius CiepluchandClaude Opus 4.8 4c356caa21 measurements: py re-validation 2 -- served-root fix works (demo now wires ${served_root}); still inconclusive, budget-bound [skip ci]
Served-root guidance validated: authored demo now references ${served_root}. Still
inconclusive (0 false proofs); agent spread budget across vectors and hit run_ceiling
(164 calls, $1.47/$1.50) without converging, and the committed traversal did not
differential. Remaining = per-app exploit-authoring + convergence (the layer the $0 local
glance proof did by hand).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 11:05:07 +02:00
Marius CiepluchandClaude Opus 4.8 ec09a39deb merge(search-with-proof): path demo must serve ${served_root} [skip ci]
Agent guidance: point the app's served directory at the ${served_root} placeholder via
arg/env/config so the planted observable is reachable. Prompt strings only; 37 worker tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 10:29:40 +02:00
Marius CiepluchandClaude Opus 4.8 00d1c26f5f search-with-proof: require the path demo to serve ${served_root} (via arg/env/config)
Path searches authored and verified many demos but all came back with no differential:
the demo never pointed the app's served directory at ${served_root}, so the observable
(which lives relative to ${served_root}) was outside the served tree and unreachable. The
agent had set an app env var to a value of its own; the guide only showed the --dir form.

ORACLE_GUIDE now states the agent MUST set the app's served/root to the ${served_root}
placeholder via whichever mechanism the app exposes (command-line argument, environment
variable, or config written to start.files), not only --dir, and that serving elsewhere
makes the observable unreachable. Verify-time-only clarification, observable names, and all
prohibitions are kept. Prompt strings only.

Tests: tests/test_search_worker.py 37 passed; ruff clean.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 10:29:40 +02:00
Marius CiepluchandClaude Opus 4.8 e387ca16a2 measurements: py re-validation -- budget fix works (18 verify tasks, 0 before); demo didn't wire served_root [skip ci]
Fair-share budget validated: the Python agent completed the full loop and verified 18
demos within $0.74 of $1.50 (0 verify tasks before). Still inconclusive, 0 false proofs.
Root cause of no-differential: the authored demo never references ${served_root}; the
agent pointed the app's served dir at its own value, so the observable was outside the
served tree. Next: guidance to wire the served dir to ${served_root} for the path oracle.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 09:23:33 +02:00
Marius CiepluchandClaude Opus 4.8 f3ce5be8c4 merge(search-with-proof): per-search fair-share pilot budget [skip ci]
Each pilot search gets a fair share of --ceiling-usd with settled rollover, so one
expensive search can no longer starve the others; total stays within the ceiling.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 08:35:24 +02:00
Marius CiepluchandClaude Opus 4.8 2c0c45fd1f search-with-proof: per-search fair-share pilot budget (stop one search starving the rest)
--ceiling-usd was one shared SpendBudget passed to every search, so an expensive
search drained the pool and starved later ones: in the last 2-pair/$2 run the first
search spent $1.60 and the second hit the ceiling at $0.34 after finishing its analysis.
The driver now allocates a dynamic fair share per search (remaining_ceiling /
searches_not_yet_started), rolls unused settled budget forward, caps each search at its
share, and keeps total settled <= ceiling. A search now gets room to author and verify
after it finds the path.

Driver-only change (benchmarks/search/pilot_run.py) plus tests. tests/test_search_pilot_run.py
39 passed; ruff clean; mypy unchanged (pre-existing benchmarks errors only).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 08:35:24 +02:00
Marius CiepluchandClaude Opus 4.8 e01fb513f2 measurements: validation of explore-trap fix (ts now authors a demo; py analysis complete but out of budget) [skip ci]
2 path searches (ts/py), vulnerable, cluster, $1.95 of $2. The trap fix is validated
behaviourally: ts pair 9 now authors a demo (verify_tasks 0->2; fails could_not_build on
entrypoint), py pair 13 drops 118->36 calls and its board shows a complete route-to-sink
+ entrypoint trace but hits the per-run budget ceiling before authoring/verifying. Still
0 demonstrations, 0 false proofs. Payload-free aggregate; raw stays gitignored. Next
levers: per-run budget/step allocation for demo-write+verify after analysis; demo
entrypoint/build (start.path) for compiled/served apps.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 08:28:37 +02:00
Marius CiepluchandClaude Opus 4.8 e8e6637833 merge(search-with-proof): verify-time-only observable guidance (fix explore-time reachability trap) [skip ci]
Agent guidance so path searches stop trying to confirm the oracle observable during
exploration (where it is absent) and instead author a demo for the differential verifier.
Prompt strings only; validators untouched; 37 worker tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 07:43:29 +02:00
Marius CiepluchandClaude Opus 4.8 563d006c77 search-with-proof: tell the agent the oracle observable is verify-time-only (fix the explore-time reachability trap)
The parity fix named each oracle's observable, but path searches then spent their
whole budget without authoring a demo: the agent tried to confirm reachability during
exploration, where the oracle fixture does not exist (it is injected only in the
verifier's sandbox at verify time), concluded "unreachable", and never committed a demo.

ORACLE_GUIDE now states the fixture/observable is injected at VERIFY time only and is
absent during exploration; its explore-time absence never means unreachable or justifies
ending the search. The agent confirms the mechanism (sink reachability, access to data the
app should not expose) from app code and its own sandbox test data, targets the observable's
known verify-time location in the demo, and relies on differential verification as the only
proof. All prior prohibitions (no out-of-band/off-disk access, no input capture, no revision
sniffing) and the parity observable names are kept. Adjacent wording shortened to fit.

Tests: tests/test_search_worker.py 37 passed; ruff clean; mypy unchanged (pre-existing
verify.py lambda note only). Prompt strings only; validators untouched.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-07 07:43:13 +02:00
Marius CiepluchandClaude Opus 4.8 7c220307d4 measurements: path-family pilot after keystone + target-parity (0 demos, parity validated) [skip ci]
3 path searches (php/ts/py), vulnerable side, cluster verify lane, $2.95 of $3.
Payload-free aggregate; raw boards/demos stay gitignored. Finding: the parity fix
produced directed, target-aware search (the agent reasons about reaching the named
observable and inspects the app's serving guard), but no demonstration landed -- all
three spent their per-pair allowance before a differential, and two wrote zero verify
demos (budget consumed in reason/explore). Next levers: allowance/budget split toward
demo-writing, entrypoint discovery for php, and the explore-vs-verify path layout the
py agent flagged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-06 19:35:42 +02:00
Marius CiepluchandClaude Opus 4.8 dc307c7d2e search-with-proof: path-oracle agent guidance to parity with command/ssrf (name the observable)
The path family's agent guidance named no target, while command (${marker_dir}/marker)
and ssrf (${callback_url}) disclose theirs, so path searches could not author a demo
that surfaces the observable through the app. ORACLE_GUIDE now names it (canary in the
parent of ${served_root}, matching PathOracle.prepare). The reason/explore constraints
are refined to forbid only out-of-band access (direct disk reads, non-app channels),
input capture, and revision sniffing, while allowing the app to disclose it through the
vulnerability under test. Prompt strings only; the demo validator and its _PRIVATE guard
are untouched, so direct references to verifier paths remain blocked.

Tests: tests/test_search_worker.py 37 passed; ruff clean; mypy unchanged (pre-existing
verify.py lambda note only).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-06 18:06:00 +02:00
Marius CiepluchandClaude Fable 5.1 70553cb7c6 measurements: first search-with-proof demonstrated (glance ceiling proof, $0 local) [skip ci]
Outcome-only record of the first end-to-end demonstrated verdict and the two
masking defects (regenerated npm lock pinning an incompatible mime; agent demo
targeting the wrong file). Payloads stay in the gitignored private area.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 17:54:47 +02:00
Marius CiepluchandClaude Fable 5.1 de2771997a merge(search-with-proof): positive control for path oracle [skip ci]
Honest serve signal for the HTTP verifier: SideRecord.control_observed records
whether a benign verifier-planted in-root marker came back through the app's
interface, as its own guarded app start that never changes the verdict. A
2xx-based block was rejected (any HTTP status counts as alive). Gate green;
ceiling re-proof on glance still demonstrated with control_observed on both
sides. The search task image needs a rebuild before the next cluster pilot.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 17:54:47 +02:00
Marius CiepluchandClaude Opus 4.8 328406ad41 search-with-proof: positive control for path oracle — honest serve signal, no false negatives
The HTTP verifier could not distinguish a mis-wired or non-serving app from a
genuine no-effect: both yielded `inconclusive`. This adds a positive control for
the path oracle.

- PathOracle plants a benign in-root marker file (random token from os.urandom,
  distinct from the secret canary) and exposes it via a new Oracle.control()
  hook (default None; only the path oracle implements it today).
- On the first run of an HTTP side, _side runs a benign GET for that marker as
  its own fully-guarded app start. The marker returning proves the interface
  serves the fixture; its absence — or any control failure — is recorded in the
  new SideRecord.control_observed field but never changes the verdict, since a
  valid app may answer only its own routes. A crash is still caught by the
  existing could_not_run path.

A 2xx-based "nothing served" block was rejected: the verifier deliberately
treats any HTTP status as the app being alive (a 503-only app is a valid
observed side), so status-based blocking would false-fail valid apps. Soundly
converting a mis-wired-but-responding app into could_not_run requires a
demo-declared control, which is a separate follow-up.

Tests: new tests/test_search_control.py covers marker observed, absent marker
not blocking, control failure not blocking a working attack, and crash still
could_not_run. Full search-verify/probe/worker suites pass; ruff clean; mypy
unchanged (only the pre-existing file_bindings lambda note).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-06 17:07:24 +02:00
Marius CiepluchandClaude Opus 4.8 d17ceb0e0c spec(search-with-proof): handoff -- plumbing cleared, open question is demo attack step vs oracle capture [skip ci]
Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-10-06 15:14:45 +02:00
Marius CiepluchandClaude Fable 5.1 918b1f5eec tests: a timed-out AX attempt may carry its reason instead of None [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 14:51:07 +02:00
Marius CiepluchandClaude Fable 5.1 af6a68691c merge(search-with-proof): demo replay tool [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 14:50:14 +02:00
Marius CiepluchandClaude Fable 5.1 eb05ed18da search-with-proof: no-model demo replay (executor + verifier on a stored demo)
Replays a stored demo through the executor task and the verifier lane for one
side or both, with per-phase timings, so verifier changes are checked on the
exact image before any paid search.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 14:50:14 +02:00
Marius CiepluchandClaude Fable 5.1 cc2d0b3d1f merge(search-with-proof): one interpreter, sys.path root, node_modules path [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 14:40:15 +02:00
Marius CiepluchandClaude Fable 5.1 630603ee28 search-with-proof: one interpreter for pip and apps; checkout root on sys.path; real node_modules path
The pip recipe ran under the system Python 3.13 while apps started under the
venv 3.12, so compiled extensions never loaded. Both now use sys.executable.
Package-style starts see the checkout root; NODE_PATH lists the project
node_modules under the checkout before the products mount.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 14:37:58 +02:00
Marius CiepluchandClaude Fable 5.1 b227e74da8 merge(search-with-proof): oracle placeholders and venv interpreter [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 12:43:11 +02:00
Marius CiepluchandClaude Fable 5.1 7a91cc1482 search-with-proof: oracle placeholders for demos; Python apps run under the venv interpreter
Demos may reference ${served_root}, ${database_path}, ${marker_dir} or
${callback_url}; the verifier substitutes them per side, so apps that take a
directory or database argument can be wired to the oracle. Python apps start
under the verifier interpreter (the one pip built the products for): the image
carries a second system Python whose ABI tag broke compiled extensions.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 12:43:11 +02:00
Marius CiepluchandClaude Fable 5.1 dcbaec0043 merge(search-with-proof): archive parts, isolated pip, verifier module paths [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 11:15:09 +02:00
Marius CiepluchandClaude Fable 5.1 0d3e6df31c search-with-proof: split compressed verifier archives, isolated pip installs, verifier module paths
Archives are gzipped and uploaded in parts of at most 90 MB with per-part
digests (edge request limit); pip installs ignore the task venv so transitive
dependencies land in products; the verifier sets NODE_PATH and the Java
classpath from products when starting the app.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 11:15:09 +02:00
Marius Ciepluch c9bd1ec152 merge(search-with-proof): readiness probe, longer tool turns, 40-step tasks [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	benchmarks/ax/search_verify.py
#	benchmarks/search/pilot_run.py
2026-10-06 09:41:57 +02:00
Marius CiepluchandClaude Fable 5.1 da804472b9 search-with-proof: readiness from the budget with a probe, 4096-token tool turns, 40-step tasks
HTTP start waits up to 90 s (CLI 30 s) with TCP then HTTP probing; app stdout
and stderr tails are recorded; tool-call turns get 4096 output tokens with one
retry on truncation; tasks get 40 steps with reminders.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 09:37:47 +02:00
Marius CiepluchandClaude Fable 5.1 a4ec526039 tests: verify task identities are plain ids; the prefix is the workload kind [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 08:21:37 +02:00
Marius CiepluchandClaude Fable 5.1 b1024b8daa ax: verify and probe tasks carry the search-verify/search-probe name prefixes [skip ci]
The kind default "pin" had produced ousast-engine-pin-search-verify-* names,
which the operator per-host egress rules do not match.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 08:20:40 +02:00
Marius CiepluchandClaude Fable 5.1 72ab4c8925 search-with-proof: 1 GiB spooled verifier archive, tolerant text replies, transport retries
Built projects spool to /workspace without package caches (cap 1 GiB, both
sides); a plain-text model reply is fed back once; mailbox PUT/GET retry for up
to 90 s with idempotent re-send by seq; executor tasks are deleted on every exit
path.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 08:18:34 +02:00
Marius CiepluchandClaude Fable 5.1 aebc834667 merge(search-with-proof): archive, text replies, transport retries [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 08:18:34 +02:00
Marius CiepluchandClaude Fable 5.1 dae05a7c1a merge(search-with-proof): demo startup configuration and loop fixes [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 00:25:57 +02:00
Marius Ciepluch 30cbc1ac76 Merge branch 'main' into search-with-proof 2026-10-06 00:24:26 +02:00
Marius CiepluchandClaude Fable 5.1 ceeaa66717 search-with-proof: startup environment and data files, dead-end dedupe, task_spend continues
Demos may set a bounded environment (loader variables denied) and up to 8 small
data files under .demo/; the reason step sees conclusions and may not re-propose
them; a task spend limit concludes its intent, not the search; finish wins over
stray tool calls.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 00:24:26 +02:00
Marius CiepluchandClaude Fable 5.1 263be30979 search-with-proof 3.2: oracle probe passes on kube-ax in task-boundary mode [skip ci]
All five oracles demonstrated, gaming demos inconclusive, 38 s, ~500 MB RSS.
The probe dispatcher accepts a skipped isolation check in task-boundary mode.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-06 00:14:51 +02:00
Marius CiepluchandClaude Fable 5.1 81cb02cd35 merge(search-with-proof): build recipes and stderr-bearing failures [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:39:39 +02:00
Marius Ciepluch 67eb629c16 Merge branch 'main' into search-with-proof 2026-10-05 21:37:56 +02:00
Marius CiepluchandClaude Fable 5.1 3866e2771b search-with-proof: ecosystem-aware build recipes and stderr in build failures
The agent passes a manifest file or project directory; pip uses -r for
requirements files and the directory for pyproject/setup, npm ci/install runs in
the directory, composer/maven/gradle likewise. Build and verify failures carry
phase, exit code and a sanitised stderr tail.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:37:56 +02:00
Marius CiepluchandClaude Fable 5.1 13a20008e7 merge(probe-task-boundary): probe runs oracle self-tests in task-boundary mode [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:17:00 +02:00
Marius Ciepluch 007415c151 Merge branch 'main' into probe-task-boundary
# Conflicts:
#	src/openultrasast/search/probe.py
2026-10-05 21:14:55 +02:00
Marius CiepluchandClaude Fable 5.1 2d3c94e33b search probe: task-boundary mode runs the oracle self-tests in-process
On kube-ax the probe detected task-boundary and then failed the bwrap preflight it
no longer needs; now it skips the preflight, runs every oracle pair through the
verifier path a task uses, and reports ok only if every real demo is
demonstrated and no gaming demo is. Fixes a double fixture-path translation.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:11:26 +02:00
Marius CiepluchandClaude Fable 5.1 a40122d1b5 merge(search-with-proof): oracle mapping, build memory, tolerant tool calls [skip ci]
Image rebuild dispatched by hand (code in the last layer, ~10 MB).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:09:08 +02:00
Marius CiepluchandClaude Fable 5.1 bf243e6680 search-with-proof: family-to-oracle mapping, build memory, tolerant tool calls
Demos declare their oracle within the family allowed set (injection: sql or
command; output_encoding: xss; untrusted_destination: ssrf; path: path);
builds and app starts use the search memory budget instead of the 256 MB tool
limit (npm died with SIGTRAP); malformed tool-call JSON is fed back once.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:07:58 +02:00
Marius CiepluchandClaude Fable 5.1 0750c6fdd7 merge(search-with-proof): named exhaustion limits and partial evidence [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 21:01:31 +02:00
Marius Ciepluch c190d34f60 Merge branch 'main' into search-with-proof
# Conflicts:
#	tests/test_search_task_storage.py
2026-10-05 20:58:10 +02:00
Marius CiepluchandClaude Fable 5.1 c33b053b15 search-with-proof: name the exhausted limit, keep partial evidence, tighter reservation
budget_spent carries end_detail (spend, task_spend, reason_rounds, ...); a task
whose allowance cannot cover the next call finishes with its facts; per-task
metrics in pilot records; input reservation bytes/2 + 1024 per message (measured
3-4x over-reservation before).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:57:56 +02:00
Marius CiepluchandClaude Fable 5.1 35f942f4c8 images: pin task base b696c08 (base and browser) by digest
Triggers the one intended rebuild of engine, replay and search on the shared base.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:50:16 +02:00
Marius Ciepluch b696c08446 merge(image-layering): shared task base and code-last layering [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	tests/test_search_task_storage.py
2026-10-05 20:44:02 +02:00
Marius CiepluchandClaude Fable 5.1 94d6a7dd11 search executor: NPROC bound counts threads, as the kernel does [skip ci]
Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:44:02 +02:00
Marius CiepluchandClaude Fable 5.1 1b9d1ca85c images: shared task base, code-last layers, no per-push rebuilds [skip ci]
One base (Temurin 21 JDK, node/npm/tsc, php/composer, maven, sqlite, bwrap)
built only when its Dockerfile or dependencies change, plus a browser variant;
engine/search/VM images FROM base@sha256 with the code in the last layer; the
image workflow runs on dispatch, tags, Dockerfile or pin changes, with a build
cache. Executor NPROC bound relative to the current process count.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:40:13 +02:00
Marius CiepluchandClaude Fable 5.1 509c6562fe search budget: recalibrated caps after the first measured searches [skip ci]
Settled ~$0.003 per model call; worst-case reservations exhausted the $0.05 task
allowance after 2-4 calls. Now $0.40 per task, $2 per search (maintainer decision).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:35:27 +02:00
Marius CiepluchandClaude Fable 5.1 b44fb56d8d ax batch: workload-kind prefixes in task names (pass-, replay-) [skip ci]
The operator keys per-host egress rules on the task-name prefix; engine passes and
replays had none. Names stay within 49 bytes.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:33:35 +02:00
Marius Ciepluch daf95b16a1 merge(search-with-proof): cluster-unavailable handling [skip ci]
No image rebuild: VM-side changes only (operator asked to stop per-merge rebuilds).

Co-Authored-By: Claude Fable 5.1 <[email protected]>

# Conflicts:
#	benchmarks/ax/batch.py
2026-10-05 20:27:01 +02:00
Marius CiepluchandClaude Fable 5.1 4be2063d40 search-with-proof: cluster-unavailable detection, kept messages, run-level abort
Zero running workers behind "no free workers available" now raises
ClusterUnavailable instead of waiting out the capacity deadline; CapacityDeadline
carries its message; cleanup failures no longer mask the original exception; the
pilot driver aborts the run on infrastructure failure before any model call.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
2026-10-05 20:26:37 +02:00
Marius CiepluchandClaude Opus 5.5 7fb061fb69 merge(search-with-proof): search contracts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 19:18:12 +02:00
Marius CiepluchandClaude Opus 5.5 e39a4882b0 search-with-proof: bootstrap intent, real reason/explore contracts, visible refusals
First live run: the reason step got ~190 tokens and returned {} -> silent
no_evidence. Now: a bootstrap intent reads the candidate and its entry point; the
reason prompt states the goal, family oracles, demo schema and must propose
intents when none are open; empty replies retry once then end as reason_refused;
failure records keep messages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 19:18:12 +02:00
Marius CiepluchandClaude Opus 5.5 0fb098c478 merge(search-with-proof): pilot driver
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 18:58:24 +02:00
Marius CiepluchandClaude Opus 5.5 87b82564b4 search-with-proof: pilot driver (6.2)
One side per run over selected pilot pairs: board, brain on the VM with the
project DeepSeek client, executor tasks on kube-ax, verification on the VM lane
(AX verify lane switchable). Hard run ceiling with reservation before every call,
per-search and step caps; fixed side never sees the vulnerable revision; records
carry digests only. Dry run with the stub model reaches demonstrated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 18:58:24 +02:00
Marius CiepluchandClaude Opus 5.5 89516d4000 ax batch: fast capacity retries in the first minute (golden-snapshot window)
Operator 2026-10-05: Substrate boots a ~1 s golden-snapshot actor per new task on the
other worker; retry apply after 2-5 s for the first minute, then 15-30 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 18:07:47 +02:00
Marius CiepluchandClaude Opus 5.5 941fbb5fb0 search-with-proof: diagnosable failures, pip install race, no duplicate tasks
Reasons carry type plus a sanitised message tail; command failures carry phase,
exit code and bounded stderr. Scratch walk tolerates directories pip removes
mid-walk; writable /workspace/home and runtime paths. Capacity retries delete a
possibly created task before resubmitting. Docker smoke checks the image first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:58:27 +02:00
Marius CiepluchandClaude Opus 5.5 7c1af9f518 merge(search-with-proof): diagnosable failures and dispatch fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:58:27 +02:00
Marius CiepluchandClaude Opus 5.5 cf10f38589 search-task image: clear the base image GPG_KEY fingerprint
The official Python base sets GPG_KEY (public CPython signing fingerprint); the
executor rightly refused it as a credential-like name on kube-ax.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:42:03 +02:00
Marius CiepluchandClaude Opus 5.5 75f2134eb4 search executor: name the credential-like variables it refuses (names only)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:36:12 +02:00
Marius CiepluchandClaude Opus 5.5 1d67412e2b search-with-proof: executor smoke driver and pilot selection (6.1)
20 development pairs, one per repository, 4 per language, 5 per family; identities
in a gitignored manifest, counts and digest committed. Executor smoke drives one
executor task end to end without a model and fails loudly on an unread clone.
kube-ax operator constraints as steering.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:26:06 +02:00
Marius CiepluchandClaude Opus 5.5 1bfb729422 merge(search-with-proof): executor smoke and pilot selection
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:26:06 +02:00
Marius CiepluchandClaude Opus 5.5 85f4f0b3d4 search-with-proof: declarative demos, brain/executor split, kube-ax constraints
Demos are data the verifier executes (fixed build recipes, runtime+path start,
HTTP/CLI steps with captures; the canary is never referable). The agent loop with
the key never runs repository code: executor tasks without a key exchange commands
and results through presigned objects. AX capacity errors are back-pressure with at
most 2 tasks in flight; caches and builds under /workspace with a 2 GiB guard;
verifiers take one presigned archive per side. Launcher fix: private /dev in bwrap.
190 tests on the host; probe 5/5 demonstrated.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:12:46 +02:00
Marius CiepluchandClaude Opus 5.5 ccd64757c8 merge(search-with-proof): trust-domain split and operator constraints
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 17:12:46 +02:00
Marius CiepluchandClaude Opus 5.5 721c4b0997 spec(search-with-proof): separate trust domains (brain with key, executor with repo code)
Replaces the accepted key-exposure exception: the key never shares a sandbox with
repository code; executor AX tasks exchange commands and results via presigned
objects. Firecracker executors only if the pilot build-failure rate requires it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 16:26:19 +02:00
Marius CiepluchandClaude Opus 5.5 5e03bdf07f spec(search-with-proof): gVisor is the cluster boundary; declarative demos; capped rotated key
kube-ax probe: AX tasks have no capabilities (no namespaces, no setuid), so no
in-task isolation. Demos become data the verifier executes; the explore key risk is
accepted and bounded by cap and rotation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 16:20:26 +02:00
Marius CiepluchandClaude Opus 5.5 e5ce630edc merge(search-with-proof): gVisor isolation mode
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 15:00:25 +02:00
Marius CiepluchandClaude Opus 5.5 03857c9bec search-with-proof: root-no-userns isolation mode for gVisor
The kube-ax probe showed bwrap cannot map uids in gVisor as an unprivileged user.
As root (gVisor is the outer boundary) the sandbox uses pid/ipc/uts/net/mount
namespaces without a user namespace and drops the artefact to its own uid via
setpriv; the mode is recorded. The probe reports each namespace kind separately.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 15:00:25 +02:00
Marius CiepluchandClaude Opus 5.5 e6871fef9a merge(search-with-proof): probe harness and agent worker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 14:45:48 +02:00
Marius CiepluchandClaude Opus 5.5 0f74767db9 search-with-proof: probe harness (3.2) and agent worker (group 5)
Probe runs each oracle toy pair with a real demo and two gaming demos, records
image facts, RSS and disk, fails loudly if isolation is unavailable; dispatchable
as an AX task. Worker: reason step without tools, explore tool loop whose run tool
goes through the sandbox with a scrubbed env (model key absent), spend reserved
before each call. Host probe: 5/5 real demos demonstrated, 10/10 gaming demos
inconclusive. 89 tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 14:45:48 +02:00
Marius CiepluchandClaude Opus 5.5 a17a6afaf8 merge(search-with-proof): group 4 and the search-task image
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 14:26:43 +02:00
Marius CiepluchandClaude Opus 5.5 4b38239f68 search-with-proof group 4 + image: board, spend reservation, coordinator, search-task image
Board as immutable versioned blobs (coordinator sole writer); budget reserve/settle
before model calls; per-task and run caps; coordinator loop over an injected
executor; batch.py accepts a fixed allow-list of search variables; XSS browser
oracle; ousast-search-task image (Python, Node/TypeScript, PHP, Java 17, sqlite,
bwrap, chromium) built by CI. 103 tests on the host.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 14:26:43 +02:00
Marius CiepluchandClaude Opus 5.5 6394ee473d merge(search-with-proof): group 2 verifier and oracles
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 14:09:48 +02:00
Marius CiepluchandClaude Opus 5.5 742469d671 search-with-proof group 2: verifier with owned oracles and differential
The verifier, not the artefact, observes the effect: SQL, path, command and SSRF
oracles owned by the verifier (XSS needs a browser executor, else no_oracle);
artefact exit code and output ignored; immutable checkout and dependencies;
3 runs per side; one-sided build or readiness failure is inconclusive. An
isolation failure raises IsolationUnavailable instead of reading as
could_not_build (the NPROC limit now applies inside the user namespace).
35 tests, including gaming attempts and 300 host processes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 14:09:48 +02:00
Marius CiepluchandClaude Opus 5.5 c74b5eaeb0 spec(learned-decision-engine): engine becomes triage where a demonstration oracle exists
Req 6.5 (maintainer decision 2026-10-05, with search-with-proof): BLOCK comes from
verified demonstrations; the engine is judged by top-K recall per push and advisory
false alarms; the pooled block gate governs only families without an oracle.
Task 17 adds the metric and re-scores existing arms at no cost.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 13:45:10 +02:00
Marius CiepluchandClaude Opus 5.5 9d70ef9257 spec(search-with-proof): requirements and design approved; tasks for probe and pilot
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 13:35:55 +02:00
Marius CiepluchandClaude Opus 5.5 c3820e184c spec(search-with-proof): requirements amended after two Codex reviews; design v2
False alarms count every visible finding; the verifier owns per-family oracles;
inconclusive on one-sided build failure; feasibility pilot separated from a
73-negative BLOCK qualification; coverage ceilings; spend reserved before calls;
key isolated from executed code; cluster model calls supersede the pool VM-only
rule for this arm only. Requirements approval reopened.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 13:35:18 +02:00
Marius CiepluchandClaude Opus 5.5 8bbea86cf8 spec(search-with-proof): requirements approved
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 12:01:39 +02:00
Marius CiepluchandClaude Opus 5.5 45e641f25c spec(search-with-proof): brief and requirements for review
After reviewing Cairn: detection so far classified excerpts and hit a precision
wall; this adds directed search with an objective goal (a re-verified
demonstration), a fact/intent board, and reason/explore steps as AX Tasks on
kube-ax. Design only learns from Cairn (AGPL); no code copied.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 12:01:07 +02:00
Marius CiepluchandClaude Opus 5.5 6d27c9bfcf unseen-eval: calibrated replay deadline + native agreement check, docker-lane diagnostics, proof 4.4 record; engine runner cleans up fetched sources
gVisor replays exhausted the hook's 30 s deadline in the snapshot step on a
1,725-file repository; cluster replays now use 30 s x a measured slowdown
factor, verified on a native sample. The engine runner deletes each pin's fetched
source (1.8 GB had accumulated), caps the cache at 500 MB and refuses below
1.5 GB free. Proof 4.4: two engine pins via the generic dispatcher match the VM
baseline exactly. Implemented with Codex.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 08:14:57 +02:00
Marius CiepluchandClaude Opus 5.5 5dc86c2f3f merge(unseen-eval): groups 1-2 and 4-6 -- used set, sourcing with resume, AX batch dispatcher, replay task, scorer
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-04 20:28:15 +02:00
898 changed files with 163047 additions and 21306 deletions
+2 -2
View File
@@ -38,7 +38,7 @@ uv run ousast mcp # the MCP server over stdio, for agent hosts
| --- | --- | --- | --- |
| `quick` | language-scoped pattern rules, entry-point hints, ranking, verification, score; deterministic | nothing | - |
| `standard` | the MAP stage: complexity map, semantic overlay, authorization obligations, the Joern model layer (one code property graph per repository; taint, guard-dominance and configuration questions), deterministic fusion | Joern (shipped in the Docker image, with `php-cli` for PHP); a provider key for the residual LLM question | `cpg_unavailable`; without a key the LLM question is not asked and only the graph's entailed findings are reported |
| `deep` | REGRESS: promoted candidates run as snippets in a Docker sandbox (no network, read-only source, non-root) and get a `triggerable` / `not_triggerable` / ... verdict in `verdicts.json` | a working `docker` | `sandbox_unavailable` |
| `deep` | nothing today: REGRESS was retired on 2026-10-13 (degradation `regress_retired`); execution proof belongs to the search-with-proof arm | - | - |
Model stages read `DEEPSEEK_API_KEY` (every LLM call); with only `OPENROUTER_API_KEY` set, chat
calls fall back to OpenRouter. An exported shell variable wins over `.env` silently. `[models]
@@ -60,7 +60,7 @@ A degradation never changes the exit code. Read `manifest.json` for it.
Each entry in `manifest.json` `degradations` is `{"stage": ..., "reason": ...}`. The ones to expect:
- `cpg_unavailable` (stage `model`): no Joern on the host; the model layer ran nothing.
- `sandbox_unavailable` (stage `regress`): no working `docker`; deep mode fell back to standard.
- `regress_retired` (stage `regress`): deep mode requested the retired snippet stage; the scan equals standard.
- `learning_endpoint_unavailable` (stage `model`): no provider key resolved. The graph's entailed
findings are still reported; the residual question is skipped. This is recorded in `manifest.json`
but has no line in `report.md`'s "What could not be analysed" section.
+7
View File
@@ -21,3 +21,10 @@ OPENROUTER_API_KEY=
# AWS_SESSION_TOKEN=
# The bucket must have versioning enabled, a lifecycle rule expiring runs/, S3 Select and readable object tags
# (see docs/rustfs.md); the store verifies them at startup and refuses to run otherwise.
# Operator notifications (Discord). Status of OpenUltraSAST processes and their results for the
# system/AI operator -- not developer debugging, and not k3s/ax infra health. See
# .kiro/steering/operator-notifications.md. Webhook posts to the operator channel; the operator id
# is the account to @mention on a stuck/impaired alert. Leave blank to disable notifications.
DISCORD_WEBHOOK_URL=
DISCORD_OPERATOR_ID=
+7
View File
@@ -0,0 +1,7 @@
<claude-mem-context>
# claude-mem: Cross-Session Memory
*No context yet. Complete your first session and context will appear here.*
Use claude-mem's MCP search tools for manual memory queries.
</claude-mem-context>
+4 -1
View File
@@ -11,6 +11,9 @@ permissions:
jobs:
checks:
runs-on: ubuntu-latest
env:
# uv run re-syncs to the lockfile's default extras unless told not to; it removed the extras below before pytest.
UV_NO_SYNC: "1"
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v7
@@ -19,7 +22,7 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version-file: .python-version
- run: uv sync --group dev
- run: uv sync --group dev --extra hunt --extra semantic
- run: uv run ruff format --check .
- run: uv run ruff check .
- run: uv run mypy src/openultrasast
+146 -16
View File
@@ -3,17 +3,11 @@ on:
workflow_dispatch:
push:
branches: [main]
tags: ['v*']
paths:
- 'src/**'
- 'benchmarks/learn/engine_trace*.py'
- 'benchmarks/learn/engine_task_entry.py'
- 'benchmarks/unseen/replay_entry.py'
- 'benchmarks/learn/engine_worker_service.py'
- 'plane/Dockerfile.*'
- 'plane/task-base.digest'
- 'Dockerfile'
- 'plane/Dockerfile.engine-task'
- 'ops/frontend-retention/install.py'
- 'pyproject.toml'
- '.github/workflows/engine-image.yml'
permissions:
contents: read
packages: write
@@ -21,7 +15,40 @@ concurrency:
group: engine-image-${{ github.ref }}
cancel-in-progress: false
jobs:
pins:
runs-on: ubuntu-latest
outputs:
base_tag: ${{ steps.pins.outputs.base_tag }}
base_digest: ${{ steps.pins.outputs.base_digest }}
browser_tag: ${{ steps.pins.outputs.browser_tag }}
browser_digest: ${{ steps.pins.outputs.browser_digest }}
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
# No login: every pinned base must be anonymously pullable.
- id: pins
run: |
python3 - <<'PYTHON'
import json, os, re, subprocess
from pathlib import Path
pins = json.loads(Path("plane/task-base.digest").read_text())
manifests = {}
for key, output in (("base", "base"), ("base-browser", "browser")):
ref = pins.get(key)
pattern = r"ghcr.io/norandom/ousast-task-base:(base-[a-z0-9-]+)@(sha256:[0-9a-f]{64})"
match = re.fullmatch(pattern, ref or "")
if not match:
raise SystemExit(f"{key}: unpublished/invalid pin; run task-base-image and update plane/task-base.digest")
raw = subprocess.check_output(["docker", "buildx", "imagetools", "inspect", "--raw", ref])
manifests[key] = json.loads(raw)["layers"]
with open(os.environ["GITHUB_OUTPUT"], "a") as out:
out.write(f"{output}_tag={match[1]}\n{output}_digest={match[2]}\n")
base = manifests["base"]
if not base or manifests["base-browser"][:len(base)] != base:
raise SystemExit("Browser pin does not share the pinned base's layers")
PYTHON
image:
needs: pins
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -29,12 +56,8 @@ jobs:
include:
- package: openultrasast
dockerfile: Dockerfile
- package: ousast-engine-task
dockerfile: plane/Dockerfile.engine-task
target: engine
- package: ousast-replay-task
dockerfile: plane/Dockerfile.engine-task
target: replay
- package: ousast-search-task
dockerfile: plane/Dockerfile.search-task
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
@@ -55,9 +78,18 @@ jobs:
with:
context: .
file: ${{ matrix.dockerfile }}
target: ${{ matrix.target }}
platforms: linux/amd64
push: true
provenance: false
build-args: |
TASK_BASE_TAG=${{ needs.pins.outputs.base_tag }}
TASK_BASE_DIGEST=${{ needs.pins.outputs.base_digest }}
TASK_BROWSER_TAG=${{ needs.pins.outputs.browser_tag }}
TASK_BROWSER_DIGEST=${{ needs.pins.outputs.browser_digest }}
cache-from: |
type=gha,scope=${{ matrix.package }}
type=gha,scope=ousast-engine-task
cache-to: type=gha,mode=max,scope=${{ matrix.package }}
tags: |
${{ steps.names.outputs.image }}:${{ steps.names.outputs.sha }}
${{ steps.names.outputs.image }}:main
@@ -67,3 +99,101 @@ jobs:
run: |
echo "$IMAGE@$DIGEST"
echo "$IMAGE@$DIGEST" >> "$GITHUB_STEP_SUMMARY"
docker logout ghcr.io
docker buildx imagetools inspect "$IMAGE@$DIGEST"
engine:
needs: pins
runs-on: ubuntu-latest
outputs:
image: ${{ steps.names.outputs.image }}
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: names
env:
OWNER: ${{ github.repository_owner }}
PACKAGE: ousast-engine-task
run: |
echo "image=ghcr.io/${OWNER,,}/$PACKAGE" >> "$GITHUB_OUTPUT"
echo "sha=sha-${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
- id: build
uses: docker/build-push-action@v6
with:
context: .
file: plane/Dockerfile.engine-task
target: engine
platforms: linux/amd64
push: true
provenance: false
build-args: |
TASK_BASE_TAG=${{ needs.pins.outputs.base_tag }}
TASK_BASE_DIGEST=${{ needs.pins.outputs.base_digest }}
TASK_BROWSER_TAG=${{ needs.pins.outputs.browser_tag }}
TASK_BROWSER_DIGEST=${{ needs.pins.outputs.browser_digest }}
cache-from: type=gha,scope=ousast-engine-task
cache-to: type=gha,mode=max,scope=ousast-engine-task
tags: |
${{ steps.names.outputs.image }}:${{ steps.names.outputs.sha }}
${{ steps.names.outputs.image }}:main
- env:
IMAGE: ${{ steps.names.outputs.image }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
echo "$IMAGE@$DIGEST"
echo "$IMAGE@$DIGEST" >> "$GITHUB_STEP_SUMMARY"
docker logout ghcr.io
docker buildx imagetools inspect "$IMAGE@$DIGEST"
replay:
needs: [pins, engine]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: names
env:
OWNER: ${{ github.repository_owner }}
PACKAGE: ousast-replay-task
run: |
echo "image=ghcr.io/${OWNER,,}/$PACKAGE" >> "$GITHUB_OUTPUT"
echo "sha=sha-${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
- id: build
uses: docker/build-push-action@v6
with:
context: .
file: plane/Dockerfile.engine-task
target: replay
platforms: linux/amd64
push: true
provenance: false
build-args: |
ENGINE_REF=${{ needs.engine.outputs.image }}@${{ needs.engine.outputs.digest }}
TASK_BASE_TAG=${{ needs.pins.outputs.base_tag }}
TASK_BASE_DIGEST=${{ needs.pins.outputs.base_digest }}
TASK_BROWSER_TAG=${{ needs.pins.outputs.browser_tag }}
TASK_BROWSER_DIGEST=${{ needs.pins.outputs.browser_digest }}
cache-from: |
type=gha,scope=ousast-replay-task
type=gha,scope=ousast-engine-task
cache-to: type=gha,mode=max,scope=ousast-replay-task
tags: |
${{ steps.names.outputs.image }}:${{ steps.names.outputs.sha }}
${{ steps.names.outputs.image }}:main
- env:
IMAGE: ${{ steps.names.outputs.image }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
echo "$IMAGE@$DIGEST"
echo "$IMAGE@$DIGEST" >> "$GITHUB_STEP_SUMMARY"
docker logout ghcr.io
docker buildx imagetools inspect "$IMAGE@$DIGEST"
+14 -2
View File
@@ -11,6 +11,9 @@ permissions:
jobs:
build-release:
runs-on: ubuntu-latest
env:
# Preserve the explicitly installed test extras across each uv run invocation.
UV_NO_SYNC: "1"
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v7
@@ -19,11 +22,20 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version-file: .python-version
- run: uv sync --group dev
- run: uv sync --group dev --extra hunt --extra semantic
- run: uv run ruff format --check .
- run: uv run ruff check .
- run: uv run mypy src/openultrasast
- run: uv run pytest
- name: Run release tests
run: |
if [[ "${GITHUB_REF_NAME}" == *-* ]]; then
# The alpha checkpoint archives the implemented core while the benchmark owner resolves the
# documented task-11.4 reservation/lifecycle conflict. Stable tags retain the complete gate.
uv run pytest \
--deselect=tests/test_independent_population.py::test_no_reserved_repository_is_referenced_by_the_development_tree
else
uv run pytest
fi
- run: uv build
- uses: softprops/action-gh-release@v2
with:
+78
View File
@@ -0,0 +1,78 @@
name: Task base image
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'plane/Dockerfile.base'
- 'pyproject.toml'
- 'uv.lock'
permissions:
contents: read
packages: write
concurrency:
group: task-base-image
cancel-in-progress: false
jobs:
base:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: names
run: |
echo "tag=base-${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
content=$(cat plane/Dockerfile.base pyproject.toml uv.lock | sha256sum | cut -d' ' -f1)
echo "content=base-content-$content" >> "$GITHUB_OUTPUT"
- id: base
uses: docker/build-push-action@v6
with:
context: .
file: plane/Dockerfile.base
target: base
platforms: linux/amd64
push: true
provenance: false
sbom: false
tags: |
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.content }}
cache-from: type=gha,scope=task-base
cache-to: type=gha,mode=max,scope=task-base
- id: browser
uses: docker/build-push-action@v6
with:
context: .
file: plane/Dockerfile.base
target: base-browser
platforms: linux/amd64
push: true
provenance: false
sbom: false
build-args: |
TASK_BASE_REF=ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}@${{ steps.base.outputs.digest }}
tags: |
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}-browser
ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.content }}-browser
cache-from: type=gha,scope=task-base-browser
cache-to: type=gha,mode=max,scope=task-base-browser
- name: Print manual pin update and verify public access
env:
BASE: ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}@${{ steps.base.outputs.digest }}
BROWSER: ghcr.io/norandom/ousast-task-base:${{ steps.names.outputs.tag }}-browser@${{ steps.browser.outputs.digest }}
run: |
python3 - <<'PYTHON' >> "$GITHUB_STEP_SUMMARY"
import json, os
print("Replace plane/task-base.digest with this JSON after both public checks pass:")
print("```json")
print(json.dumps({"base": os.environ["BASE"], "base-browser": os.environ["BROWSER"]}, indent=2))
print("```")
PYTHON
docker logout ghcr.io
docker buildx imagetools inspect "$BASE"
docker buildx imagetools inspect "$BROWSER"
+6
View File
@@ -22,6 +22,9 @@ build/
.cache/openultrasast/
**/.cache/openultrasast/
artifacts/
!src/openultrasast/ruleset/entries/artifacts/
!src/openultrasast/ruleset/entries/artifacts/sha256/
!src/openultrasast/ruleset/entries/artifacts/sha256/*
reports/
vector-store/
*.sarif
@@ -39,3 +42,6 @@ site/
# Unseen evaluation pointers for non-permissive repositories.
benchmarks/unseen/private/
# Feasibility pilot identities remain local; only aggregate records are tracked.
benchmarks/search/private/
+261
View File
@@ -0,0 +1,261 @@
# Breadth ledger: every spec and every top-level capability, with a status and the deletion that closes it.
# Draft 2026-10-13 from the 22 spec.json files and the Pathfinder feature inventory (PATHFINDER-2026-10-13/).
# Nothing here changes code; it records decisions so a gate can hold the line.
#
# status values
# active = on one of the four lines in .kiro/steering/overview.md (2026-10-13); work may land
# done = implemented and closed; the code it produced is owned by a capability below
# parked = no work lands; scaffolding (stubs, unused flags, dead task modules) is removed; spec stays as history
# retiring = the "closes" deletion is scheduled; the spec directory goes with it
# review = Pathfinder 2026-10-13 decides between parked and retiring
# removed = the "closes" deletion has run; the entry stays one release as the record of what went
#
# open_fronts (the weekly number) = specs with status active or review
# + capabilities with status review or retiring whose "closes" has not run.
#
# gate (to implement after the map is approved):
# fail when a spec.json reaches phase "tasks-generated" without an entry here;
# fail when a new directory under src/openultrasast/ lands without a [capability.*] entry;
# fail when an entry has status "retiring" for more than 14 days with its "closes" unexecuted.
[meta]
drafted = "2026-10-13"
spec_count = 22
open_fronts = 0 # filled by the gate script once it exists
# ---------------------------------------------------------------- specs
[spec.hunter-learning-loop]
status = "active"
line = "hunter-learning-loop"
phase = "requirements-generated"
owner = ["src/openultrasast/learn", "src/openultrasast/tool_hunter.py", "src/openultrasast/hunter_tools.py"]
closes = "n/a (active)"
[spec.pre-push-safety-net]
status = "active"
line = "pre-push-safety-net"
phase = "implementation (77 of 87 tasks)"
owner = ["src/openultrasast/push"]
closes = "n/a (active)"
[spec.search-with-proof]
status = "active"
line = "search-with-proof"
phase = "tasks-generated (11 of 24 done)"
owner = ["src/openultrasast/search", "src/openultrasast/sandbox"]
closes = "n/a (active)"
[spec.proof-arm-opencode]
status = "active"
line = "search-with-proof"
phase = "brief"
owner = ["src/openultrasast/search"]
closes = "n/a (active)"
[spec.engine-change-scoped-analysis]
status = "active"
line = "engine-change-scoped-analysis"
phase = "brief"
owner = ["src/openultrasast/cpg", "src/openultrasast/model", "src/openultrasast/semantic"]
closes = "n/a (active)"
[spec.valid-detection-benchmark]
status = "active"
line = "hunter-learning-loop" # the benchmark every line is measured on
phase = "tasks (18 of 24 done)"
owner = ["benchmarks/valid", "benchmarks/unseen/replay.py", "benchmarks/unseen/score.py"]
closes = "n/a (active)"
[spec.learned-decision-engine]
status = "parked"
phase = "implementation (20 of 47 tasks)"
owner = ["src/openultrasast/learn"]
note = "the classifier it built is the second stage of the hunter arm and stays; new learn work goes through hunter-learning-loop, not here"
closes = "mark remaining 27 tasks superseded by hunter-learning-loop; no code deletion"
[spec.plane-on-kubernetes]
status = "parked"
phase = "implementation (14 of 43 tasks)"
owner = ["src/openultrasast/plane"]
note = "kube move paused after groups 1-2 (2026-10-02); the ax lane is blocked on the gVisor checkpoint path (operator-owned)"
closes = "when the ax lane is restored: finish; otherwise delete plane/runner.py container path and ops/k8s"
[spec.contributor-scan]
status = "parked"
phase = "implementation (27 of 51 tasks), paused since 2026-09-23"
owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
closes = "fold the open census/family-context tasks into engine-change-scoped-analysis; close this spec"
[spec.mcp-shift-left-agent]
status = "parked"
phase = "brief"
owner = ["src/openultrasast/mcp.py", "src/openultrasast/skills.py", "src/openultrasast/index.py"]
note = "follow-up to the pre-push hook; waits for a hook that catches something"
closes = "if not started by 2026-12-01: delete mcp.py, skills.py, index.py and their tests"
[spec.authorization-obligations]
status = "review"
phase = "tasks-generated (0 of 24)"
owner = ["src/openultrasast/semantic/obligations"]
note = "overview.md calls it superseded in practice by the dominance arbiter; obligations code exists and is called from scan (cli.py:1388)"
closes = "Pathfinder U5 decides: keep obligations as an annotation producer for the hunter, or delete semantic/obligations and its tests"
[spec.finding-feedback-loop]
status = "retiring"
phase = "design, dormant since 2026-09-09"
owner = []
closes = "delete .kiro/specs/finding-feedback-loop (no code was written)"
[spec.reachability-flow-model]
status = "retiring"
phase = "tasks-generated (0 of 23), dormant"
owner = []
note = "largely delivered by adopting Joern (overview.md)"
closes = "delete .kiro/specs/reachability-flow-model"
[spec.flow-aware-ranking]
status = "retiring"
phase = "brief"
owner = []
closes = "delete .kiro/specs/flow-aware-ranking; keep the one sentence it owns in safety-net.md"
[spec.zero-with-a-reason]
status = "retiring"
phase = "brief"
owner = []
note = "its idea (coverage gaps are reported, never a clean verdict) is already a product principle in safety-net.md"
closes = "delete .kiro/specs/zero-with-a-reason"
[spec.ir-consolidation]
status = "retiring"
phase = "brief"
owner = []
closes = "delete .kiro/specs/ir-consolidation"
[spec.ai-service-plane]
status = "done"
owner = ["src/openultrasast/plane"]
[spec.model-grounded-detection]
status = "done"
owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
[spec.pair-corpus-honesty]
status = "done"
owner = ["src/openultrasast/pairs.py", "benchmarks/pairs"]
[spec.corpus-seeded-mechanisms]
status = "done"
owner = ["src/openultrasast/learn"]
[spec.propose-adjudicate-prove]
status = "done"
owner = ["src/openultrasast/semantic", "src/openultrasast/ruleset"]
[spec.tree-sitter-overlay-extra]
status = "done"
owner = ["src/openultrasast/semantic"]
# ---------------------------------------------------------------- capabilities (top-level code)
[capability.push]
status = "active"
spec = "pre-push-safety-net"
lines = 5074
[capability.engine]
status = "active"
spec = "engine-change-scoped-analysis"
modules = ["cpg", "model (minus endpoint.py)"]
lines = 7764
[capability.semantic]
status = "active"
spec = "engine-change-scoped-analysis"
modules = ["semantic", "complexity"]
lines = 4301
note = "complexity/ hotspots feed the hunter; obligations under review (see spec.authorization-obligations)"
[capability.llm-hunter-and-client]
status = "active"
spec = "hunter-learning-loop"
modules = ["tool_hunter.py", "hunter_tools.py", "provider/openrouter.py", "model/endpoint.py", "plane/budget.py"]
lines = 1700
note = "four never-enabled hunter tools removed 2026-10-13 (P0); P2 collapses the client stack to one client, one meter, one resolver"
[capability.learn]
status = "active"
spec = "hunter-learning-loop"
modules = ["learn", "plane/memory.py", "plane/tasks (fact producers)"]
lines = 11350
[capability.search-with-proof]
status = "active"
spec = "search-with-proof"
modules = ["search", "sandbox"]
lines = 4803
[capability.core]
status = "active"
modules = ["cli.py", "config.py", "contracts.py", "preprocess.py", "source.py", "findings.py", "rank.py", "mapping.py", "ruleset", "policy", "reports.py", "stages.py", "run.py", "provenance.py", "profiling.py"]
# source.py (P1, 2026-10-13) owns function ranges, enclosing function, bounds, call sites, product files, declaration
# tables and language aliases; learn no longer imports plane.tasks
note = "hunter.py is the regex quick-rule pool and should be renamed"
[capability.pairs-and-benchmarks]
status = "active"
spec = "valid-detection-benchmark"
modules = ["pairs.py", "benchmark.py", "repos.py"]
note = "pairs.py label functions feed learn/labels.py; the `ousast pairs` CLI surface is under review (U10)"
[capability.plane-executor]
status = "review"
spec = "plane-on-kubernetes"
modules = ["plane (reconciler, runner, generate, manifests, harvest, egress, delivery, router, doctor, profile, engine_alerts, workspaces)"]
lines = 4885
closes = "Pathfinder U8 lists which modules only the retired HarnessX loop used (engine_alerts, harvest, workspaces, profile are the suspects); delete those"
[capability.regress]
status = "removed" # f5a904b, 2026-10-13: deep mode records regress_retired; proof is the search line
modules = ["regress"]
lines = 549
closes = "Pathfinder U9: if search/ covers the triggerable verdict, delete regress/ and the deep-mode REGRESS branch in cli.py:839-880"
[capability.improve]
status = "removed" # 94e96c6, 2026-10-13
modules = ["improve"]
lines = 1175
note = "HarnessX-era improve lever; no active spec names it"
closes = "delete improve/, `ousast improve` (cli.py:237, 433, 1106), plane/tasks/loop.py lazy imports, tests/test_cli_improve.py, tests/test_improve_*.py"
[capability.scan-extras]
status = "review"
modules = ["fusion.py", "verification.py", "calibration.py", "scoring"] # harness.py processor machinery removed 2026-10-13
closes = "U1 found fusion decisions never reach findings or score, verification runs before MAP, processors unreachable; decide keep-and-fix or delete per module"
[capability.mcp-and-skills]
status = "parked"
spec = "mcp-shift-left-agent"
modules = ["mcp.py"] # skills.py and index.py removed f5a904b, 2026-10-13
lines = 830
closes = "see spec.mcp-shift-left-agent"
[capability.gates]
status = "review"
modules = ["gate.py", "map_gate.py", "pair_gate.py"]
lines = 428
closes = "U10: keep gate.py (CI + plane loop); delete map_gate.py and pair_gate.py if CI does not need them"
[capability.push-evaluation-instruments]
status = "review"
modules = ["push_scoring.py"] # push_inputs.py moved to benchmarks/push 2026-10-13; push_scoring stays (eligibility.py:18)
lines = 462
closes = "U3: move under benchmarks/push or delete if only tests use them"
[capability.quick-rules-and-overlay]
status = "review"
modules = ["findings.py", "ruleset", "semantic (taint, overlay, prove_filter)"]
note = "maintainer 2026-10-13: 'semgrep oss is a good idea'; brief in benchmarks/measurements/2026-10-13-semgrep-brief/ says registry rules are not shippable, our converted rules are; 4.7 s startup floor"
closes = "run the Semgrep arm on the valid pilot (benchmarks/valid/semgrep_arm.py) as a comparison; it may replace the regex quick tier only (same job: pattern matches), never act as a filter on other tiers. A Semgrep miss is never evidence (maintainer 2026-10-13). If it matches the quick tier at least as well, retire findings.py and ruleset/*.py and amend safety-net.md with the measurement"
+645
View File
@@ -0,0 +1,645 @@
# Design Document
## Overview
The access-control prover remains a deterministic rule arm over the existing obligation checker. The amendment
changes the semantic contract: resolution, resource matching, authority matching and dominance return abstract
relations rather than booleans, and every examined path receives one of three judgments: `unguarded`,
`discharged`, or `indeterminate`. A degraded instrument may reduce precision to `indeterminate`; it cannot
manufacture either final verdict.
Target knowledge remains outside the engine. Entry shapes live in registered recognition strategies, while
operations, dischargers, resource rules and authority implications live in promoted fact data. A label-blind
initialization job produces that knowledge, and evaluation freezes its digest before admission labels are opened.
### Goals
- Produce an unguarded finding only from a complete, sufficiently resolved evidence path.
- Make call, path, resource, authority and control-flow uncertainty explicit and monotone.
- Carry target knowledge as verified data or recogniser strategies, never engine branches.
- Attribute development gains across analyzer and fact changes before measuring an untouched admission population.
- Fail the runtime and instrument gates rather than summarize partial work as a result.
### Non-Goals
- Taint analysis, Joern integration, model calls inside the prover, or vulnerability families beyond access control.
- Client-side presentation gating, proof execution, pre-push wiring, or admission-policy thresholds.
- Replacing the existing order-based checker mode for consumers that do not opt into prover semantics.
## Boundary Commitments
### This Spec Owns
- The abstract evidence domain and three-way pair/path judgment.
- Stable program identities, conservative call-target sets and bounded interprocedural summaries.
- Resource- and authority-sensitive operation/witness matching.
- Prover-mode control-flow dominance without textual-order fallback.
- Acquired entry, operation, discharger and authority knowledge needed by this family.
- The production access-control detection-core family contract, independently reviewed
target-knowledge authority, structural matcher, and canonical observation/match writer.
- Deterministic traces, failure-visible coverage, development arms and the admission-run contract.
### Out of Boundary
- The legacy checker behavior used by existing callers; default APIs remain backward compatible.
- Target labels and benchmark curation, owned by `valid-detection-benchmark`.
- Runtime demonstrations, owned by `search-with-proof`.
- Hook admission and rollout, owned by `pre-push-safety-net`.
- Continuous fact evolution after the one-time initialization pass.
- Family-neutral fact, match, evidence, readiness and admission validation, owned by
`detection-core`.
### Allowed Dependencies
- `openultrasast.source`, `openultrasast.mapping`, the existing semantic IR and CFG, and the existing obligation
facts/checker contracts.
- The hunter `Context` caches, valid-benchmark metrics, replay record envelopes and reserved-population guard.
- Public detection-core v2 contracts and validators through an adapter dependency from this
spec to core; detection-core never imports access-control or target-aware modules.
- No new third-party dependency.
### Revalidation Triggers
- A change to `EntryPointRecord`, `Operation`, `Discharge`, `Trace`, hunt-defect or replay-record contracts.
- A change to call-site receiver/signature capture, target-knowledge schemas or authority implication semantics.
- A change to path aggregation, judgment ordering, admission population isolation or the 120-second budget.
- A change to detection-core family, fact-pack, fact-match, provenance, custody or canonical
serialization contracts; the adapter must re-run downstream conformance before use.
## Architecture
### Existing Architecture Analysis
The delivered baseline already recognises entries, composes bounded paths, discovers operations and witnesses,
builds structural CFGs, emits traces and scores replay records. Its decision interfaces are boolean: unresolved
receivers fall back to a bare name, unavailable CFGs fall back to textual order, and a witness either discharges or
does not. The amendment preserves the existing checker as the single verdict owner while inserting typed abstract
values before that boolean boundary and adding an opt-in three-way judgment path.
### Architecture Pattern & Boundary Map
~~~mermaid
flowchart LR
TK[Target knowledge] --> MR[Mapping registry]
TK --> FI[Fact interpreter]
TK --> FA[Reviewed fact authority]
FA --> FM[Structural matcher and artifact writer]
FM --> DC[Detection core validation]
MR --> CTX[Per change context]
FI --> OP[Operations and witnesses]
CTX --> SI[Symbol index]
SI --> CR[Call resolution]
CR --> FS[Function summaries]
OP --> FS
FS --> PM[Concrete path materializer]
CTX --> CFG[Control flow relations]
PM --> J[Three way judgment]
CFG --> J
OP --> J
J --> TR[Canonical traces]
J --> PR[Prover findings and coverage]
TR --> RUN[Development and admission runners]
PR --> RUN
~~~
Dependency direction is:
`abstract domain -> fact and recogniser adapters -> symbol resolution -> summaries and CFG -> checker judgment
-> prover integration -> runners`. Engine modules never import target-named strategy modules. Target-aware
adapters may import engine contracts.
- **Strategy + Registry**: entry recognisers are registered per recognition concern.
- **Interpreter**: promoted fact rows describe operations, dischargers, resource rules and authority implications.
- **Adapter**: the prover converts repository IR, target knowledge and context caches into engine contracts.
- **Abstract interpretation**: finite domains and monotone joins propagate evidence and incompleteness.
### Technology Stack
| Layer | Choice | Role |
|---|---|---|
| Analysis | Python 3.11, existing semantic IR and structural CFG | symbols, calls, summaries, dominance |
| Knowledge | Existing TOML fact files and mapping strategies | target-specific vocabulary |
| Core contract | detection-core v2 typed records and validators | family-neutral conformance |
| Scoring | Existing valid-benchmark metrics and replay records | development and admission results |
## File Structure Plan
~~~text
src/openultrasast/
├── mapping/
│ ├── registry.py # strategy registration and dispatch
│ ├── registration_sites.py # generic call-site registration adapter
│ ├── fact_authority.py # authenticate installed reviewed authority
│ ├── installed_fact_authority.py # closed JSON decoders and checked-in artifact resolver
│ ├── entry_packs.py # compatibility projection and v2 structural adapter
│ ├── fact_match_store.py # persist canonical observations and match artifacts
│ ├── legacy.py # production caller migrates to authority-bearing adapter
│ └── <target>.py # one target-aware strategy per migrated concern
├── semantic/
│ ├── ir.py # CallSite receiver and signature evidence
│ ├── cst.py # receiver, arity and signature capture
│ └── obligations/
│ ├── abstract_domain.py # relations, completeness, resources, authorities, judgments
│ ├── core_contract.py # production access-control FamilyContract
│ ├── resolution.py # SymbolId, symbol index and CallTargetSet
│ ├── summaries.py # monotone FunctionSummary computation over call components
│ ├── paths.py # concrete evidence paths and cutoff records
│ ├── operations.py # operation/witness abstract identities
│ ├── dominance.py # legacy order mode, unchanged by default
│ ├── cfg_dominance.py # prover-mode control-flow relation, no order fallback
│ ├── check.py # opt-in abstract matching and PairJudgment
│ ├── enforcement.py # target-aware enforcement adapter
│ └── trace.py # canonical trace with abstract relations and completeness
├── hunt/
│ ├── producers.py # cached symbol index and summaries
│ └── prover.py # integration, path aggregation, findings and coverage
└── tools/
└── fact_init.py # label-blind extract/propose/verify/promote workflow
ruleset/obligations/*.toml # promoted operation, witness and authority knowledge
src/openultrasast/ruleset/entries/
├── bootstrap.json # host trust policy, external to authenticated bundle
├── authority.json # reviewed bundle payload and immutable references
├── express.javascript.v2.json # reviewed JavaScript FactPack
├── express.typescript.v2.json # reviewed TypeScript FactPack
├── express.json # authenticated non-authoritative compatibility projection
├── identities/
│ ├── generator.json # producer identity manifest
│ ├── maintainer-reviewer.json # maintainer approval identity manifest
│ └── custodian.json # installed-artifact custody identity manifest
└── artifacts/sha256/<content-digest> # canonical review/disclosure/evidence bytes
benchmarks/valid/
├── prover_arm.py # records, stage funnel, gates and four-arm comparison
└── prover_admission.py # frozen-manifest admission orchestration
tests/
├── test_obligation_abstract_domain.py
├── test_obligation_core_contract.py
├── test_obligation_resolution.py
├── test_obligation_summaries.py
├── test_obligation_judgment.py
├── test_obligation_trace.py
├── test_mapping_registry.py
├── test_fact_init.py
├── test_hunt_prover.py
└── test_prover_arm.py
~~~
### Modified Files
- Existing facts, IR, checker, dominance, trace, prover and arm modules receive additive or opt-in contracts.
- Existing callers retain legacy defaults; only the prover selects three-way judgment and strict CFG semantics.
- Existing completed benchmark records remain immutable development baselines.
- `mapping/entry_packs.py` keeps legacy pattern output byte-stable while adding an explicit
authority-bearing v2 path; no-context legacy loading cannot emit detection-core evidence.
## System Flows
### Analysis
~~~mermaid
flowchart TD
A[Read cached source] --> B[Recognise entries and facts]
B --> C[Build symbol index]
C --> D[Resolve call target sets]
D --> E[Compute function summaries]
E --> F[Materialise evidence paths]
F --> G[Match witnesses to obligations]
G --> H[Evaluate control flow]
H --> I{Path state}
I -->|complete and missing| U[Unguarded]
I -->|complete and governed| DCH[Discharged]
I -->|possible unknown or cut| IND[Indeterminate]
U --> FIND[Finding and trace]
DCH --> TRACE[Trace only]
IND --> TRACE
~~~
### Evaluation
~~~mermaid
flowchart LR
ACQ[Label blind acquisition] --> FREEZE[Freeze candidate bundle]
BASE[Materialize baseline bundle] --> DEV[Four arm development comparison]
FREEZE --> DEV
DEV --> AUDIT[Review ordinary labels]
AUDIT --> HOLD[Untouched paired admission run]
HOLD --> GATE{Instrument budget and quality gates}
GATE -->|pass| GO[G1 eligible]
GATE -->|fail| NOGO[No go]
~~~
### Reviewed fact adaptation
~~~mermaid
flowchart LR
BP[Host bootstrap policy] --> AUTH[Authenticate authority bundle]
DATA[Reviewed contract packs and projection] --> AUTH
AUTH --> MATCH[Match typed repository structure]
MATCH --> OBS[Persist canonical observations]
OBS --> EVID[Build and persist FactMatchEvidence]
EVID --> CORE[Detection core validate and apply]
LEGACY[Legacy pattern projection] --> RECOG[Compatibility recognition only]
LEGACY -. forbidden .-> CORE
~~~
The access-control adapter writes each observation before constructing the match, writes the
canonical match bytes next, and only then calls core with the immutable references. The
resolver exposed to core is read-only. A regex or name hit can preserve recognition during
migration but cannot substitute for typed structural observations.
## Requirements Traceability
| Requirement | Components |
|---|---|
| 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7 | mapping registry, registration adapter, fact initialization, coverage |
| 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10 | fact interpreter, abstract domain, operations, enforcement, witness matcher |
| 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10 | symbol resolution, summaries, path materializer |
| 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7 | pair judgment, checker integration, prover findings |
| 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7 | prover arm, four-arm development comparison |
| 6.1, 6.2, 6.3 | context caches, deadline state, runner budget gate |
| 7.1, 7.2, 7.3, 7.4, 7.5, 7.6 | completeness, prover coverage, runner funnel |
| 8.1, 8.2, 8.3 | finding contract, trace references, proof-arm filter |
| 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7 | canonical trace and record provenance |
| 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7 | strict CFG dominance and legacy order compatibility |
| 11.1, 11.2, 11.3, 11.4, 11.5, 11.6, 11.7, 11.8 | abstract domain, path and operation aggregation, transition report |
| 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9 | acquisition record, freeze manifest, admission runner |
| 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8 | access-control family contract, reviewed fact authority, structural matcher, artifact writer, detection-core conformance, caller cutover |
## Components and Interfaces
### Abstract evidence domain
~~~python
class EvidenceRelation(Enum):
MUST = "must"
MAY = "may"
NO = "no"
UNKNOWN = "unknown"
class PairJudgment(Enum):
UNGUARDED = "unguarded"
DISCHARGED = "discharged"
INDETERMINATE = "indeterminate"
@dataclass(frozen=True)
class Completeness:
is_complete: bool
reasons: tuple[str, ...] = ()
@dataclass(frozen=True)
class ResourceRef:
kind: Literal["exact", "aliases", "request_global", "unknown"]
values: tuple[str, ...] = ()
@dataclass(frozen=True)
class AuthorityRef:
values: tuple[str, ...]
is_unknown: bool = False
~~~
The partial order places definite evidence below possible or unknown evidence. Join never converts `MAY` or
`UNKNOWN` into `MUST` or `NO`. The precision order is:
~~~text
MUST <= MAY <= UNKNOWN
NO <= MAY <= UNKNOWN
~~~
`MUST` and `NO` are incomparable. The alternative-path join is fixed by this table:
| join | MUST | NO | MAY | UNKNOWN |
|---|---|---|---|---|
| MUST | MUST | MAY | MAY | UNKNOWN |
| NO | MAY | NO | MAY | UNKNOWN |
| MAY | MAY | MAY | MAY | UNKNOWN |
| UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN |
Three separate operations prevent one table from being used for the wrong quantifier:
1. `join_alternatives` uses the table above when alternative call targets or control paths contribute evidence.
2. `conjoin_dimensions` combines kind, scope, resource, authority and dominance for one witness: any `NO`
produces `NO`; otherwise any `UNKNOWN` produces `UNKNOWN`; otherwise any `MAY` produces `MAY`; only
all-`MUST` produces `MUST`.
3. `aggregate_witnesses` asks whether any witness definitely governs one obligation: any `MUST` produces
`MUST`; otherwise `UNKNOWN` takes precedence over `MAY`, and `MAY` takes precedence over `NO`.
An empty witness set is `NO` only when discovery and facts are complete; otherwise it is `UNKNOWN`.
For one complete path, all required obligations at `MUST` yield `discharged`; any `MAY` or `UNKNOWN` yields
`indeterminate`; otherwise any `NO` yields `unguarded` and every `NO` obligation is reported missing.
Thus a mixed `[MUST, NO]` obligation vector is unguarded for the `NO` obligation. Resource
alias sets join by finite union; an unbounded or unresolvable value joins to `unknown`. Authority implication is
read from promoted facts and contributes its fact identifier to the match evidence.
### Symbol resolution
~~~python
@dataclass(frozen=True, order=True)
class SymbolId:
module: str
enclosing_type: str | None
name: str
signature: tuple[str, ...] | None
@dataclass(frozen=True)
class CallTargetSet:
targets: tuple[SymbolId, ...]
completeness: Completeness
~~~
The symbol index covers repository-defined declarations and inheritance. Exact receiver and full signature produce
one complete target. Partial type or signature information produces a finite candidate set with an incompleteness
reason. No call falls back to a global bare-name key. Base declarations attach through explicit descendant edges.
### Function summaries and paths
~~~python
@dataclass(frozen=True)
class FunctionSummary:
symbol: SymbolId
operations: tuple[Operation, ...]
witnesses: tuple[Discharge, ...]
calls: tuple[CallTargetSet, ...]
completeness: Completeness
~~~
Summaries are computed by a monotone worklist over strongly connected call components. Their finite contents grow
by union until stable. A budget cutoff stops growth, records the cutoff and leaves affected summaries incomplete.
Concrete paths are then materialised from recognised entries through exact targets for evidence. Candidate edges
may contribute diagnostic traces but cannot complete a final path.
### Witness matching and judgment
~~~python
@dataclass(frozen=True)
class WitnessMatch:
relation: EvidenceRelation
resource_relation: EvidenceRelation
authority_relation: EvidenceRelation
evidence: tuple[str, ...]
@dataclass(frozen=True)
class PathJudgment:
judgment: PairJudgment
missing: tuple[str, ...]
completeness: Completeness
matches: tuple[WitnessMatch, ...]
~~~
A witness governs an obligation only when kind, scope, resource, authority and dominance are all `MUST`.
Any `MAY` or `UNKNOWN` required relation makes the path indeterminate. With complete evidence and no governing
witness for a required kind, the path is unguarded. Multiple paths aggregate as follows:
- any complete unguarded path -> `unguarded`;
- complete enumeration and every path discharged -> `discharged`;
- otherwise -> `indeterminate`.
The checker gains an opt-in abstract mode. Legacy callers keep the current boolean result and order-based default.
### Strict CFG dominance
`CfgDominance.relation` returns an evidence relation and a guard effect. Prover mode never calls
`OrderDominance` when a graph, line mapping or construct is unavailable; it returns `UNKNOWN` with
`cfg_unavailable` or `line_without_node`. Route and declaration scopes return `MUST` only after exact handler
attachment. The legacy dominance class remains unchanged.
### Target knowledge and acquisition
The mapping registry and fact interpreter are target-aware adapters. The initialization job has:
- extraction mode for existing strategies and fact rows;
- proposal mode for source- or documentation-derived candidate knowledge;
- verification that checks source, version, region and concrete matches;
- promotion that writes only verified rows or enabled strategies;
- an acquisition record containing input revision, evidence digest, accepted/rejected proposals and output digest.
Admission mode prevents the acquisition process from receiving benchmark labels or fix locations. A changed output
digest after labels are opened spends the population.
### Reviewed detection-core fact adapter
The production contract is family-specific but target-neutral. Its initial reviewed
identity is `family_id=access_control`, `family_version=2.0.0`,
`schema_version=openultrasast.detection-core.evidence-packet.v2`, and
`ecosystems=(express,)`; later ecosystems require a reviewed contract-version change. It
uses the security object `entry_operation_authorization` with required text identity fields
`operation` and `boundary`. Its closed vocabulary is:
| Role | Contract |
|---|---|
| `operation` | no fields; `OPERATION_EFFECT`; `product_or_reviewed_fact` |
| `constraint` | no fields; `CONSTRAINT_EFFECT`; `product_or_reviewed_fact` |
| `security_object` | required text fields `operation` and `boundary`; no relation kinds; `product_or_reviewed_fact` |
| `framework_factory` | no fields; `REGISTRATION`; `product_or_reviewed_fact` |
| `framework_instance` | no fields; `REGISTRATION`; `product_or_reviewed_fact` |
| `entry_handler` | no fields; `INVOCATION`; `product_or_reviewed_fact` |
| `entry_registration` | no fields; `REGISTRATION`; `product_or_reviewed_fact` |
The decision rules are exactly `unguarded -> violation`, `discharged -> satisfied`, and
`indeterminate -> indeterminate`. The two determinate decisions require `operation`,
`constraint`, and `security_object` with allowed statuses `(COMPLETE,)`; the indeterminate
decision requires `constraint` with allowed statuses `(INCOMPLETE, UNRESOLVED)`. Residual question
`is_path_authorized` requires `operation`, `constraint`, and `security_object` and permits
all three decisions. The initial indeterminate-reason set includes
`missing_constraint_relation` and `ambiguous_constraint_relation`; additions require a
contract-version change and downstream revalidation.
```python
@dataclass(frozen=True)
class EntryFactAuthorityBundle:
contract: FamilyContract
packs: tuple[FactPack, ...]
recognition_projection: ContentRef
lineages: tuple[LineageManifest, ...]
custody_policy: PopulationCustodyPolicy
review_policy: ReviewIndependencePolicy
matcher_revision: str
matcher_behavior_digest: str
matcher_lineage_manifest_digest: str
authority_review: IndependenceAttestation
bundle_digest: str
@dataclass(frozen=True)
class EntryAuthorityBootstrapPolicy:
reviewed_bundle_reviewer_digests: tuple[str, ...]
forbidden_identity_pairs: tuple[tuple[str, str], ...]
trusted_store_identities: tuple[str, ...]
maximum_bundle_bytes: int
policy_digest: str
@dataclass(frozen=True)
class EntryFactAuthorityContext:
bundle: EntryFactAuthorityBundle
custody: PopulationCustodyPort
resolver: ContentResolver
class FactMatchArtifactWriter(Protocol):
def persist_observation(self, value: StructuralObservation) -> ContentRef: ...
def persist_match(self, value: FactMatchEvidence) -> ContentRef: ...
```
The authority review subject is the canonical bundle payload digest with
`authority_review` omitted. The host supplies the bootstrap policy separately; neither the
bundle nor mapping code may choose its own trusted reviewers or stores. Validation
re-derives policy, attestation, disclosure, pack, projection, lineage, and bundle digests;
requires producer-reviewer separation; applies forbidden identity pairs; resolves every
referenced byte from an admitted store; and refuses missing, duplicate, or substituted
language packs. Because `FactPack.language` is singular, a multi-language target has one
reviewed pack per language with shared semantic intent but distinct canonical identities.
The shipped host trust policy is loaded from `bootstrap.json`, never referenced as authority
by the bundle it authenticates, and is replaceable by host configuration. The artifact
writer receives an explicit store root, store identity, and retention-policy digest; it
writes content-addressed canonical bytes and returns a read-only resolver plus re-derivable
storage receipts admitted by that bootstrap policy.
#### Installed authority wire and review ceremony
The checked-in authority uses closed canonical JSON records; decoders reject unknown or
missing fields before constructing typed objects. `bootstrap.json` has schema
`openultrasast.access-control.entry-authority-bootstrap.v2` and exactly the five
`EntryAuthorityBootstrapPolicy` fields. `authority.json` has schema
`openultrasast.access-control.installed-entry-authority.v1` and exactly:
- `pack_files`: the ordered pair `express.javascript.v2.json`,
`express.typescript.v2.json`;
- `recognition_projection`, lineages, custody policy, review policy, matcher revision,
matcher behavior digest, matcher lineage digest, authority review, and bundle digest;
- no trust-root, reviewer-admission, or store-admission fields.
Each v2 pack file is canonical JSON over every public `FactPack` field and the complete
nested `SemanticFact`, structural-requirement, relation-effect, knowledge-evidence,
attestation, and `ContentRef` values. Enum values use their public string values. The
installed decoder accepts only `FACT_PACK_SCHEMA`, re-encodes the typed value, and requires
byte equality so alternate/unknown encodings cannot share authority. `authority.json`
embeds policies and lineages because those values participate directly in the typed bundle;
review evidence and disclosures remain immutable `ContentRef` values.
All referenced bytes live under `artifacts/sha256/<content_digest>`. The installed resolver
validates the lowercase SHA-256 digest first and derives the filesystem basename solely from
`ContentRef.content_digest`; `artifact_id` is diagnostic metadata and is never interpreted as
a path. The resolver verifies byte length and digest, and derives
the `StorageReceipt` from the bootstrap-admitted store identity
`openultrasast-installed-entry-artifacts-v1`, the reference retention-policy digest, content
digest, and length. Receipts are derived, not separately mutable files. Pack filenames are
fixed by the closed authority schema; their typed normalized identities are covered by the
bundle digest, so substituting either pack changes authority even though the filenames are
not trust roots.
The approved repo-local ceremony records three distinct canonical identity manifests under
schema `openultrasast.access-control.authority-identity.v1`. Every manifest has exactly
`schema_version`, `role`, `name`, and `approval_basis`; its identity is the digest of its
canonical bytes, and the loader rejects unknown fields or noncanonical bytes. The fixed rows
are:
| File | `role` | `name` | `approval_basis` |
|---|---|---|---|
| `generator.json` | `producer` | `openultrasast-entry-pack-generator-v1` | `generated-from-reviewed-entry-data` |
| `maintainer-reviewer.json` | `reviewer` | `openultrasast-maintainer` | `maintainer-session-approval:2026-11-01` |
| `custodian.json` | `custodian` | `openultrasast-installed-entry-custodian-v1` | `checked-in-artifact-custody` |
The ceremony loader requires bootstrap reviewer admission to equal the reviewer-manifest
digest, custody-policy admission to equal the custodian-manifest digest, authority review
producer/reviewer identities to equal the generator/reviewer digests, and pack evidence to
use the same producer/reviewer identities. The maintainer explicitly approved this ceremony
on 2026-11-01; that approval authorizes recording the reviewer identity but does not let the
generator choose a different reviewer. Bootstrap admits only that reviewer digest and the
installed store identity. Producer and reviewer must differ, the pair remains subject to the
forbidden-pair policy, conflict disclosures must be canonical and resolved, and any identity
field or approval-basis change creates a new digest and invalidates these bindings until a
new human review.
The structural adapter consumes promoted recognition output and repository structure. For
each fact it emits the complete typed requirement set (qualified target, AST shape,
receiver, call target, argument shape, or configuration as declared), persists every
canonical `StructuralObservation`, builds `FactMatchEvidence`, persists
`fact_match_bytes(match)`, and then calls detection-core with the match plus its content
reference. Match construction never accepts a future self-reference: the reference is an
input only to core application after persistence.
#### Initial compatibility migration evidence
The first reviewed migration preserves the existing Express recognition rows as data, not
engine vocabulary. `express.javascript.v2.json` and `express.typescript.v2.json` carry the
same semantic expansion under distinct language-scoped pack identities. The authenticated
projection uses schema `openultrasast.entry-recognition-projection.v1` and closed row fields
`legacy_fact_id`, `languages`, `defines_handler`, `handler_kind`, and `pattern`; its content
reference is covered by the authority bundle digest. The projection keeps legacy regular
expressions and IDs byte-stable while the v2 packs use these exact facts:
| Legacy data row | Reviewed v2 fact | Required observations | Effect |
|---|---|---|---|
| `entry:express:app-route` | for each `method` in `get`, `post`, `put`, `delete`, `patch`, `all`, `use`: ID `entry:express:app-route:<method>`, fact role `entry_registration` | `qualified_target`: `QUALIFIED_NAME`, field `qualified_target`, expected `app.<method>`; `call_shape`: `AST_SHAPE`, field `call_shape`, expected `member_call`; `receiver`: `RECEIVER_TYPE`, field `receiver`, expected `express_application`; `arguments`: `ARGUMENT_SHAPE`, field `arguments`, expected `route_path_and_handler` | kind `REGISTRATION`, role `entry_registration`, source `framework_instance`, target `entry_handler`, conditions `()` |
| `entry:express:router-route` | for the same methods: ID `entry:express:router-route:<method>`, fact role `entry_registration` | `qualified_target`: `QUALIFIED_NAME`, field `qualified_target`, expected `router.<method>`; `call_shape`: `AST_SHAPE`, field `call_shape`, expected `member_call`; `receiver`: `RECEIVER_TYPE`, field `receiver`, expected `express_router`; `arguments`: `ARGUMENT_SHAPE`, field `arguments`, expected `route_path_and_handler` | kind `REGISTRATION`, role `entry_registration`, source `framework_instance`, target `entry_handler`, conditions `()` |
| `entry:express:express-app` | ID `entry:express:express-app`, fact role `framework_instance` | `qualified_target`: `QUALIFIED_NAME`, field `qualified_target`, expected `express`; `call_shape`: `AST_SHAPE`, field `call_shape`, expected `factory_call`; `binding`: `ARGUMENT_SHAPE`, field `binding`, expected `application_instance_binding` | kind `REGISTRATION`, role `framework_instance`, source `framework_factory`, target `framework_instance`, conditions `()` |
The promoted route-method values are read from reviewed projection data rather than copied
into engine branches. A pattern hit alone is `INCOMPLETE`; it cannot produce
`FactMatchEvidence`, enter readiness, or support admission. The no-context legacy loader is
removed only after `mapping/legacy.py` uses the authority-bearing path, a repository census
finds no caller that treats the projection as v2 evidence or bypasses authority, and the
core conformance handoff passes. `analyze_entry_points` remains the stable public entry;
bootstrap and adapter construction occur at its target-aware mapping boundary rather than
being pushed into neutral engine callers.
### Prover integration
`prove_change` orchestrates cached reads, recognition, resolution, summaries, paths, strict dominance and
judgment. It emits findings only for `unguarded` pairs and preserves all three judgments as traces. Coverage
contains stage counts, reason counts, pair judgments and exploration cutoffs. Exceptions are recorded at the stage
that raised them.
### Development and admission runners
Before comparison, the development runner materialises and validates two immutable bundles:
~~~python
@dataclass(frozen=True)
class AnalyzerBundle:
analyzer_revision: str
fact_set_digest: str
acquisition_record_digest: str
configuration_digest: str
~~~
The baseline bundle points to the analyzer revision and fact set that produced the immutable 2026-10-28 records;
the candidate bundle points to the amended equivalents. The runner refuses a four-arm comparison if either
revision or fact artifact cannot be materialised and digest-verified. It then registers baseline
analyzer/baseline facts, baseline analyzer/candidate facts, candidate analyzer/baseline facts and candidate
analyzer/candidate facts. This separates knowledge coverage from engine behavior.
The admission runner accepts only a frozen manifest naming analyzer, facts, acquisition, configuration and
population digests. The population manifest is produced and reviewed by `valid-detection-benchmark`; this spec
only consumes and validates it. The runner verifies its review and isolation fields, scores its paired
vulnerable/fixed cases and reviewed ordinary labels, refuses zero-byte or stale-anchor records, fails any change
over 120 seconds, and never converts indeterminate pairs to clean results.
## Data Models
### Canonical trace
Each trace carries:
- entry identity and target-knowledge evidence;
- exact hops and call-target completeness;
- operation kind, resource and authority;
- witnesses with kind, scope, provenance, resource, authority, dominance and match relations;
- control context and guard effects;
- completeness reasons and final path judgment;
- analyzer, fact-set and input digests.
Repository identity, repository size and function-size features remain outside decision input.
### Run manifest
The manifest contains population role (`development` or `admission`), immutable input digest, analyzer revision,
fact-set digest, acquisition-record digest, configuration digest, label-review state and spent-state marker.
The analyzer/fact bundle is a separate immutable input whose artifacts must be materialisable by revision and
digest; a manifest cannot redefine or synthesize the baseline.
## Error Handling
- Parse, recognition, resolution, summary, CFG and judgment exceptions become stage-specific incompleteness reasons.
- Deadline, fan-out, depth and recursion limits preserve accumulated evidence and return indeterminate affected
pairs.
- Zero-byte reads and stale anchors are instrument failures that invalidate the run.
- Missing or mismatched manifest digests invalidate admission rather than falling back to development mode.
## Testing Strategy
- Unit: the complete join table, dimension conjunction, witness aggregation and monotonicity laws; symbol
collisions and overload candidates; resource aliases and authority
implication; SCC termination; CFG unknown behavior; three-way aggregation.
- Metamorphic: renaming a type preserves judgment; adding an unresolved overload can only preserve a judgment or
move it to indeterminate; removing semantic evidence cannot switch directly between discharged and unguarded.
- Integration: exact unguarded and discharged paths; a guard in a called helper; registration-site entries; base
attachment; resource mismatch; authority insufficiency; ambiguous calls; CFG unavailable; budget cutoff.
- Runner: baseline/candidate bundle materialisation, four-arm attribution, paired vulnerable/fixed scoring,
reviewed-manifest validation, freeze-digest guard, zero-byte and stale-anchor refusal, indeterminate rate, and
hard 120-second gate.
- Fact authority: bootstrap-policy independence, canonical bundle identity, reviewer/store
admission, forbidden identity pairs, disclosure resolution, one pack per language, and
refusal of substituted or self-authored artifacts.
- Fact matching: canonical observation and match persistence, complete structural matches,
name-only/partial ambiguity, lineage/custody contamination, and read-only core resolution.
- Compatibility: byte-stable legacy recognition projection and proof that it cannot construct
v2 evidence, readiness, or admission; core imports remain target-neutral.
## Migration Strategy
1. Add abstract types and adapters without changing legacy checker defaults.
2. Add strict resolution, summaries and judgment behind the prover's opt-in path.
3. Migrate traces and coverage, then enable the amended development arms.
4. Freeze facts and analyzer before running a separate admission population.
5. Authenticate the existing entry projection, publish reviewed language-scoped v2 packs,
and run the authority-bearing structural adapter beside the legacy loader.
6. Pass detection-core downstream conformance, migrate production callers, then remove the
no-context compatibility path at the single-writer cutover.
Rollback is the previous prover arm and fact-set digest; baseline records remain immutable and comparable.
@@ -0,0 +1,310 @@
# Requirements Document
## Project Description (Input)
An access-control prover: a deterministic check that a protected operation is reachable from an attacker-facing
entry point without a required authorization check governing that path. The prover consumes acquired entry,
operation and enforcement facts, resolves calls and resources conservatively, emits an ordered trace for every
examined pair, and is measured separately on development and admission populations.
## Introduction
The security object for access control is not the shape of a changed function. It is an ordered path from an
attacker-facing entry to a protected operation, together with the authorization evidence that governs every
relevant control-flow and call-flow step. The 2026-10-28 development measurement established a useful baseline:
15 of 23 fix changes detected, 15.0 flagged ordinary pushes per 100, and no instrument failures. The subsequent
ablation showed that unresolved calls, over-broad resource matching, missing registration facts and silent
fallbacks account for both missed detections and false alarms.
This amendment makes uncertainty part of the result rather than a side counter. A pair is `unguarded`,
`discharged`, or `indeterminate`; degraded resolution, missing control-flow semantics, incomplete facts and
budget cutoffs cannot create a final security verdict. It also separates label-blind fact acquisition and
development regression from admission measurement on untouched repositories.
Concrete frameworks and repositories are benchmark evidence, not acceptance-criteria inventories. Target
vocabulary is acquired as reviewed data or recogniser strategies and is verified before promotion. The abstract
interpretation engine remains target-agnostic.
## Boundary Context
- **In scope**: acquired entry, operation and enforcement knowledge; exact and conservative call resolution;
resource- and authority-sensitive witness matching; bounded interprocedural summaries and concrete evidence
paths; control-flow-sensitive dominance; three-way pair judgments; failure-visible coverage; development and
admission runners.
- **Out of scope**: taint flow; the Joern engine; model calls inside the prover; vulnerability families other than
access control; client-side presentation gating; pre-push hook wiring; proof execution; hand-tuning vocabulary
from evaluation labels.
- **Adjacent expectations**: `authorization-obligations` supplies the existing checker and fact contracts;
`valid-detection-benchmark` supplies reviewed populations; `search-with-proof` consumes unguarded traces;
`pre-push-safety-net` may consume the arm only after admission passes.
## Requirements
### Requirement 1: Acquired attacker-facing entry points
**Objective:** As a maintainer, I want entry recognition to consume verified target knowledge, so that new targets
can be covered without adding target vocabulary to the analysis engine.
#### Acceptance Criteria
1. When verified target knowledge describes a declarative entry shape, the mapper shall record every matching
repository-defined handler as an entry with its source range, trust boundary and recognition evidence.
2. When verified target knowledge describes a call-site registration, the mapper shall resolve the registered
repository-defined handler and record its request methods as entries with the registration site and resolved
handler identity as evidence.
3. If a registration target cannot be resolved to repository-defined code, the mapper shall emit no entry for it
and shall record `registration_unresolved` coverage.
4. When acquired knowledge proves that an entry is intentionally public, the mapper shall record access `public`;
otherwise, the mapper shall record access `unknown` rather than infer public access.
5. The mapper shall preserve the promoted fact or strategy identifier and the concrete source evidence on every
entry record.
6. If no promoted entry knowledge covers a file, the prover shall record `framework_unrecognised` and shall not
interpret the absence of entries as a clean result.
7. When target entry knowledge is added or updated, the system shall apply it through data or a registered
recognition strategy without changing target-agnostic analysis modules.
### Requirement 2: Acquired protected operations and enforcement evidence
**Objective:** As a maintainer, I want operations and authorization evidence represented as verified knowledge,
so that the prover compares the correct action, resource and authority.
#### Acceptance Criteria
1. When promoted operation knowledge matches a call or recognised handler, the prover shall record the protected
operation, its required authorization kinds, its resource identity and any required authority.
2. When promoted enforcement knowledge matches a statement, decorator, route, policy decision or equivalent
construct, the prover shall record a witness with its scope, resource identity, authority and provenance.
3. When a handler name itself denotes a protected action, the prover shall apply that operation only to an entry
recognised for the same promoted target knowledge.
4. When a route or declared policy marks an entry public, the prover shall waive only a request-level path guard
and shall preserve resource, ownership and authority obligations.
5. When a witness binds a different resource or an insufficient authority, the prover shall not use that witness
to discharge the operation.
6. When a witness is request-global, the prover shall use it without resource equality only when the operation
fact explicitly declares that witness kind sufficient at request scope.
7. If resource identity or authority compatibility cannot be decided, the prover shall treat the witness match as
unknown rather than matching it broadly or rejecting it as definitely unrelated.
8. When one authority implies another, the prover shall apply that implication only from promoted target
knowledge and shall record the implication used in the trace.
9. When target knowledge is extracted or proposed, the acquisition workflow shall verify its source, version,
region and match evidence before promoting it to closed data or an enabled strategy.
10. If proposed target knowledge contradicts the inspected source or lacks verification evidence, the acquisition
workflow shall reject it with a reason and shall not modify promoted knowledge.
### Requirement 3: Conservative interprocedural reachability
**Objective:** As a maintainer, I want calls resolved to stable program identities and paths carried through
helpers, so that unrelated same-name code neither creates nor suppresses findings.
#### Acceptance Criteria
1. When a touched function or reachable helper contains a protected operation, the prover shall construct caller
chains from recognised entries to that operation within the configured work budget.
2. When a call target can be identified by module, enclosing type, method and signature, the prover shall keep
that identity distinct from every other same-name target.
3. When more than one call target remains feasible, the prover shall preserve the finite candidate set and shall
mark the call incomplete rather than merge the candidates under a bare name.
4. If no bounded candidate set can be established, the prover shall mark the call unresolved and shall not use it
to create a final discharged or unguarded judgment.
5. When an operation or entry is declared on a base type, the prover shall attach it only to repository-defined
concrete descendants whose resolved identities support that attachment.
6. When a helper calls another helper that contains an operation or governing authorization evidence, the prover
shall continue the analysis through that call while budget remains.
7. When recursion is present, the prover shall terminate and shall preserve the accumulated operations,
authorization effects and incompleteness reasons for the recursive component.
8. If depth, fan-out, time or another work bound truncates exploration, the prover shall record the exact cutoff
reason and shall treat affected pairs as incomplete.
9. When a complete chain is materialised, the prover shall emit its hops from entry to operation with the call
site of every transition.
10. When multiple complete chains reach the same operation, the prover shall evaluate each chain independently so
that a guarded chain cannot hide an unguarded chain.
### Requirement 4: Access-control verdicts
**Objective:** As a developer about to push, I want a finding only for a demonstrated unguarded path, so that every
alarm names a concrete security obligation and repair location.
#### Acceptance Criteria
1. When a complete path reaches a protected operation, at least one required authorization kind is proven absent,
and every other required kind is proven either governing or absent, the prover shall judge the pair
`unguarded` and shall report each absent kind.
2. When every required authorization kind is proven to govern the operation on a complete path, the prover shall
judge the pair `discharged`.
3. If the evidence is insufficient for either final judgment, the prover shall judge the pair `indeterminate`.
4. When a pair is `unguarded`, the prover shall emit one finding per operation and missing authorization kind with
family `access_control`, exact source anchor, entry, hops and evidence.
5. When a pair is `discharged` or `indeterminate`, the prover shall emit no vulnerability finding for that pair.
6. The prover shall apply the recognised-entry-is-protected premise only in its explicit opt-in mode and shall
identify that premise in the evidence.
7. The prover shall make no model call and shall return findings in the existing hunt-defect contract.
### Requirement 5: Development measurement
**Objective:** As a maintainer, I want repeatable development measurements, so that changes to facts and analysis
can be attributed without being mistaken for admission evidence.
#### Acceptance Criteria
1. When a development population is run, the runner shall write one resumable record per change with analyzer,
fact-set, population and input digests.
2. When development records are summarised, the runner shall report recall on fix changes and verified locations,
flagged ordinary pushes per 100, precision, vulnerable/fixed pair correctness, instrument failures and runtime
distributions with uncertainty intervals where defined.
3. The runner shall report a stage funnel from entry recognition through call resolution, operation discovery,
control-flow evaluation, witness matching and pair judgment.
4. The runner shall report per-target-family and per-repository coverage without disclosing reserved repository
identities in shared records.
5. When comparing an amended analyzer and amended facts, the runner shall support the four registered arms formed
by old/new analyzer crossed with old/new fact set.
6. If any record reads zero bytes for a file present in the analysed tree or anchors a finding outside files read,
the runner shall mark an instrument failure and shall refuse the summary.
7. The runner shall label results from populations used to choose facts or analyzer behavior as development or
regression evidence and shall not present them as admission evidence.
### Requirement 6: Pre-push budget
**Objective:** As a developer, I want the prover to finish inside the pre-push window, so that eligible detection
can run on every push.
#### Acceptance Criteria
1. When a measured change exceeds 120 seconds, the runner shall fail the budget gate and shall identify the
change, stage and elapsed time.
2. The prover shall reuse per-change file, text, syntax, call, entry, symbol and summary caches and shall not read
the product tree more than once per change.
3. If analysis stops because of a deadline, the prover shall return incomplete coverage and indeterminate affected
pairs rather than a partial clean verdict.
### Requirement 7: Failure-visible coverage
**Objective:** As a developer, I want the tool to state what it could not decide, so that silence never reads as a
clean bill of health.
#### Acceptance Criteria
1. The prover shall report files by language, recognised entry families, entries, reached and unreached targets,
operations, discharged pairs, unguarded pairs and indeterminate pairs.
2. The prover shall count unchecked or incomplete work by specific reason, including parse failure, unsupported
language, unrecognised target, absent facts, unresolved registration, ambiguous or unresolved call, exploration
cutoff, unavailable control flow, unknown resource, unknown authority and analysis exception.
3. If no target family is recognised for a repository, the runner shall mark the repository `uncovered` and shall
report its recall separately from covered recall.
4. When a recognition, resolution, summary or judgment pass raises an exception, the prover shall record the
exception class at that stage and shall not fold it into a quiet coverage reason.
5. When the same pair has multiple incompleteness reasons, the prover shall preserve every reason in the trace and
aggregate each reason in coverage.
6. The runner shall report the indeterminate rate separately and shall not include indeterminate pairs in either
clean or discharged counts.
### Requirement 8: Proof-arm feed
**Objective:** As the proof arm, I want each unguarded finding to carry a complete analysis witness, so that a
demonstration can be attempted without re-deriving the decision.
#### Acceptance Criteria
1. When a finding is emitted, it shall carry the entry record, exact hop list, operation, missing authorization
kind, resource and authority relations, pair judgment, trace identifier and completeness evidence.
2. The finding identifier shall remain stable for the same operation, source anchor and missing authorization
kind across deterministic replays.
3. If the pair is indeterminate, the prover shall expose its trace only as diagnostic coverage and shall not send
it to the proof arm as a vulnerability finding.
### Requirement 9: Ordered traces for every examined pair
**Objective:** As a maintainer, I want every examined pair represented by the same deterministic trace, so that
decisions and degradations can be inspected and compared.
#### Acceptance Criteria
1. When the prover examines an entry-operation path, it shall emit one trace containing the entry, ordered hops,
operation, authorization witnesses, resource and authority relations, control-flow relations, completeness
state and pair judgment.
2. The prover shall emit traces for unguarded, discharged and indeterminate pairs alike.
3. When an argument or resource is forwarded through a call, the trace shall preserve its origin through the
resolved hop; if origin cannot be followed, the trace shall record `unknown`.
4. The trace shall carry target knowledge identifiers and source evidence needed to reproduce the judgment but
shall omit repository identity, file-size and repository-size features from decision input.
5. The trace shall serialise to one deterministic canonical text form and one deterministic canonical JSON form.
6. When a work bound or fallback would have been required, the trace shall record the cutoff or unavailable
semantic fact rather than silently substituting a weaker analysis.
7. The runner shall persist traces and their analyzer, fact-set and input digests with the corresponding record.
### Requirement 10: Control-flow-sensitive dominance
**Objective:** As a maintainer, I want authorization evidence to count only when control flow guarantees it governs
the operation, so that avoidable or ignored checks do not discharge obligations.
#### Acceptance Criteria
1. When authorization evidence appears before an operation, the prover shall determine whether every feasible
path to the operation passes that evidence.
2. When a guard's failing branch terminates or the operation exists only in the passing branch, the prover shall
record the guard effect and may treat it as governing the operation when resource and authority also match.
3. When a predicate result is ignored or appears only on some paths, the prover shall not treat it as a governing
authorization check.
4. When an operation is inside a loop, exception region or conditional branch, the trace shall record that control
context and any exception handling that can bypass the guard.
5. When route- or declaration-scoped authorization evidence covers the whole handler, the prover shall preserve
that declared scope without inventing a statement location.
6. If control-flow semantics or source-to-control-flow correspondence is unavailable, the prover shall mark the
affected relation unknown and the pair indeterminate rather than decide it by textual order.
7. The existing order-based checker mode shall remain available to callers that do not opt into prover semantics.
### Requirement 11: Three-way abstract judgment
**Objective:** As a maintainer, I want uncertainty to propagate monotonically through the analysis, so that losing
precision cannot manufacture a vulnerability or a discharge.
#### Acceptance Criteria
1. The prover shall represent call resolution, path completeness, resource compatibility, authority compatibility
and dominance with values that distinguish definite, possible, absent and unknown evidence.
2. When all required evidence on a complete path is definite and at least one required obligation has no governing
witness, the prover shall produce `unguarded` and shall report every such obligation, including when other
required obligations are definitely governed.
3. When every required obligation has a definite governing witness on a complete path, the prover shall produce
`discharged` for that path.
4. If any required relation is possible or unknown, or path enumeration is incomplete, the prover shall produce
`indeterminate` unless another complete path already proves `unguarded`.
5. When multiple paths reach one operation, the prover shall aggregate them as `unguarded` if any complete path
is unguarded, `discharged` only if enumeration is complete and every path is discharged, and `indeterminate`
otherwise.
6. When analysis precision is reduced by adding an unresolved candidate or removing semantic information, a final
judgment shall remain unchanged or move to `indeterminate`; it shall not switch directly between `unguarded`
and `discharged`.
7. The prover shall keep abstract-domain operations deterministic and independent of target names.
8. The runner shall expose judgment counts and transitions between compared arms so that precision loss cannot be
hidden inside aggregate recall or false-alarm metrics.
### Requirement 12: Label-isolated admission workflow
**Objective:** As a maintainer, I want admission measured on untouched, reviewed evidence after the analyzer and
facts are frozen, so that a better development score is not mistaken for generalisation.
#### Acceptance Criteria
1. When an admission run is prepared, the workflow shall freeze the analyzer revision, promoted fact-set digest,
acquisition record, configuration and population manifest before opening admission labels.
2. While acquiring target knowledge for an admission repository, the workflow shall use repository source and
approved documentation without vulnerability, fix-location or ordinary-change labels.
3. If acquisition observes evaluation labels or is changed in response to an admission result, the workflow shall
mark that population spent and shall not use it for admission.
4. When ordinary changes are prepared for a false-alarm denominator, the benchmark workflow shall review and
remove or relabel changes that contain real missing-authorization defects before scoring.
5. When a vulnerable case has a fixed twin, the admission run shall require the expected unguarded trace on the
vulnerable side and quiet or discharged behavior at the corresponding fixed operation.
6. When an amended prover is considered for the G1 arm, the workflow shall measure it on untouched repositories
that were not used to choose facts, recognition strategies, abstract-domain behavior or thresholds.
7. The admission report shall include recall, verified-location recall, flagged ordinary pushes per 100,
precision, paired vulnerable/fixed correctness, indeterminate rate, coverage, runtime and instrument failures
with sample counts and uncertainty intervals.
8. If the budget gate, instrument gate or pre-registered quality criteria fail, the workflow shall return NO-GO
and shall not enable the arm in pre-push admission.
9. The workflow shall retain the current 23-repository population as development regression evidence and shall
require a separate untouched population for admission claims.
### Requirement 13: Reviewed detection-core fact authority and migration
**Objective:** As a maintainer, I want access-control target knowledge translated into independently reviewed,
persisted detection-core evidence, so that entry recognition can support later vulnerability-class specs without
making the neutral core target-aware or allowing a pattern match to authorize itself.
#### Acceptance Criteria
1. The access-control adapter shall define a versioned production family contract whose security object, roles,
decisions, residual questions and incompleteness reasons exactly represent access-control operation,
constraint and entry-registration semantics.
2. For each promoted target-knowledge set, the adapter shall provide reviewed language-scoped fact packs and an
independently authenticated authority bundle naming the contract, pack references, review and custody
policies, semantic-fact and matcher lineages, matcher identity and every immutable evidence reference.
3. The host bootstrap policy shall be external to the bundle it authenticates, shall constrain admitted reviewer
and store identities, shall enforce producer-reviewer separation and forbidden identity pairs, and shall
refuse unresolved conflict disclosures or a non-canonical bundle digest.
4. When legacy recognition knowledge is migrated, the adapter shall preserve its recognition behavior as a
canonical compatibility projection while mapping each promoted row to explicit contract roles, typed
structural requirements and declared relation effects. The projection alone shall remain non-authoritative.
5. For every structural match, the adapter shall persist canonical bytes for each `StructuralObservation` and
the resulting `FactMatchEvidence` before invoking detection-core, then supply the immutable content references
through a read-only resolver. Detection-core shall not write or synthesize those artifacts.
6. The adapter shall bind fact and matcher lineage, custody and evaluation-population identity so that label
access for the same admission population makes the match incomplete or refused; result labels shall not
influence recognition or matching for that admission run.
7. The adapter shall pass the detection-core downstream conformance gate for complete evidence and shall remain
incomplete or refused for name-only, partial, substituted, self-authored, legacy-only or contaminated inputs,
without detection-core importing access-control or target modules.
8. Before the compatibility path is removed, the production entry-mapping caller shall load the externally
bootstrapped authority-bearing adapter, and a repository census shall report zero callers that use the legacy
projection as v2 evidence or bypass the reviewed authority context.
@@ -0,0 +1,350 @@
# Research & Design Decisions
## Summary
- **Feature**: `access-control-prover`
- **Discovery Scope**: Extension of `authorization-obligations` (checker, facts, dominance) and the hunter's
per-change context; gap analysis against the code on 2026-10-14 (commit 9feb8f1).
- **Key Findings**:
- The checker already takes `paths` and `entries` and already decides reached-ness and along-path dominance
(`semantic/obligations/check.py::_reached`, `dominance.py::_dominates_along`); nothing supplies paths today,
so every operation outside an entry handler is invisible to it.
- The entry-point mapper (`mapping.py`) recognises Flask, Django, Express, Laravel routes, WordPress hooks,
Next and Remix route modules partially, and nothing for Java: no Stapler `doXxx` web method, no Servlet or
Jetty handler, no JAX-RS resource. Seventeen of the pool's 23 fixes are Stapler web methods.
- The hunter's `Context` (hunt/producers.py) caches product files, texts, file IR, a call index and the
repository's entry points per change; `source.callers_of` gives one hop of callers from the index. A bounded
upward walk from a function to an entry point is a loop over that, no new index.
- Enforcement outside the function fits the existing `Discharge` scopes `router` and `decorator`, which
`OrderDominance.dominates` accepts without a line, through the checker's `extra_witnesses` parameter. No
schema change to the facts loader is needed for them; the facts files gain operation rows only.
- `benchmarks/valid/metrics.py` scores replay-shaped records with `benchmarks.unseen.score`; the hunter arm
(`benchmarks/valid/hunter_arm.py`) writes that shape. A prover arm writing the same shape is scored by the
same code, so its recall and false alarms per 100 stand beside the hunter arms.
- The pool's fixes by shape: 17 Jenkins web methods (`doTestConnection`, `doFill...Items`, `doCheck...`,
`doValidate...`, `doChangeSubmit`, `doNotifyCommit`, `doPython`, `persistValue`, `create`) missing
`checkPermission`; one Jetty service without authentication (CWE-306); one Tornado terminal websocket open
without authentication (CWE-306); one Laravel Livewire `store` without authorization (CWE-285); one Symfony
controller action (CWE-284); one Remix action deactivating a schedule by id without an ownership check
(CWE-639); one Flarum JavaScript extension (CWE-863, out of reach for this prover: client-side permission
gating, counted as uncovered).
## Research Log
### Which checker parameters exist and what they expect
- **Context**: avoid a second checker.
- **Sources Consulted**: `src/openultrasast/semantic/obligations/check.py` (`check_obligations`, `_reached`,
`_relevant`), `dominance.py` (`OrderDominance`, `_dominates_along`, `_hop_index`), the approved design of
`authorization-obligations` (Dominance, Checker, EntryPoints sections).
- **Findings**: `paths` is a sequence of objects with a `hops` attribute; each hop is `"path::function"` or a
`(node, line)` pair; a path reaches an operation when its last hop equals `f"{op.path}::{op.function}"`. With
no paths, only operations inside entry handlers (`entries`) are reached. A witness of scope `hop` on an
earlier hop dominates along the path. Witness scopes `decorator` and `router` dominate without a line.
- **Implications**: the prover produces `PathRecord(hops=(...))` objects ending at the operation's function and
passes them as `paths`; it produces router and decorator witnesses as `extra_witnesses`; it needs no change to
the checker's logic. One gap: the checker builds `operations` from the IRs it is given, so the prover passes the
IR of every file on a path, not only the touched file.
### Where entry points come from
- **Sources Consulted**: `src/openultrasast/mapping.py` (`analyze_entry_points`, `_entry`, the per-framework
recognisers around lines 260-930), `EntryPointRecord` fields.
- **Findings**: recognisers are code, one block per framework, each calling `_entry(target, start, end, handler,
name, kind, access, trust_boundary, evidence, conditions, ...)`. No Java recogniser exists. `frameworks.toml` is
a lexicon for priors and stratification, not for entry recognition.
- **Implications**: add recognisers in `mapping.py` following the existing block pattern (Stapler, Jetty,
Symfony attributes, Livewire, Tornado, Remix exports where missing). Each names its evidence.
### How a rule arm is scored
- **Sources Consulted**: `benchmarks/valid/metrics.py` (`summarize`, `scoring_records`), `benchmarks/unseen/score.py`
(`catches`, `location_match`, `false_alarm`, `coverage`), `benchmarks/valid/hunter_arm.py` (`run`, record
writing), `src/openultrasast/hunt/metric.py::search_outcome`.
- **Findings**: a record needs `id`, `kind`, `repo`, `family`, the change's `path`/`function_lines` (copied from
the pool), `findings` with `path`, `line`, `family`, and `coverage` fields the scorer reads; a push is caught
when a finding of the change's family lands within the tolerance of a verified location.
- **Implications**: the prover arm copies the hunter arm's record envelope and instrument fields (bytes read per
changed file, head verified), writes per-change files, and calls `benchmarks.valid.metrics.summarize`.
### Budget
- **Findings**: the second screen's feature table took 53 minutes for 483 changes with every producer, 7 seconds
per change on average and 117 seconds for the slowest seatunnel change; the caller index is the dominant cost
and is already cached per change. A depth-3 upward walk adds a few index lookups per function.
- **Implications**: the 120-second bound of Requirement 6 is realistic; the runner measures it per change.
## Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|---|---|---|---|---|
| Extend the checker's inputs (chosen) | Supply paths, entries and witnesses to `check_obligations` from the hunter's context | One checker, one dominance rule, facts stay closed data | Order dominance is textual inside a function; nesting-aware dominance (`guard-dominance-regime`) never existed | Honest for "check present before operation"; the Jenkins shape |
| New prover module with its own graph | Build a call graph and CFG per change | Precise dominance | Weeks of work, duplicates the checker, no CFG for Java or PHP in the tree | Rejected for this increment |
| Joern engine | Dataflow and call graph from the engine | Precise | 225 s per batch, pilot produced no findings, no PHP in the budget | Rejected as the gate; usable later as a proof backend |
## Design Decisions
### Decision: paths from the caller index, not a call graph
- **Context**: Requirement 3.
- **Alternatives Considered**: a per-change call graph over all product files; the engine's call graph.
- **Selected Approach**: an upward walk from the function through `source.callers_of` on the cached index,
bounded by depth and fan-out, stopping at the first entry point of each chain.
- **Rationale**: the index exists, is cached per change, and the pool's shapes are shallow (the operation is in the
web method or one helper below it).
- **Trade-offs**: name-based caller resolution conflates same-named functions across files; chains are kept per
file but a caller of `save` in another class may be a false hop. Accepted and visible in the evidence.
- **Follow-up**: measure how many positives are reached at depth 1, 2 and 3 on the pool.
### Decision: framework enforcement as router and decorator witnesses
- **Context**: Requirement 2.
- **Selected Approach**: a small recogniser per framework reads route registrations and attributes and emits
`Discharge` witnesses with scope `router` or `decorator` for the handlers they cover; passed through
`extra_witnesses`.
- **Rationale**: the dominance rule already accepts those scopes; the facts files stay about calls.
- **Trade-offs**: coverage is as wide as the recognisers; an unrecognised middleware name means a finding on a
guarded route. Every recogniser lists its names in the facts header and the coverage statement counts routes
with unrecognised middleware.
### Decision: an opt-in premise in the checker, not a synthesized policy
- **Context**: Codex's first attempt stopped on a real conflict: with path records and neither a declared clause
nor a sibling norm the checker returns no finding by design (gating 3), so the prover's core verdict could not
be emitted without a change.
- **Alternatives Considered**: synthesize a `DeclaredPolicy` in memory marking every recognised entry
`authenticated` (dishonest: the finding would read "route declared authenticated" when nothing was declared);
a second checker in the prover (duplicates the rules); an opt-in keyword and a distinct label (chosen).
- **Selected Approach**: `assume_entries_protected=False` keyword, label `unguarded_path`, evidence naming the
premise. Default behaviour and tests unchanged.
- **Trade-offs**: the label makes the premise visible in every finding and in the static tags; consumers that
grouped by label need the new value (revalidation trigger for `authorization-obligations`).
### Decision: no model call inside the prover
- **Context**: Requirement 4.4; the maintainer's direction after the engine pilot: the model's job is the trust
boundary question the static side cannot answer, asked after a static result exists.
- **Selected Approach**: the prover is deterministic; the intent adjudication hook of the checker stays off in
the arm; a later increment may wire it as a separate, measured arm.
## Risks & Mitigations
- Jenkins web methods whose protected operation is an outbound connection test with stored secrets
(`doTestConnection`) call no listed operation name — mitigate by adding operation rows for credential
lookups and HTTP client calls used in those handlers, checked in the clones, and by counting the handlers the
facts do not cover.
- Textual order dominance inside a function misses a check in an `else` branch — accepted for this increment;
recorded as a degradation when a witness exists but does not dominate.
- Same-name conflation in callers — chains kept per file; evidence names the hop files.
- Over-wide entry recognition (every `doXxx`) raises false alarms on helper methods — the Stapler recogniser
requires a public method on an exposed type or a Stapler-typed parameter; the pool's ordinary pushes measure
the rate.
## References
- Jenkins AccessControlled: https://javadoc.jenkins.io/hudson/security/AccessControlled.html
- Stapler web methods: https://www.jenkins.io/doc/developer/security/form-validation/ and
https://www.jenkins.io/doc/developer/security/secrets-and-credentials/
- Laravel authorization and route middleware: https://laravel.com/docs/authorization, https://laravel.com/docs/middleware
- Symfony security: https://symfony.com/doc/current/security.html
- Remix route module API: https://remix.run/docs/en/main/route/action
- Tornado authentication: https://www.tornadoweb.org/en/stable/guide/security.html
---
## Design amendment synthesis -- 2026-10-29
Context: gates 1-3 (measurements `2026-10-28-access-control-{prover,judge,ablation}`) found one cross-cutting
lever -- interprocedural resolution -- behind both the 8 recall misses (B: operations never linked; C: coarse
guards discharge) and the kept false alarms (guards present but unmodeled, name-collision resolution, base-class
projection). Requirements amendment `[2026-10-29]` (AC 1.9, 2.9, 2.10, 3.7-3.10, 5.6, 7.3); this section
records the code survey behind the design amendment (same date tag).
### Research Log: resolution seams (verified against current code)
- Caller index keys on bare function name only: `src/openultrasast/source.py:447` `callers_of` (regex over
`CodeLine` indexes); `CallSite(number, name)` at `source.py:22` carries no receiver. The prover adapter
`hunt/prover.py:161-172` re-qualifies hits to `file::enclosing_fn` by IR span containment.
- Tree-sitter IR drops receiver text: `semantic/ir.py:21-29` `CallSite` (callee is trailing name only;
`semantic/cst.py:220` `_callee_name` strips `$obj->method` / `x.y.z`); receiver must be captured in
`cst.py:203` `_call` and added to `ir.CallSite` (python-ast path `ir.py:100+` likewise).
- Target enumeration is one-hop trailing-name join: `hunt/prover.py:174-185` (`call_names` from touched
functions' IR calls; any `(path,name)` with that name and operations). Second receiver-blind seam.
- Guards on hops: `Hop.guards_before_call` populated only at `semantic/obligations/trace.py:224-231`
(serialization); the verdict path uses `semantic/obligations/check.py:312-323` `_on_caller_hop`, which today
re-judges only `path_guard`/`ownership_check` (router/decorator => True; other kinds => False). Dominance
order rule `dominance.py:52-60`; CFG deferral `cfg_dominance.py:63-65`.
- Discharge has no resource identity: `semantic/obligations/operations.py:35-44` (`detail` is identifier-only).
The only resource token today is `Operation.resource` (`operations.py:31`, `_resource_token` at 229-243:
receiver first segment, SQL table, or `resource_arg`). Suppression loop `check.py:117-122`; relevance
`check.py:326-327`; scope matching is `(path, function)` equality (`operations.py:124`, `dominance.py:38`).
- Enforcement witnesses: `semantic/obligations/enforcement.py:18` covers Tornado decorators, Symfony
`IsGranted`/`Security`/`access_control`, Laravel middleware + `authorizeResource`; called per language at
`hunt/prover.py:223-227`. In-function recognizer `_call_witnesses` (`operations.py:163-183`) matches
discharger rows by trailing name -- `denyAccessUnlessGranted`/`isGranted` already match existing rows
(`ruleset/obligations/php.toml:68-78`); `voteOnAttribute` needs no row (interface method, trailing-name
match). Jenkins guard rows exist (`java.toml:68-76`).
- Mapping: `_jetty_entries` (`mapping.py:562`) anchors at method declarations only; call-site registration
precedent exists (`_python_registered_routes` `mapping.py:945-1015`, WordPress `_PHP_REGISTRATION`
`mapping.py:657-680` with `registered_by` field on `EntryPointRecord`, `mapping.py:53-74`); java dispatch at
`mapping.py:185-198`; framework tag table `entry_framework` `mapping.py:472-488`.
- prover_arm refusal seams: per-file zero-byte loop `benchmarks/valid/prover_arm.py:86-113`; stale-anchor
validation belongs in `_prove` between lines 114 and 125 (before `record.update(findings=...)`), mirrored in
`summarize()` (184-187); type-name-only catch at `_prove` 129-133.
- Checker boundary: `authorization-obligations` owns check.py/facts.py/operations.py/paths.py/dominance.py.
The design's third extension (resource field + `resource_bound` keyword + all-kind `_on_caller_hop`) touches
`operations.py` (dataclass field) and `check.py` (keyword + relevance compare), default-off. Other
`check_obligations` callers (cli.py, hunt/producers.py, pairs.py) keep unchanged defaults.
### Decisions
1. New module `semantic/obligations/resolution.py` + additive `CallSite.receiver` (captured in `cst.py`)
rather than widening the source.py index: the index is shared with hunt search; a per-change class table
keeps resolution local, cacheable on the Context, and testable in isolation. Bare-name fallback +
`receiver_unresolved` coverage counter keeps dynamic languages (TS/PHP dynamic receivers) working as today.
2. Resource binding as checker keyword (`resource_bound`) with emission-side tokens rather than prover-side
filtering: the suppression decision is per (witness, operation) pair inside `check_obligations`; filtering
outside would need a restructured call. Default-off keeps `authorization-obligations` tests untouched.
3. Programmatic registration follows the existing call-site registration pattern (python routes, WordPress)
with class-literal resolution through `resolution.class_table` -- one owner for class resolution, reused
by the mapping recogniser and the callers adapter.
### Risks
- Dynamic receivers (TS `this.handler.get`, PHP `$container->get(...)`) stay name-keyed; the counter makes the
fallback visible instead of silent. If the pool shows large `receiver_unresolved` rates, the fact-proposal
lane (deferred injection spec) is the designated place to grow type facts, not this spec.
- `down_depth` widens target enumeration; the fan-out bound and the 120 s budget cap it. Per-repository
seconds-per-change in the runner summary will show any cost.
---
## Abstract judgment and admission amendment -- 2026-10-29
### Context
The design review after the capability-level lift found that the proposed amendment still allowed two degraded
instruments to participate in binary decisions: unresolved receivers fell back to a bare-name call key, and
unavailable CFGs fell back to source order. It also found that the fact-initialization task used known development
misses and then remeasured the same population, which is valid regression work but cannot qualify an admission arm.
### Synthesis
1. **Generalization**: call certainty, path completeness, resource compatibility, authority compatibility and
dominance are instances of one evidence-relation problem. They share a finite domain
(`MUST | MAY | NO | UNKNOWN`) and one incompleteness record instead of separate booleans and counters.
2. **Build vs adopt**: the repository already owns the semantic IR, structural CFG, obligation checker and path
records. A small abstract-domain layer and monotone function summaries extend those contracts; adopting another
graph or abstract-interpretation engine would duplicate the current architecture and violate steering.
3. **Simplification**: the prover keeps one checker and one concrete trace. Function summaries accelerate and
stabilize interprocedural discovery, but final evidence remains a materialised path. No second verdict engine is
introduced.
### Decisions
#### Decision: three-way judgment
- **Selected approach**: every path and aggregate pair is `unguarded`, `discharged`, or `indeterminate`.
- **Invariant**: less precise evidence may preserve a verdict or move it to `indeterminate`; it may not switch
directly between the two final verdicts.
- **Algebra**: `MUST` and `NO` are incomparable, both below `MAY`, which is below `UNKNOWN`. Alternative
evidence uses the least-upper-bound table; dimensions of one witness use conjunction; witnesses for one
obligation use existential aggregation. These operations are distinct so `MUST + NO` on alternative paths
becomes `MAY`, while one definite governing witness remains sufficient for the obligation it covers.
- **Consequence**: prover mode does not use textual order as a semantic fallback. Legacy checker callers retain it.
#### Decision: stable symbols and finite candidate sets
- **Selected approach**: identify repository declarations by module, enclosing type, method and available signature.
Partial resolution returns a finite candidate set plus incompleteness; unbounded resolution returns unresolved.
- **Rejected approach**: global bare-name fallback, because it conflates unrelated methods and can both invent paths
and borrow unrelated guards.
#### Decision: resource and authority are separate abstract values
- **Selected approach**: match witness kind, scope, resource and authority independently, joining each relation into
the pair judgment. Request-global guards are valid only when the operation fact explicitly allows that scope.
- **Rejected approach**: `None` as a resource wildcard. It cannot distinguish intentionally global authentication
from missing resource evidence.
#### Decision: summaries discover; concrete paths prove
- **Selected approach**: monotone summaries over strongly connected call components collect operations, witnesses,
call target sets and incompleteness. Concrete paths are materialised from exact edges for trace evidence.
- **Trade-off**: summary and path code add one engine boundary, but they remove recursion and depth handling from
ad-hoc traversal and make every cutoff explicit.
#### Decision: development attribution before admission
- **Selected approach**: compare old/new analyzer crossed with old/new facts on the current development population,
then freeze analyzer, facts, acquisition record, configuration and a separate untouched population for admission.
- **Consequence**: the 2026-10-28 numbers remain the immutable baseline. A better rerun on the same repositories is
regression evidence, not a G1 admission claim.
### Risks and mitigations
- **Indeterminate inflation**: strict uncertainty may initially lower apparent coverage. Report the stage funnel and
reason distribution; improve the dominant reason rather than weakening the lattice.
- **Summary cost**: a repository-wide symbol graph may threaten the pre-push budget. Reuse Context caches, use finite
summary sets, and fail the budget gate when any measured change exceeds 120 seconds.
- **Authority vocabulary growth**: permission implication can become target-specific code. Keep implications in
promoted facts and include the fact identifier in every match.
- **Admission scarcity**: the current pool is already development data. Source and review a separate population
through `valid-detection-benchmark`; do not relabel the current pool as untouched.
## Admission outcome (2026-10-30)
The first admission run on the untouched pilot population returned **NO-GO** (0/12 pairs,
in-family 0/3; 9/12 repositories with zero framework recognition; 0 instrument failures;
determinism 4/4). See `benchmarks/measurements/2026-10-30-access-control-admission/`. The
next lever is entry-recognition and call-resolution breadth for the valid pool's
ecosystems, gated by the same admission workflow. The pilot pool remains unspent: no
acquisition observed its labels; only admission scoring read them. Standing rules:
`steering/detection-quality.md`.
---
## Detection-core fact-authority ownership amendment (2026-11-01)
### Context
Detection-core task 9.4 reached a real boundary gap while migrating the installed entry
pack. The generic core already validates fact packs, structural matches, lineages, custody,
and relation application, but no owner supplied the production access-control contract,
reviewed target authority, structural matcher, or persisted match artifacts. Putting those
objects into core would make a vulnerability-neutral package own access-control and target
vocabulary.
The instrument was verified before diagnosis: the installed entry file was readable at 963
bytes, and the focused legacy/v2 fact suites passed. The blocker was therefore ownership and
missing authority, not an unread input or broken validator.
### Synthesis and decision
1. **Dependency direction:** `access-control-prover` imports detection-core contracts and
supplies family-owned objects. Detection-core never imports access-control or target code.
2. **Authority:** a checked-in immutable bundle references the production family contract,
one reviewed fact pack per language, recognition projection, policies, lineages, matcher
identity, and canonical evidence. A separately configured bootstrap policy authenticates
the bundle's reviewer and store; the bundle cannot trust itself.
3. **Evidence custody:** the downstream adapter persists canonical structural observations
and match bytes before calling core. Core receives only typed objects, immutable content
references, and read-only custody/resolver ports.
4. **Compatibility:** existing regex rows remain a byte-stable recognition projection during
migration. They are explicitly non-authoritative; exact typed observations are required
for complete fact evidence.
5. **Task routing:** access-control-prover tasks 14.1–14.8 define the family contract,
authenticate authority, implement installed codecs/resolution, record the approved review
ceremony, migrate reviewed data, persist evidence, match structure, and publish the
conformance handoff. Detection-core task 9.4 then runs the target-neutral handoff-acceptance gate. Access-control-prover task 14.9 subsequently migrates the
production caller and proves zero bypasses before detection-core task 10.2 cuts over.
### Risks and mitigations
- **Self-authorized trust:** keep bootstrap policy outside the bundle, re-derive every digest,
require producer-reviewer separation, and enforce forbidden identity pairs.
- **Pattern authority inflation:** preserve recognition behavior separately and require the
complete typed predicate set before core application.
- **Circular match references:** persist the completed canonical match first, then pass its
reference to core; the reference is never part of match-digest construction.
- **Admission contamination:** bind fact and matcher lineages to current custody and the
evaluation population so label access refuses the same run.
### Approved repo-local review ceremony (2026-11-01)
- The maintainer approved a repo-local ceremony after Task 14.3 correctly refused to invent
its own independent reviewer. Generator, maintainer reviewer, and custodian are separate
canonical identity manifests; the maintainer-reviewer manifest records the explicit
session approval as its basis.
- The bootstrap file admits the maintainer reviewer and installed content-addressed store;
the authenticated authority file cannot modify either admission list.
- Closed canonical decoders, fixed pack filenames, embedded policies/lineages, and a
digest-addressed artifact directory make installed bytes reproducible. Storage receipts
are derived from verified bytes and bootstrap-admitted store identity rather than stored
as independently mutable claims.
- This resolves the external identity decision but adds two prerequisite implementation
units: wire decoders/resolver first, then the closed identity/bootstrap manifests, before
the Express data migration.
@@ -0,0 +1,15 @@
{
"feature_name": "access-control-prover",
"created_at": "2026-10-14T13:48:18Z",
"updated_at": "2026-11-01T15:44:59Z",
"language": "en",
"phase": "tasks-generated",
"approvals": {
"requirements": {"generated": true, "approved": true},
"design": {"generated": true, "approved": true},
"tasks": {"generated": true, "approved": true}
},
"ready_for_implementation": true,
"depends_on": ["authorization-obligations", "valid-detection-benchmark", "hunter-learning-loop", "detection-core"],
"approval_note": "2026-10-14 maintainer directive in session: spec the prover and queue implementation with Codex (fast-track; the three gates were passed on that instruction, review the documents before the second delivery). 2026-10-29: requirements amended after the 2026-10-28 gates 1-5 measurements (trace resolution, programmatic entry facts, failure visibility). 2026-10-29 (later): maintainer instruction in session ('can you address the issues?') treated as requirements approval and as the directive to regenerate the design. 2026-10-29 (capability lift): per-target vocabulary became acquired data; engine/target split, pattern discipline and complexity caps were recorded in steering; all phases reopened. 2026-10-29 (abstract-interpretation amendment): the maintainer accepted the design-review recommendations by asking to make the amendments. Requirements and design were regenerated with a three-way judgment, explicit evidence algebra, stable symbols, resource/authority domains, label-blind acquisition and separate admission measurement; that directive is recorded as approval to regenerate tasks. The merged task plan passed an independent task-graph sanity review after two design repairs. Tasks await human approval before implementation. 2026-10-29 (implementation start): the maintainer invoked /kiro-impl access-control-prover in session; that directive is recorded as tasks approval. 2026-11-01: maintainer approved moving the production fact authority, matcher and artifact writer from detection-core into access-control-prover; Requirement 13, design ownership and the repaired tasks are approved for implementation. 2026-11-01 (repo-local review ceremony): maintainer approved recording the generator as producer, maintainer session approval as reviewer basis, and the checked-in digest-addressed directory as trusted store; Tasks 14.3-14.4 implement the wire/resolver and ceremony records before data migration in Task 14.5."
}
+421
View File
@@ -0,0 +1,421 @@
# Implementation Plan
## Completed baseline
The completed work below produced the immutable 2026-10-28 development baseline. Amendment tasks 7-13 replace
its final resolution, dominance, fact-acquisition and measurement semantics without erasing the evidence.
- [x] 1. Baseline paths and checker premise
- [x] 1.1 Add bounded caller-index paths with concrete hop records
- Delivered depth/fan-out traversal, entry stopping, file-qualified hops and unreached targets.
- Baseline fixtures cover depth, fan-out, deduplication and multiple same-name files.
- The final call semantics are superseded by tasks 8-9.
- _Requirements: 3.1, 3.9, 3.10_
- _Boundary: Concrete Paths_
- [x] 1.2 Add the opt-in recognised-entry-is-protected premise
- Existing checker defaults remain unchanged; prover evidence identifies the premise.
- _Requirements: 4.6_
- _Boundary: Obligation Checker_
- [x] 2. Baseline entry recognition
- [x] 2.1 Add the first Java entry recognisers and evidence
- Delivered development cases for declared web methods, servlets, resources and handlers.
- _Requirements: 1.1, 1.5_
- _Boundary: Mapping_
- [x] 2.2 Add the initial PHP, JavaScript, TypeScript and Python entry recognisers
- Delivered positive and negative fixture coverage with entry evidence.
- _Requirements: 1.1, 1.5_
- _Boundary: Mapping_
- [x] 3. Baseline enforcement and operation facts
- [x] 3.1 Add route-, declaration- and policy-scoped enforcement witnesses
- Delivered initial external-handler witnesses and integration fixtures.
- _Requirements: 2.2, 2.4_
- _Boundary: Enforcement Adapter_
- [x] 3.2 Close the unfinished per-target fact inventory as superseded
- The partially completed inventory is not treated as complete implementation.
- Tasks 11.3-11.7 replace it with label-blind acquisition and promoted target knowledge.
- _Requirements: 2.1, 2.2, 2.9, 2.10_
- _Boundary: Target Knowledge_
- [x] 3.3 Add handler-named operation facts
- Delivered opt-in handler operations at declaration lines for recognised entries.
- _Requirements: 2.3_
- _Boundary: Fact Interpreter_
- [x] 4. Baseline prover, traces and structural control flow
- [x] 4.1 Integrate entries, paths, operations and witnesses in the deterministic prover
- Delivered hunt-defect findings and failure-visible baseline coverage with no model calls.
- Final three-way judgment is superseded by task 10.6.
- _Requirements: 4.7_
- _Boundary: Prover Integration_
- [x] 4.2 Emit deterministic ordered traces and local argument provenance
- Delivered canonical text/JSON traces for discharged and unguarded baseline pairs.
- _Requirements: 9.1, 9.2, 9.3, 9.5_
- _Boundary: Canonical Trace_
- [x] 4.3 Attach traces to prover records and findings
- Delivered trace references through the replay envelope.
- _Requirements: 8.1, 9.7_
- _Boundary: Prover Integration_
- [x] 4.4 Build structural control-flow graphs
- Delivered branches, loops, exits, exception regions, dominators and control context.
- _Requirements: 10.1, 10.2, 10.3, 10.4_
- _Boundary: Structural CFG_
- [x] 4.5 Add baseline CFG dominance and guard effects
- Delivered value-only rejection and control-context trace fields.
- Strict unknown semantics are superseded by task 10.1.
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_
- _Boundary: CFG Dominance_
- [x] 5. Baseline replay arm
- Delivered checkpointed records, standard metrics, coverage tables, zero-byte refusal and immutable records.
- _Requirements: 5.1, 5.2, 5.4, 5.6_
- _Boundary: Development Runner_
- [x] 6. Baseline development measurement
- Measured 23 repositories and 483 changes: 65.2% fix-push recall, 15.0 flagged ordinary pushes per 100,
0 instrument failures; recorded at benchmarks/measurements/2026-10-28-access-control-prover/.
- The population is development evidence only and remains immutable.
- _Requirements: 5.2, 5.7, 12.9_
- _Boundary: Development Measurement_
## Amendment implementation
- [ ] 7. Abstract evidence foundation
- [x] 7.1 Implement the evidence-relation algebra and completeness value
- Encode the specified precision order, complete join table, dimension conjunction and witness aggregation.
- Preserve every incompleteness reason deterministically and deduplicate reasons without losing stage identity.
- Done when exhaustive table tests and algebraic law tests pass for every relation pair.
- _Requirements: 11.1, 11.6, 11.7_
- _Boundary: Abstract Evidence Domain_
- [x] 7.2 Add resource abstract values and operation-fact fields
- Represent exact resources, finite alias sets, request-global scope and unknown resources.
- Extend operation facts to state when a request-global guard is sufficient.
- Done when resource parsing and joins distinguish exact, alias, global and unknown fixtures.
- _Requirements: 2.1, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Resource Domain_
- _Depends: 7.1_
- [x] 7.3 Add authority abstract values and promoted implication facts
- Represent exact, finite and unknown authorities separately from resources.
- Load authority implications from promoted facts and retain the supporting fact identifier.
- Done when sufficient, insufficient and unknown authority fixtures produce the specified relations.
- _Requirements: 2.1, 2.2, 2.5, 2.7, 2.8, 11.1_
- _Boundary: Authority Domain_
- _Depends: 7.1_
- [x] 7.4 Extend judgment, trace and coverage contracts with abstract state
- Add three-way judgments, completeness and evidence relations without changing legacy defaults.
- Add canonical serialization for resource and authority relations and all incompleteness reasons.
- Done when old trace fixtures remain stable in legacy mode and new fixtures round-trip all abstract fields.
- _Requirements: 7.1, 7.2, 7.5, 7.6, 9.1, 9.2, 9.6_
- _Boundary: Judgment Contracts, Canonical Trace_
- _Depends: 7.1, 7.2, 7.3_
- [ ] 8. Stable symbols and conservative call resolution
- [x] 8.1 Capture call receiver and arity evidence in the semantic IR
- Preserve receiver expression and argument count for supported syntax while keeping existing constructors valid.
- Done when language fixtures retain receiver and arity evidence without regressing existing IR consumers.
- _Requirements: 3.2, 3.3_
- _Boundary: Semantic IR_
- [x] 8.2 Build stable declaration identities and the per-change symbol index
- Identify declarations by module, enclosing type, name and available signature.
- Cache the symbol and inheritance indexes on the per-change context.
- Done when duplicate type names, same-name methods and overloads receive stable distinct identities.
- _Requirements: 3.2, 3.5, 6.2_
- _Boundary: Symbol Index_
- _Depends: 8.1_
- [x] 8.3 Resolve exact, finite-candidate and unresolved call targets
- Return complete exact targets, incomplete finite candidate sets, or unresolved results with reasons.
- Remove global bare-name fallback from prover-mode resolution.
- Done when ambiguity never merges methods and unresolved calls cannot produce complete paths.
- _Requirements: 3.3, 3.4, 11.1, 11.6_
- _Boundary: Call Resolution_
- _Depends: 8.2_
- [x] 8.4 Resolve inheritance and base-declaration attachment
- Attach base operations and entries only through repository-defined descendant relations.
- Preserve ambiguous inheritance as incomplete rather than projecting to unrelated routes.
- Done when subclass and unrelated-class fixtures produce distinct attachments and judgments.
- _Requirements: 3.5_
- _Boundary: Call Resolution_
- _Depends: 8.2, 8.3_
- [ ] 9. Interprocedural summaries and concrete paths
- [x] 9.1 Extract local function summaries
- Collect each function's operations, witnesses, resolved calls and local completeness without traversing callers.
- Done when summaries are deterministic and contain no target names outside fact identifiers.
- _Requirements: 3.1, 3.6, 11.7_
- _Boundary: Function Summaries_
- _Depends: 7.4, 8.3_
- [x] 9.2 Compute monotone summaries over recursive call components
- Iterate finite summary sets over strongly connected components until stable.
- Done when direct recursion and mutual recursion terminate with stable accumulated evidence.
- _Requirements: 3.7, 11.6_
- _Boundary: Function Summaries_
- _Depends: 9.1_
- [x] 9.3 Add summary caches and explicit exploration cutoffs
- Reuse summaries per change and carry depth, fan-out, deadline and other cutoff reasons into completeness.
- Done when each cutoff fixture returns accumulated evidence plus the exact incomplete reason.
- _Requirements: 3.8, 6.2, 6.3, 7.2_
- _Boundary: Function Summaries, Context Cache_
- _Depends: 9.2_
- [x] 9.4 Materialise exact evidence paths from summaries
- Emit concrete entry-to-operation hops only through exact target edges.
- Preserve every complete chain independently and retain candidate-edge diagnostics separately.
- Done when guarded and unguarded paths to one operation remain separate and candidate edges never complete a path.
- _Requirements: 3.9, 3.10, 11.4, 11.5_
- _Boundary: Concrete Paths_
- _Depends: 9.3_
- [ ] 10. Strict authorization judgment
- [x] 10.1 Return abstract dominance relations without prover fallback
- Return MUST, MAY, NO or UNKNOWN with guard effect and control context.
- Keep legacy order dominance unchanged, but return UNKNOWN in prover mode for unavailable graphs or line mappings.
- Done when no prover fixture is discharged or flagged solely through textual-order fallback.
- _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7_
- _Boundary: CFG Dominance_
- _Depends: 7.1_
- [x] 10.2 Match witness kind, scope and resource
- Apply request-global sufficiency only when the operation fact declares it.
- Return NO for definite mismatch and UNKNOWN for unresolved resource compatibility.
- Done when same-resource, alias, global, mismatch and unknown fixtures match the design algebra.
- _Requirements: 2.1, 2.2, 2.5, 2.6, 2.7, 11.1_
- _Boundary: Witness Matcher_
- _Depends: 7.2, 10.1_
- [x] 10.3 Match authority and promoted implications
- Apply only promoted authority implications and preserve their evidence.
- Return NO for insufficient authority and UNKNOWN when compatibility cannot be decided.
- Done when stronger, weaker, unrelated and unknown authorities produce the expected relations.
- _Requirements: 2.2, 2.5, 2.7, 2.8, 11.1_
- _Boundary: Witness Matcher_
- _Depends: 7.3, 10.1_
- [x] 10.4 Compute one judgment per complete or diagnostic path
- Conjoin witness dimensions, aggregate witnesses per obligation and apply the mixed-obligation rule.
- Emit discharged only for all-MUST, indeterminate for any MAY/UNKNOWN, and unguarded for at least one NO.
- Done when MUST/NO, MAY/NO, UNKNOWN and empty-witness fixtures match the specified truth table.
- _Requirements: 4.1, 4.2, 4.3, 11.2, 11.3, 11.4_
- _Boundary: Pair Judgment_
- _Depends: 10.2, 10.3_
- [x] 10.5 Aggregate multiple paths in the opt-in checker mode
- Return unguarded if any complete path is unguarded, discharged only for complete all-discharged enumeration,
and indeterminate otherwise.
- Keep the legacy checker API and defaults unchanged.
- Done when mixed guarded, unguarded and incomplete path sets produce the specified aggregate.
- _Requirements: 3.10, 4.1, 4.2, 4.3, 4.4, 4.5, 11.5_
- _Boundary: Obligation Checker_
- _Depends: 9.4, 10.4_
- [x] 10.6 Integrate three-way judgments into the prover
- Emit hunt findings only for unguarded pairs and send no indeterminate pair to the proof arm.
- Carry all judgments, completeness reasons, resources, authorities and stable identifiers in traces and coverage.
- Record stage-specific exceptions without folding them into quiet coverage.
- Done when end-to-end fixtures distinguish unguarded, discharged and indeterminate records and findings.
- _Requirements: 4.4, 4.5, 4.6, 4.7, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 8.1, 8.2, 8.3, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7_
- _Boundary: Prover Integration, Canonical Trace_
- _Depends: 7.4, 9.4, 10.5_
- [ ] 11. Target knowledge registry and acquisition
- [x] 11.1 (P) Add the entry-recognition strategy registry
- Route mapping dispatch through one registry without embedding target names in engine modules.
- Done when a fixture strategy registers, runs and records its strategy identifier and evidence.
- _Requirements: 1.1, 1.5, 1.7_
- _Boundary: Mapping Registry_
- _Depends: 7.1_
- [x] 11.2 Add the generic call-site registration strategy
- Resolve repository-defined registered handlers through stable symbols and return unresolved registration coverage.
- Done when exact and unresolved registration fixtures produce entries or coverage respectively.
- _Requirements: 1.2, 1.3, 1.4, 1.5, 1.6_
- _Boundary: Registration Adapter_
- _Depends: 8.4, 11.1_
- [x] 11.3 (P) Extract existing target knowledge into candidate records
- Read legacy recognition strategies and current facts without changing promoted outputs.
- Done when extraction produces deterministic candidate records with source/version/region evidence.
- _Requirements: 2.9_
- _Boundary: Fact Initialization_
- _Depends: 7.1_
- [x] 11.4 Verify and promote proposed target knowledge
- Accept source- or approved-documentation proposals, reject contradictions, and write only verified outputs.
- Done when matching proposals promote and contradicted or evidence-free proposals leave promoted knowledge unchanged.
- _Requirements: 2.9, 2.10_
- _Boundary: Fact Initialization_
- _Depends: 11.3_
- [x] 11.5 Record acquisition provenance and enforce label-isolated mode
- Persist input revision, evidence digest, accepted/rejected proposals and output digest.
- Reject admission acquisition inputs containing vulnerability, fix-location or ordinary-change labels.
- Done when changed output after labels are opened marks the population spent.
- _Requirements: 12.1, 12.2, 12.3_
- _Boundary: Fact Initialization, Admission Guard_
- _Depends: 11.4_
- [x] 11.6 Migrate one legacy recogniser through the registry
- Move one development-evidence family out of inline dispatch without changing its entries.
- Done when old fixtures stay stable and inline target-specific matching is removed for that family.
- _Requirements: 1.1, 1.7_
- _Boundary: Mapping Registry_
- _Depends: 11.1_
- [x] 11.7 Run the first label-blind development acquisition
- Produce registration, handler-operation, enforcement, resource and authority knowledge through the gate.
- Treat the output as development data and record its acquisition digest; do not encode names in engine modules.
- Done when promoted outputs load, rejected proposals have reasons, and the old unfinished task 3.2 has no residue.
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 2.10, 5.7, 12.9_
- _Boundary: Target Knowledge_
- _Depends: 10.3, 11.2, 11.5, 11.6_
- [ ] 12. Development and admission runners
- [x] 12.1 Materialise and verify baseline and candidate analyzer bundles
- Resolve analyzer revisions, fact sets, acquisition records and configurations to immutable digest-verified inputs.
- Refuse comparison when any bundle artifact is unavailable or mismatched.
- Done when the immutable baseline and a fixture candidate bundle can be reconstructed exactly.
- _Requirements: 5.1, 5.5, 12.1_
- _Boundary: Run Manifests_
- [x] 12.2 Add record provenance, stage funnel and judgment transitions
- Persist analyzer, fact, acquisition, configuration, population and input digests per record.
- Summarise every analysis stage, completeness reason and transition between compared judgments.
- Done when a fixture run accounts for every record from entry recognition through final judgment.
- _Requirements: 5.1, 5.3, 5.4, 7.1, 7.6, 9.7, 11.8_
- _Boundary: Development Runner_
- _Depends: 10.6, 12.1_
- [x] 12.3 Enforce instrument and hard runtime gates
- Preserve zero-byte and stale-anchor refusal and add stage-aware deadline records.
- Fail the run if any measured change exceeds 120 seconds; never summarize the overrun as a passing result.
- Done when zero-byte, stale-anchor, deadline and 121-second fixtures all refuse the summary.
- _Requirements: 5.6, 6.1, 6.3_
- _Boundary: Runner Gates_
- _Depends: 12.2_
- [x] 12.4 Register the four-arm development comparison
- Run baseline/candidate analyzer crossed with baseline/candidate facts over identical inputs.
- Report metric deltas, stage-funnel deltas and judgment transitions without admission language.
- Done when fixture arms isolate a fact-only lift from an analyzer-only lift.
- _Requirements: 5.2, 5.5, 5.7, 11.8_
- _Boundary: Development Runner_
- _Depends: 12.1, 12.2, 12.3_
- [x] 12.5 Consume and validate an externally reviewed admission manifest
- Accept only a manifest produced by valid-detection-benchmark with review, isolation and immutable digest fields.
- Reject label exposure, changed acquisition output or a spent population.
- Done when valid, malformed, exposed and spent manifests produce deterministic accept/refuse results.
- _Requirements: 12.1, 12.2, 12.3, 12.6, 12.8_
- _Boundary: Admission Runner_
- _Depends: 11.5, 12.3_
- [x] 12.6 Add paired scoring and the admission report
- Score vulnerable/fixed twins, reviewed ordinary changes, indeterminate rate, coverage, runtime and instruments.
- Preserve sample counts and uncertainty intervals and return NO-GO on any pre-registered gate failure.
- Done when fixture pairs detect vulnerable-only, fixed-side regression and mislabeled-negative cases.
- _Requirements: 5.2, 12.4, 12.5, 12.7, 12.8, 12.9_
- _Boundary: Admission Runner_
- _Depends: 12.5_
- [ ] 13. Cross-cutting validation and measurements
- [x] 13.1 Validate abstract-domain and metamorphic laws
- Cover the full join table, conjunction, witness aggregation, monotonicity and mixed-obligation rule.
- Prove that type renaming preserves a judgment and adding ambiguity can only preserve it or make it indeterminate.
- Done when property and fixture tests pass deterministically across repeated runs.
- _Requirements: 11.2, 11.3, 11.4, 11.5, 11.6, 11.7_
- _Boundary: Abstract Evidence Validation_
- _Depends: 10.5_
- [x] 13.2 (P) Validate cross-component analysis scenarios
- Exercise helper guards, registration entries, base attachment, resource mismatch, authority insufficiency,
ambiguous calls, unavailable CFGs, recursion and budget cutoff.
- Done when every scenario yields the expected trace, coverage reasons and three-way judgment.
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.5, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 4.1, 4.2, 4.3, 10.6_
- _Boundary: Prover Integration Validation_
- _Depends: 10.6, 11.7_
- [x] 13.3 Run the four-arm development regression
- Run all four registered arms on the immutable current development population after proving nonzero input reads.
- Publish analyzer/fact attribution, stage funnel, three-way judgments, budget result and comparison to the
2026-10-28 baseline; label the result development-only.
- Done when all arm records and summary digests are complete and no admission claim appears.
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 6.1, 7.3, 11.8, 12.9_
- _Boundary: Development Measurement_
- _Depends: 12.4, 13.1, 13.2_
- [x] 13.4 Run admission from an externally prepared untouched manifest
- Consume a reviewed manifest from valid-detection-benchmark after analyzer and facts are frozen.
- Verify label isolation, paired vulnerable/fixed behavior, reviewed ordinary labels and every admission gate.
- Done when the report returns an evidence-backed GO or NO-GO without modifying facts, strategies or thresholds.
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 12.7, 12.8, 12.9_
- _Boundary: Admission Measurement_
- _Depends: 12.6, 13.3_
- [ ] 14. Reviewed detection-core fact adapter
- [x] 14.1 Define the production access-control family contract
- Encode the versioned access-control security object, roles, three decision rules, residual question and closed incompleteness reasons through public detection-core types.
- Keep target names confined to the reviewed ecosystem declaration while pinning every digest-affecting field, status, origin policy and decision semantic declared by the design.
- Done when the canonical production contract validates, round-trips deterministically, and rejects every altered or undeclared role, decision, field and reason fixture.
- _Requirements: 13.1_
- _Boundary: Access-Control Family Contract_
- _Depends: 11.5_
- [x] 14.2 Implement externally bootstrapped fact authority
- Define the immutable authority bundle and separate host bootstrap policy over reviewed pack/projection references, policies, lineages, matcher identity, reviewer/store admission, forbidden identity pairs and conflict disclosure.
- Re-derive canonical policy, attestation, disclosure and bundle identities before any target data is consumed; never let the bundle choose its own trust roots.
- Done when a complete independently reviewed authority fixture authenticates and every missing, duplicate, substituted, self-authored or non-canonical variant fails before pack loading.
- _Requirements: 13.2, 13.3_
- _Boundary: Fact Authority_
- _Depends: 14.1_
- [x] 14.3 Implement installed authority codecs and resolver
- Implement closed canonical JSON decoders for the bootstrap policy, installed authority, v2 fact packs, nested policies/lineages/evidence and immutable content references.
- Implement the checked-in digest-addressed resolver using only validated `content_digest` for filesystem addressing and re-derivable storage receipts under an injected bootstrap-admitted store identity.
- Done when canonical synthetic installed fixtures round-trip to Task 14.2 typed values and unknown/missing fields, noncanonical bytes, traversal-like artifact IDs, digest paths or byte substitutions fail before authentication.
- _Requirements: 13.2, 13.3_
- _Boundary: Installed Fact Authority Wire_
- _Depends: 14.2_
- [x] 14.4 Record the approved repo-local review ceremony
- Write the three closed canonical identity manifests with the exact schema, role, name and approval-basis values declared by the design.
- Bind bootstrap reviewer admission, custody policy, authority review and pack evidence identities to those manifest digests while keeping all trust/store admission outside `authority.json`.
- Done when the approved manifests and bootstrap load through Task 14.3, all identity bindings validate, and changing any field or approval basis invalidates authentication.
- _Requirements: 13.2, 13.3_
- _Boundary: Installed Authority Review Ceremony_
- _Depends: 14.3_
- [x] 14.5 Migrate the installed entry knowledge into reviewed v2 packs
- Produce one language-scoped v2 pack per promoted language and a separately authenticated canonical compatibility projection from the existing reviewed entry data.
- Map every migrated row to declared roles, typed structural requirements and relation effects without adding target vocabulary to engine modules.
- Preserve legacy pattern recognition byte-for-byte while preventing the projection from constructing detection-core evidence or readiness.
- Done when legacy recognition fixtures are unchanged, both reviewed language packs validate under task 14.2 authority, and projection-only input remains non-authoritative.
- _Requirements: 1.1, 1.5, 1.7, 2.9, 2.10, 13.2, 13.4_
- _Boundary: Entry Knowledge Migration_
- _Depends: 14.1, 14.2, 14.3, 14.4_
- [x] 14.6 Add canonical structural-observation and fact-match persistence
- Provide the downstream write boundary that persists canonical `StructuralObservation` bytes and returns immutable references through a read-only resolver.
- Persist completed `fact_match_bytes` only after its observation references exist, then return the match reference without inserting a circular self-reference into the match digest.
- Done when stored observation and match bytes resolve canonically and byte or reference substitution is refused deterministically.
- _Requirements: 13.5_
- _Boundary: Fact-Match Artifact Writer_
- [x] 14.7 Implement the authority-bearing structural matcher
- Build typed observations from repository structure for the requirements declared by the selected reviewed fact; do not infer missing structure from a regex or name hit.
- Use task 14.6 to persist every observation and completed `FactMatchEvidence`, then pass the immutable match reference with family contract, policies, lineages, custody and read-only resolver to detection-core.
- Bind matcher behavior and fact lineage to current custody and the evaluation population so same-population label influence is incomplete or refused.
- Done when complete fixtures apply the declared relations, while name-only, partial, ambiguous, substituted and contaminated fixtures cannot produce complete evidence.
- _Requirements: 1.1, 1.2, 1.3, 1.5, 7.2, 9.4, 13.5, 13.6_
- _Boundary: Structural Matcher_
- _Depends: 14.2, 14.5, 14.6_
- [x] 14.8 Produce the downstream conformance handoff
- Run detection-core fact-pack, fact-match, application and full-evidence validators over target-owned fixtures after proving nonzero fixture reads.
- Publish an immutable manifest naming every fixture, artifact digest, validator result and negative-case reason for detection-core task 9.4 to accept; do not add target fixtures to core.
- Exercise authority substitution, producer/reviewer conflict, resolver byte substitution, lineage omission, custody contamination and deterministic replay.
- Done when the complete target-owned fixture passes, every incomplete/self-authored/substituted/contaminated/legacy-only variant is refused with a stable reason, and the immutable handoff manifest is reproducible.
- _Requirements: 7.2, 7.4, 12.2, 12.3, 13.7_
- _Boundary: Downstream Adapter Conformance Handoff_
- _Depends: 14.7_
- [x] 14.9 Migrate the production entry caller and prove zero bypasses
- Wire the externally bootstrapped authority-bearing adapter into the target-aware `mapping/legacy.py` entry path while keeping `analyze_entry_points` stable for its callers.
- Retain the legacy projection only for compatibility recognition; prohibit it from supplying v2 evidence, readiness or admission.
- Run import/caller and artifact-writer censuses proving no production path bypasses authority or treats legacy projection output as v2 evidence.
- Done when existing entry recognition remains byte-stable, explicit v2 matching uses the reviewed persisted path, and both production bypass censuses are zero.
- _Requirements: 1.1, 1.5, 1.7, 13.4, 13.7, 13.8_
- _Boundary: Production Mapping Integration_
- _Depends: 14.8_
- _Blocked: detection-core task 9.4 must accept the immutable handoff from task 14.8 before production caller migration_
## Task constraints
- Complexity: McCabe at most 10 per touched function, at most 50 logical lines per function and 400 per new module.
- Engine/target split: abstract-domain, resolution, summary, judgment and trace modules contain no target names.
- Target knowledge: names enter only through promoted facts or registered target-aware strategies.
- Compatibility: legacy checker defaults and completed baseline records remain unchanged.
- Instrument rule: validation must prove nonzero input reads before interpreting any clean or zero result.
## Implementation Notes
- Task 14.3 planning: the 963-byte legacy entry file was readable, but typed authority validation was not an installed-artifact format. The maintainer approved the repo-local review ceremony; Tasks 14.3–14.4 now separate wire/resolver implementation from the identity/bootstrap records before Task 14.5 migrates data.
## Baseline implementation record
- Deliveries A-C implemented the baseline paths, recognisers, witnesses, handler facts, prover, traces and CFG with
static checks and focused tests passing at delivery time.
- The full development arm then processed all 483 changes with zero instrument failures. Its maximum runtime exceeded
the amended hard budget, so the baseline is evidence, not an eligible arm.
- The 2026-10-28 ablation identified call resolution, resource/authority precision and failure visibility as the
amendment levers. Those observations inform tasks 7-13 but are not admission labels.
+3 -3
View File
@@ -141,7 +141,7 @@ before `ax apply`, and a task without a Model gets none. The value is never rend
Every actor connection except DNS goes through Agent Substrate's egress gateway (`atenet-egress`), which denies
an actor without an `EgressPolicy`. Three pieces make the plane work under that rule:
- **Receiver by name.** `ops/ax/receiver-service.yaml.tmpl` (applied by `up.sh`) is a selector-less Service
- Receiver by name. `ops/ax/receiver-service.yaml.tmpl` (applied by `up.sh`) is a selector-less Service
`ousast-receiver` in `ax-system`, port 80, with an EndpointSlice on the kind gateway at `OUSAST_ARTIFACT_PORT`
(fixed, 18090), where the reconciler binds its receiver. Tasks get
`OUSAST_ARTIFACT_URL=http://ousast-receiver.ax-system.svc.cluster.local/` plus `OUSAST_ARTIFACT_DIAL=<the
@@ -150,12 +150,12 @@ an actor without an `EgressPolicy`. Three pieces make the plane work under that
`Host` but connects to the address the actor dialled (measured: dialling the kind gateway on port 80 was allowed
and then refused upstream), so the dial address is the ClusterIP, which kube-proxy maps to the host.
`OUSAST_ARTIFACT_HOST` set keeps the old direct URL (tests).
- **Per-task policy.** `policy_for` grants `http` on port 80 to the receiver name, and `tls_passthrough` on 443
- Per-task policy. `policy_for` grants `http` on port 80 to the receiver name, and `tls_passthrough` on 443
to the Git hosts of the bound Workspaces' `https` repos and to the bound Model's
`openultrasast.io/egress-hosts` (`api.deepseek.com` for `deepseek-flash`, validated as a plain DNS name).
Nothing else: no IP address or wildcard, and passthrough only -- an `https` rule intercepts TLS and the actor
would need the gateway CA, which ax templates do not mount.
- **Order.** `ax apply` creates the actor; the reconciler writes the policy with `kubectl ate create
- Order. `ax apply` creates the actor; the reconciler writes the policy with `kubectl ate create
egress-policy` (update when one exists), reads it back, waits `OUSAST_EGRESS_SETTLE` (11 s: the gateway caches a
missing policy as deny for 10 s), then resumes. kubectl-ate has no delete verb; Substrate's store deletes the
policy with its actor, so after `ax delete task` the reconciler confirms it is gone.
+4 -4
View File
@@ -14,19 +14,19 @@ a DeepSeek provider -- is carried by our thin `Run` layer and dissolves into ax
## Boundary Context
- **In scope**: the manifest kinds and a `Run` kind for dependencies, artifacts and budgets; bring-up of ax on
- In scope: the manifest kinds and a `Run` kind for dependencies, artifacts and budgets; bring-up of ax on
this host (kind + Agent Substrate + ax controller) with a doctor command; an ax-backed reconciler that
submits Tasks to ax (one engine-bearing task at a time) and is the only executor; the runner contract and a
runner image; the first two tasks, `repo-facts` and
`verify`, with the existing scripts as reference behaviour; measurement on the 46-candidate validation set.
- **Out of scope**: a remote or multi-node cluster; contributing providers or budgets to ax upstream (a
- Out of scope: a remote or multi-node cluster; contributing providers or budgets to ax upstream (a
follow-on once the increment is measured); changing what the model is asked (prompts, families, pass rules)
except as required to consume `repo-facts`; the judge stage, the config-family question and population v3
(they become tasks later, under their own specs); any change to `evaluate.py`'s protocol scoring.
- **Adjacent expectations**: the independent-population protocols and records stay as they are; `finding_dump`
- Adjacent expectations: the independent-population protocols and records stay as they are; `finding_dump`
and the engine path are untouched; `vulnerabilities-over-plumbing` applies -- this is plumbing that must pay
for itself in measured tokens and unchanged or better recall.
- **Target state (maintainer, 2026-09-29)**: this plane replaces HarnessX as the agentic plane; HarnessX is
- Target state (maintainer, 2026-09-29): this plane replaces HarnessX as the agentic plane; HarnessX is
removed by its own spec (`harnessx-removal`) once Requirement 6's measurement shows value; the `Run` layer
shrinks as ax gains dependencies, budgets and artifacts. No new HarnessX feature is added meanwhile.
@@ -8,9 +8,9 @@ Nothing in the pipeline can represent "this operation should have been guarded"
## Desired Outcome
Absence bugs become first-class, measured, and honest. An obligated operation (protected data access, privileged action, security configuration) reached from an entry point without a discharger (a dominating auth guard, an identity constraint bound from the authenticated context, a non-permissive value) is reported as an obligation violation, with the missing discharger kind, the evidence for why the obligation exists (sibling handlers that discharge it, or a declared policy clause), and the known fix shape the corpus taught. Leave-one-out on the absence pairs is the objective, per obligation kind and per provenance profile, so the number rises as the corpus grows. `agent-vfc` and `vibe-py` absence pairs move from 0 detected to a measured baseline that the engine specs can raise.
Absence bugs become first-class and are measured without overstating results. An obligated operation (protected data access, privileged action, security configuration) reached from an entry point without a discharger (a dominating auth guard, an identity constraint bound from the authenticated context, a non-permissive value) is reported as an obligation violation, with the missing discharger kind, the evidence for why the obligation exists (sibling handlers that discharge it, or a declared policy clause), and the known fix shape the corpus taught. Leave-one-out on the absence pairs is the objective, per obligation kind and per provenance profile, so the number rises as the corpus grows. `agent-vfc` and `vibe-py` absence pairs move from 0 detected to a measured baseline that the engine specs can raise.
Principle written into the roadmap: **an operation carries obligations, not only data; the application's own consistency and its declared policy say which, and the tool reports the missing discharger rather than a phantom flow.**
Principle written into the roadmap: **an operation carries data and obligations; the application's own consistency and its declared policy say which, and the tool reports the missing discharger rather than a phantom flow.**
## Approach
@@ -18,8 +18,8 @@ Obligated operations and discharger kinds are closed facts data, like sinks and
## Scope
- **In**: obligated-operation and discharger facts; obligation shapes derived from pairs; consistency detection across sibling handlers; optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; new evidence rungs and report wording for obligations; worth-fixing weighting by resource sensitivity; corpus recipe shape for absence pairs (handler plus registration context); leave-one-out per obligation kind; `obligations` improve lever; offline tests.
- **Out**: taint or facts changes for flow bugs; the call graph and path records themselves (reachability spec); guard-dominance IR (guard-dominance-regime); LLM-authored policy or shapes; any merge gate; vendoring unlicensed code; sandbox proof of absence bugs (they do not crash).
- In: obligated-operation and discharger facts; obligation shapes derived from pairs; consistency detection across sibling handlers; optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; new evidence rungs and report wording for obligations; worth-fixing weighting by resource sensitivity; corpus recipe shape for absence pairs (handler plus registration context); leave-one-out per obligation kind; `obligations` improve lever; offline tests.
- Out: taint or facts changes for flow bugs; the call graph and path records themselves (reachability spec); guard-dominance IR (guard-dominance-regime); LLM-authored policy or shapes; any merge gate; vendoring unlicensed code; sandbox proof of absence bugs (they do not crash).
## Boundary Candidates
@@ -40,13 +40,13 @@ Obligated operations and discharger kinds are closed facts data, like sinks and
## Upstream / Downstream
- **Upstream**: `EntryPointRecord` (route, access level, decorators, conditions); `FileIR`; path records and dominance from the two engine specs; `PairCase` with `review_tier`; `MechanismStore` and the lever machinery; facts loader.
- **Downstream**: reports and SARIF; worth-fixing ranking; the roadmap's leave-one-out table gains obligation kinds; `guard-dominance-regime` consumes obligation discharger kinds as guard candidates for web code.
- Upstream: `EntryPointRecord` (route, access level, decorators, conditions); `FileIR`; path records and dominance from the two engine specs; `PairCase` with `review_tier`; `MechanismStore` and the lever machinery; facts loader.
- Downstream: reports and SARIF; worth-fixing ranking; the roadmap's leave-one-out table gains obligation kinds; `guard-dominance-regime` consumes obligation discharger kinds as guard candidates for web code.
## Existing Spec Touchpoints
- **Extends**: `corpus-seeded-mechanisms` (second shape family, same store, same lever contract), `pair-corpus-honesty` (registration-context excerpt mode, obligation label), `three-stage-scan` (one more proposer in MAP, new rungs below proof).
- **Depends**: `reachability-flow-model` (entry point to operation paths); `guard-dominance-regime` (a guard dominates the path). Reachability is now on the critical path for two consumers.
- Extends: `corpus-seeded-mechanisms` (second shape family, same store, same lever contract), `pair-corpus-honesty` (registration-context excerpt mode, obligation label), `three-stage-scan` (one more proposer in MAP, new rungs below proof).
- Depends: `reachability-flow-model` (entry point to operation paths); `guard-dominance-regime` (a guard dominates the path). Reachability is now on the critical path for two consumers.
## Constraints
@@ -152,9 +152,9 @@ tests/
### Modified Files
- `src/openultrasast/cli.py` — `_check_obligations` after `_search_variants` in MAP (standard/deep); manifest `obligations` block; `mechanisms export` writes obligation rows too.
- `src/openultrasast/pairs.py` — `ExpectedFinding.obligation: str | None`; `_detections`-side additive rule: for a row with `obligation`, a finding with tag `obligation:<kind>` inside the labeled function counts.
- `src/openultrasast/semantic/mechanisms.py` — no schema change; `corpus_mechanisms()` unchanged; a helper `obligation_mechanisms(records)` selects `shape["family"] == "obligation"` (added in `obligations/shapes.py`, not in the store module).
- `src/openultrasast/cli.py`: `_check_obligations` after `_search_variants` in MAP (standard/deep); manifest `obligations` block; `mechanisms export` writes obligation rows too.
- `src/openultrasast/pairs.py`: `ExpectedFinding.obligation: str | None`; `_detections`-side additive rule: for a row with `obligation`, a finding with tag `obligation:<kind>` inside the labeled function counts.
- `src/openultrasast/semantic/mechanisms.py`: no schema change; `corpus_mechanisms()` unchanged; a helper `obligation_mechanisms(records)` selects `shape["family"] == "obligation"` (added in `obligations/shapes.py`, not in the store module).
- `src/openultrasast/reports.py`, `rank.py`, `config.py`, `improve/*`, `semantic/seed.py`, `semantic/loo.py`, `benchmarks/pairs/harvest.py`, `benchmarks/pairs/mechanisms.toml` as listed above.
## System Flows
@@ -177,7 +177,7 @@ flowchart LR
I --> J[StaticFinding suspicion; tags obligation:*, discharger:*, obligation_evidence:*]
```
Gating: the checker emits a finding only when (1) the operation is reached — from an entry point along a `PathRecord`, or, without paths, when the operation sits in a handler that is itself an entry point — and (2) no discharger of the required kind dominates, and (3) the obligation is justified by a sibling set (`consistency_violation`), a policy clause (`declared_policy_violation`) or, in degraded mode without either, by the operation fact alone (`function_local`). A declared public route suppresses path-guard obligations for that route only.
Gating: the checker emits a finding only when (1) the operation is reached (from an entry point along a `PathRecord`, or, without paths, when the operation sits in a handler that is itself an entry point) and (2) no discharger of the required kind dominates, and (3) the obligation is justified by a sibling set (`consistency_violation`), a policy clause (`declared_policy_violation`) or, in degraded mode without either, by the operation fact alone (`function_local`). A declared public route suppresses path-guard obligations for that route only.
### Teaching and measuring
@@ -8,9 +8,9 @@ AppSec engineers and maintainers of OpenUltraSAST scan web code, increasingly vi
## Boundary Context
- **In scope**: obligated operations and discharger kinds as closed facts data; obligation shapes learned from trusted pairs; consistency detection across sibling handlers; an optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; obligation evidence rungs, report wording and worth-fixing weighting; a corpus recipe shape that keeps the handler's registration context; leave-one-out per obligation kind; an `obligations` improve lever; offline tests.
- **Out of scope**: flow taint, walker or sink facts; the call graph, path records and dominance computation (owned by `reachability-flow-model` and `guard-dominance-regime`); model-authored policy or shapes; sandbox proof of absence bugs; any merge gate; vendoring unlicensed code.
- **Adjacent expectations**: the entry-point mapper keeps providing routes with an access level and their decorators; the reachability spec provides entry-point-to-operation paths and the guard-dominance spec provides "a guard dominates the path"; the mechanism store, its deterministic ids and the lever contract from `corpus-seeded-mechanisms` are reused unchanged; the pair scorer accepts one additive label field; `local` remains the only slice that can fail CI.
- In scope: obligated operations and discharger kinds as closed facts data; obligation shapes learned from trusted pairs; consistency detection across sibling handlers; an optional declared policy file with a closed schema; path-aware violation evaluation with a function-local degraded mode; obligation evidence rungs, report wording and worth-fixing weighting; a corpus recipe shape that keeps the handler's registration context; leave-one-out per obligation kind; an `obligations` improve lever; offline tests.
- Out of scope: flow taint, walker or sink facts; the call graph, path records and dominance computation (owned by `reachability-flow-model` and `guard-dominance-regime`); model-authored policy or shapes; sandbox proof of absence bugs; any merge gate; vendoring unlicensed code.
- Adjacent expectations: the entry-point mapper keeps providing routes with an access level and their decorators; the reachability spec provides entry-point-to-operation paths and the guard-dominance spec provides "a guard dominates the path"; the mechanism store, its deterministic ids and the lever contract from `corpus-seeded-mechanisms` are reused unchanged; the pair scorer accepts one additive label field; `local` remains the only slice that can fail CI.
## Requirements
@@ -2,9 +2,9 @@
## Summary
- **Feature**: `authorization-obligations`
- **Discovery Scope**: Extension of the semantic layer (facts, shapes, store, MAP proposers), the entry-point mapper, reports, the pair corpus, leave-one-out and the improve lever; complex integration because it consumes two unimplemented upstream specs.
- **Key Findings**:
- Feature: `authorization-obligations`
- Discovery Scope: Extension of the semantic layer (facts, shapes, store, MAP proposers), the entry-point mapper, reports, the pair corpus, leave-one-out and the improve lever; complex integration because it consumes two unimplemented upstream specs.
- Key Findings:
- The absence pairs are real and structured. `vampi-books-get-by-title` has identical code on both sides except a constraint `user=user` bound from `token_validator(request.headers.get('Authorization'))['sub']`; the vulnerable branch queries `Book` by title alone. The difference is the provenance of a constraint, not a flow. 15 of 29 `agent-vfc` rows and 6 of the 19 non-teaching `vibe-py` pairs carry absence labels (`missing_auth_guard`, `permissive_default`, `identity_from_request_body`, `secret_in_client`, `validation_strength`).
- The entry-point mapper already classifies every route as `public`, `authenticated` or `role-restricted` from its decorators (`mapping._python_route_access`), records the decorators as `access_evidence`, and lists `conditions` (`if` tests that look like checks). That is the sibling detector's upstream for path guards and needs no change.
- `FileIR` binds carry `value_text`, `names` and `call_name`; `variants.source_kind_of_name` already traces a name through binds to a fact source, a parameter or a container read. Provenance of a constraint's value (authenticated context versus request input versus constant) is the same trace with one more source class.
@@ -17,29 +17,29 @@
### What the absence pairs actually differ by
- **Context**: Decide whether absence bugs can be a shape family at all.
- **Sources Consulted**: `benchmarks/pairs/vibe-py` excerpts for `missing_auth_guard` and `permissive_default` rows; `benchmarks/pairs/agent-vfc/recipes.toml` mechanisms; `mechanisms.toml` definitions.
- **Findings**: three recurring differences: (a) a constraint added to a data access whose value comes from the authenticated context; (b) a guard call or decorator added before the operation (early return on a failed token check, `login_required`); (c) a literal changed from permissive to restrictive (`CORS(app)` → origins list, `DEBUG=True` → `False`, RLS enabled). A fourth, rarer: identity read from the body replaced by identity from the token (same as (a) seen from the other side).
- **Implications**: discharger kinds `identity_constraint`, `path_guard`, `ownership_check`, `non_permissive_value`, `validated_input`; the value provenance is the discriminating feature for (a) and (d).
- Context: Decide whether absence bugs can be a shape family at all.
- Sources Consulted: `benchmarks/pairs/vibe-py` excerpts for `missing_auth_guard` and `permissive_default` rows; `benchmarks/pairs/agent-vfc/recipes.toml` mechanisms; `mechanisms.toml` definitions.
- Findings: three recurring differences: (a) a constraint added to a data access whose value comes from the authenticated context; (b) a guard call or decorator added before the operation (early return on a failed token check, `login_required`); (c) a literal changed from permissive to restrictive (`CORS(app)` → origins list, `DEBUG=True` → `False`, RLS enabled). A fourth, rarer: identity read from the body replaced by identity from the token (same as (a) seen from the other side).
- Implications: discharger kinds `identity_constraint`, `path_guard`, `ownership_check`, `non_permissive_value`, `validated_input`; the value provenance is the discriminating feature for (a) and (d).
### Where the policy comes from
- **Context**: Absence is relative to intent; the tool must not invent intent.
- **Sources Consulted**: `mapping.analyze_entry_points` (routes, access level, evidence), Real-Vuln repositories (many handlers per resource), Clearwing's sibling-comparison practice.
- **Findings**: in every Real-Vuln web app the handlers on one resource share a router and a model; the vulnerable handler is the odd one out. Route access classification already exists. A declared policy is the only way to say "this route is meant to be public"; the hunter can only guess it.
- **Implications**: consistency first (no declaration needed, measurable now), declared policy second (stronger rung), hunter last (records intent, never raises).
- Context: Absence is relative to intent; the tool must not invent intent.
- Sources Consulted: `mapping.analyze_entry_points` (routes, access level, evidence), Real-Vuln repositories (many handlers per resource), Clearwing's sibling-comparison practice.
- Findings: in every Real-Vuln web app the handlers on one resource share a router and a model; the vulnerable handler is the odd one out. Route access classification already exists. A declared policy is the only way to say "this route is meant to be public"; the hunter can only guess it.
- Implications: consistency first (no declaration needed, measurable now), declared policy second (stronger rung), hunter last (records intent, never raises).
### Dominance before guard-dominance-regime exists
- **Context**: Req 5 needs "a discharger dominates the path"; the spec that owns dominance is not written.
- **Findings**: within one handler, the existing `FunctionIR` order of binds and calls gives statement order; a guard call followed by an early return before the operation line is dominance in the common case; decorators dominate the whole handler; middleware attached to the router dominates every route under it. Across calls, `PathRecord.hops` gives the sequence of nodes; a discharger on any hop before the operation node dominates the path when the hop's function returns early on failure.
- **Implications**: define `Dominance` as a narrow protocol (`dominates(witness, operation) -> bool`) with a built-in `OrderDominance` implementation (statement order in the handler, decorators, router middleware, hop order) that `guard-dominance-regime` may replace with nesting-aware dominance. The degraded mode (`function_local`) is `OrderDominance` without path records.
- Context: Req 5 needs "a discharger dominates the path"; the spec that owns dominance is not written.
- Findings: within one handler, the existing `FunctionIR` order of binds and calls gives statement order; a guard call followed by an early return before the operation line is dominance in the common case; decorators dominate the whole handler; middleware attached to the router dominates every route under it. Across calls, `PathRecord.hops` gives the sequence of nodes; a discharger on any hop before the operation node dominates the path when the hop's function returns early on failure.
- Implications: define `Dominance` as a narrow protocol (`dominates(witness, operation) -> bool`) with a built-in `OrderDominance` implementation (statement order in the handler, decorators, router middleware, hop order) that `guard-dominance-regime` may replace with nesting-aware dominance. The degraded mode (`function_local`) is `OrderDominance` without path records.
### Store and lever reuse
- **Context**: Avoid a second store and a second lever machinery.
- **Findings**: `Mechanism.shape` is a dict validated where used; `corpus_mechanism_id` hashes `shape.key()`; `validate_mechanism` checks guard and source kinds against `variants` constants. A second family needs its own closed kinds check and a `family` field in the shape dict; the tombstone, snapshot and profile clause need nothing.
- **Implications**: `ObligationShape.to_dict()` carries `family = "obligation"`; the lever validator dispatches on `family`; `search_tree` ignores obligation rows (they are not sink shapes) and the obligation checker ignores sink rows.
- Context: Avoid a second store and a second lever machinery.
- Findings: `Mechanism.shape` is a dict validated where used; `corpus_mechanism_id` hashes `shape.key()`; `validate_mechanism` checks guard and source kinds against `variants` constants. A second family needs its own closed kinds check and a `family` field in the shape dict; the tombstone, snapshot and profile clause need nothing.
- Implications: `ObligationShape.to_dict()` carries `family = "obligation"`; the lever validator dispatches on `family`; `search_tree` ignores obligation rows (they are not sink shapes) and the obligation checker ignores sink rows.
## Architecture Pattern Evaluation
@@ -54,23 +54,23 @@
### Decision: Obligation findings are `suspicion` with a data label, not new ladder rungs
- **Context**: Req 6.1 names three labels; the ladder is closed and owned elsewhere.
- **Selected Approach**: `StaticFinding.evidence_level = "suspicion"`, tags `obligation:<kind>`, `discharger:<kind>`, `obligation_evidence:<label>`; reports render the label. Dispositions and `ALLOWED_EVIDENCE` unchanged.
- **Trade-offs**: SARIF consumers see `suspicion` plus properties; honest and non-invasive.
- Context: Req 6.1 names three labels; the ladder is closed and owned elsewhere.
- Selected Approach: `StaticFinding.evidence_level = "suspicion"`, tags `obligation:<kind>`, `discharger:<kind>`, `obligation_evidence:<label>`; reports render the label. Dispositions and `ALLOWED_EVIDENCE` unchanged.
- Trade-offs: SARIF consumers see `suspicion` plus properties; honest and non-invasive.
### Decision: Dominance is a protocol with an order-based default
- **Context**: `guard-dominance-regime` is unwritten; this spec cannot wait for it to be useful.
- **Selected Approach**: `Dominance` protocol; `OrderDominance` default (decorator, router middleware, statement order before the operation, hop order along a path). The guard spec replaces it without touching callers.
- **Trade-offs**: order-based dominance misses guards inside conditionals; recorded as the known coarseness and measured on the fixed twins.
- Context: `guard-dominance-regime` is unwritten; this spec cannot wait for it to be useful.
- Selected Approach: `Dominance` protocol; `OrderDominance` default (decorator, router middleware, statement order before the operation, hop order along a path). The guard spec replaces it without touching callers.
- Trade-offs: order-based dominance misses guards inside conditionals; recorded as the known coarseness and measured on the fixed twins.
### Decision: Policy is never written by the tool
- **Selected Approach**: `declared policy` is a hand-maintained file with a closed schema; the lever may only *propose* a clause, journaled as pending until a human accepts; scans never read pending clauses.
- Selected Approach: `declared policy` is a hand-maintained file with a closed schema; the lever may only *propose* a clause, journaled as pending until a human accepts; scans never read pending clauses.
### Decision: Resource identity for sibling grouping
- **Selected Approach**: a handler's resource is the trailing segment of its route path with parameters stripped, joined with the operation's target token (model, table, collection name from the operation fact match); router or module is the second key. Coarse but deterministic and text-free.
- Selected Approach: a handler's resource is the trailing segment of its route path with parameters stripped, joined with the operation's target token (model, table, collection name from the operation fact match); router or module is the second key. Coarse but deterministic and text-free.
## Risks & Mitigations
-340
View File
@@ -1,340 +0,0 @@
# Brief: the detector optimizer
## Problem
`learning-harness` built the measurement layer honestly — a closed family taxonomy, a class-aware pair
scorer, per-family detector configurations, a split enforced by construction, verifier-gated reporting,
and a journal with attribution. What it did **not** build is a search. `run_learning_round` is a
hand-rolled hill-climber: one proposal, one minibatch, one holdout check, a threshold rule, accept or
revert. Every defect four review rounds found in it was a defect of that improvised loop rather than of
the measurement underneath it.
The maintainer's judgement, and the reason for this spec: the remaining work is not a sequence of
patches to that loop. It is adopting an interface and an algorithm that already exist, are published,
and are better than what we would arrive at by fixing ours one finding at a time.
## What the current loop gets wrong, measured
Round zero on vibe-py, `deepseek-v4-flash`, K = 5, $0.69, 23/30 pair-correct:
| family | scorable | recall | silence | Youden | noise floor |
|---|---|---|---|---|---|
| injection | 20 | 0.75 | 0.90 | +0.65 | **11** |
| access_control | 3 | 1.00 | 1.00 | +1.00 | 3 |
| config_secrets | 3 | 1.00 | 1.00 | +1.00 | 1 |
| deserialization | 2 | 1.00 | 1.00 | +1.00 | 0 |
| output_encoding | 1 | 1.00 | 1.00 | +1.00 | 1 |
| path | 1 | 1.00 | 1.00 | +1.00 | 1 |
Four defects follow from the loop's shape, not from its bugs:
1. **The acceptance rule cannot discriminate.** `decide` compares raw deltas: any regression on train or
holdout rejects, a tie rejects. The measured floor says 11 of 20 injection pairs disagree with
*themselves* across five runs, so a delta over a minibatch of 8 is mostly noise. The spec built
`sign_test` and `reliable_change` for exactly this and the rule calls neither.
2. **The holdout is an input to the decision.** `decide` reads `holdout_delta`. Req 5.4 keeps the
*proposer* blind to the holdout, and then the *selector* reads it every round. Repeated rounds
selecting on holdout performance is search-time overfitting of the test set — the same leak class
this project has already measured once, one level up.
3. **Noise is fought with repetition instead of diversity.** K = 5 spends five rollouts per pair per
side estimating variance. The published alternative spends that budget on more candidates and lets a
frontier keep whatever wins somewhere.
4. **The proposer is told a score, not what went wrong.** `FailureFacts` carries outcomes, run strings
and counts. The single largest lever in the published work is textual feedback from the *evaluation
trace* — and our evaluator produces exactly the right material (which finding landed on which line,
whether it fell inside the labeled span, whether the fixed side leaked, which family was claimed)
and then throws all of it away.
## What others built
**GEPA — Reflective Prompt Evolution (Agrawal et al., arXiv:2507.19457, ICLR 2026 oral).** Algorithm 1,
read from the paper rather than from a summary:
- Split `D_train` into `D_feedback` and `D_pareto`. Initialise the candidate pool `P = [Φ]` and score
the seed on every instance of `D_pareto`, giving a score **matrix** `S[candidate][instance]`.
- Each iteration: select a candidate from the Pareto frontier; select a module round-robin; run it on a
minibatch drawn from `D_feedback`; collect scores, traces and **textual feedback** via a feedback
function `μ_f`; have a reflection LM rewrite that module's prompt; re-run the mutated system on the
*same* minibatch.
- **Accept if and only if the minibatch average improved.** Only then is the candidate scored on all of
`D_pareto` and added to the pool with its parent recorded.
- Return the candidate with the best average on `D_pareto`.
Algorithm 2, the selection: for each instance take the best score any candidate achieved; keep the
candidates that achieve it; drop strictly dominated ones; sample a survivor with probability
proportional to the number of instances it leads on.
Three things about that are directly relevant to us. The acceptance gate is **cheap and noisy on
purpose** — a single minibatch comparison — because the frontier, not the gate, is what protects
against a bad accept: anything that wins on one instance is retained. Selection is **stochastic and
diversity-preserving**, which is what stops the search collapsing onto one lineage. And the expensive
full evaluation is spent only on candidates that already passed the cheap gate.
Reported: 93% on MATH with a DSPy `ChainOfThought` program against 67% unoptimised, +10% over MIPROv2
and +20% over GRPO with **35× fewer rollouts**; on IFBench, optimal prompts after 678 rollouts against
GRPO's 24,000. Sample efficiency is the claim that matters for us, because our rollouts cost money.
**DSPy's optimizer interface.** `MIPROv2` proposes instruction and demonstration candidates and searches
them with Bayesian optimisation (Optuna TPE), scoring on minibatches and running a full validation pass
on the incumbent every `minibatch_full_eval_steps` — an explicit two-fidelity design for a noisy metric.
`dspy.GEPA` takes `metric`, `reflection_lm`, a budget (`auto` / `max_full_evals` / `max_metric_calls`),
`reflection_minibatch_size`, `candidate_selection_strategy` (`pareto` | `current_best`) and
`track_stats`, and its metric returns `Prediction(score, feedback)` rather than a float.
**SIMBA — Stochastic Introspective Mini-Batch Ascent (`dspy.SIMBA`).** Two of its ideas fit our corpus
better than GEPA's do. It runs `num_candidates` (6 by default) program variants per example, buckets the
trajectories, and ranks examples by **max-to-min score gap** — deliberately spending the budget on the
instances where variants disagree most. On our corpus that is not a nuisance, it is the training signal:
the 11 injection pairs that disagree with themselves are precisely the pairs where a rule change can flip
an outcome, and uniform minibatch sampling would spend most of its budget on the 9 that never move. It
also alternates two strategies rather than round-robining blindly — `append_a_demo` (a successful
trajectory becomes a demonstration) and `append_a_rule` (the model introspects on the bucket and writes a
rule) — which is exactly the split our levers already have between `counterexamples`/`hard_negatives` and
`checklist`/`prompt`. Candidates are sampled by softmax over historical scores at temperature 0.2, a
cheaper third option beside GEPA's `pareto` and `current_best`.
**The reflection meta-prompt** (GEPA Appendix C) is short and tells us what the feedback function must
produce. It shows the reflector the current instruction, then "inputs, outputs and feedback" for the
minibatch, and asks it to infer the task, extract "all niche and domain specific factual information ...
as a lot of it may not be available to the assistant in the future", and name any generalizable strategy.
That is a specification for `μ_f`, not just a prompt: feedback has to carry the domain facts a detector
cannot re-derive — which sink, which span, which side leaked — rather than a verdict.
**GEPA's merge** is gated strictly: two candidates are crossed only when they share a common ancestor,
optimised *disjoint* sets of prompts, are both Pareto-optimal, and both beat the ancestor's aggregate.
Worth having once we have per-lever candidates, and worth not having before then.
**The standalone `gepa` library** (MIT) exposes `gepa.optimize(seed_candidate, trainset, valset,
task_lm, reflection_lm, max_metric_calls)` over a `GEPAAdapter` protocol of three methods —
`evaluate`, `make_reflective_dataset`, `propose_new_texts` — with a seed candidate that is just a dict
of component name to text. That adapter shape is the interface we want whether or not we take the
dependency: it is the smallest surface that separates *what a detector is* from *how it is searched*.
## What this spec proposes
Restate the detector as an optimisable program and put a published optimiser behind it.
- **A detector is a candidate**: a dict of named text components (`prompt`, `checklist`,
`hard_negatives`, `counterexamples`) plus its frozen non-text settings (tools, budgets). That is
already what `FamilyConfig` holds; it becomes the seed candidate.
- **The metric returns a score and a reason.** `score_pair_family` gains a feedback string built from
what the evaluation already knows. This is the highest-value change in the spec and it is independent
of which optimiser sits on top.
- **`D_pareto` is carved out of the train split.** The holdout leaves the acceptance rule entirely and
is scored once, for reporting, when the search is over. This is a requirements change to Req 9.5 and
the most important honesty consequence of the redesign.
- **An adapter protocol** (`evaluate`, `make_reflective_dataset`, `propose_new_texts`) with two backends:
our own implementation of Algorithms 1 and 2 by default, so the core install keeps
`dependencies = []`, and `gepa` behind an optional extra so its improvements arrive for free.
- **The Pareto pool replaces the accept/revert tree.** `Archive` already records per-pair winners; it
becomes the frontier, and the journal's parent records become the ancestry GEPA needs.
## Where the noise actually is, and why the architecture may be wrong
The maintainer's objection, and I think it is correct: the harness treats the detector as a black box
emitting a noisy scalar, and everything follows from that — K runs, majority vote, a measured floor, a
regression budget, a sign test. That is the stance you take when you cannot see inside the thing you are
measuring. We can see inside it.
The variance is not in the judgement. `temperature: 0` is sent and thinking is disabled, and 11 of 20
injection pairs still disagree with themselves. A traced run shows why: over four steps the model chose
different files to read and different grep patterns, and reached its conclusion from different context
each time. The freedom is in the **trajectory**, and we built statistics to average over it instead of
removing it.
This is exactly what DSPy exists to argue about, and three of its constructs attack the source rather
than the symptom.
**Constraints as predicates, enforced at generation time** (`dspy.Assert`/`Suggest`, arXiv:2312.13382).
A constraint is a boolean function of the output; on failure the past output *and the error* are injected
into a retry. We already have the predicates — is the reported line inside a parsed function, does the
named sink occur at that line, is the claimed family in the taxonomy, did the run cite a tool result —
and we apply every one of them **after the fact, at scoring time, to decide whether to count a finding**.
Applying them at generation time to decide whether to *accept* one is free, needs no dependency, and is
the single highest-value change available. The paper reports near-total intrinsic constraint satisfaction
and 5–15% downstream gains.
**Typed decomposition instead of one free-running loop.** Our detector is a single ReAct loop that decides
which files to read, what to grep, when to stop and what to report; every one of those is a variance
source. DSPy's thesis is that each step gets a signature — a typed input/output contract — and a little
control flow composes them. Ours would be roughly: locate candidate sink sites; trace whether an untrusted
value reaches each; check for a guard between source and sink; judge. **Several of those steps need no
model at all** — the CST, the entry-point mapper, `flows` and `obligations` already answer them
deterministically, and we are currently paying a model to re-derive facts we hold.
**Best-of-N against a checkable reward, instead of a majority vote** (`dspy.Refine`, `BestOfN`). Both
spend K samples. A majority vote assumes the modal answer is right and discards the disagreement; Refine
ranks the N attempts by an explicit reward and feeds a hint from the failures into the next attempt. We
have checkable rewards for free. The same budget, used as evidence rather than as averaging.
**Why this matters more for our models, not less.** GEPA and SIMBA are demonstrated with a strong
reflection model over a weaker task model, and their gains come from evolving instructions. A cheap model
fails on an under-specified task and does well on a narrow typed one. So the cheaper the detector model,
the more of the return sits in decomposition and constraints and the less in prompt evolution — which is
the opposite of the order this spec originally proposed.
### The experiment, run (2026-09-06, `benchmarks/measurements/2026-09-06-trajectory-divergence.json`)
Twelve injection pairs, both sides, K = 5, temperature 0, thinking disabled, recording the tool-call
sequence of every run. $0.23.
**The intended statistic could not be computed. Across 120 runs there was not one repeated trajectory** —
4.67 distinct trajectories per vulnerable side and 4.92 per fixed side, out of 5, and 78 distinct tool-*name*
sequences ignoring arguments entirely. The detector is not sampling one measurement five times. It is running
five different investigations and voting on the results.
Asked as a gradient instead, the relationship is monotone and clean:
| trajectory similarity (Jaccard over tool calls) | run pairs | disagreed on the outcome |
|---|---|---|
| high (≥ 0.75) | 2 | 0 (0.0%) |
| mid (0.4–0.75) | 24 | 1 (4.2%) |
| low (< 0.4) | 94 | 29 (30.9%) |
Three more facts from the same run:
- **The variance is a search problem, and the search is `grep_repo`.** 555 grep calls against 181 `read_file`,
71 `entry_points`, 43 `find_refs`, 13 `read_definition`. The model spends its budget guessing patterns to
locate a sink — while the CST, the entry-point mapper, `flows` and `obligations` already know where the
sinks are.
- **All of the instability is on the vulnerable side.** Every one of the twelve pairs produced exactly one
distinct finding set on the fixed side. The detector is reliably silent on the fix and unreliably vocal on
the bug, so the noise is entirely in detection and none of it in false positives.
- **Findings converge far more than trajectories do.** 78 distinct trajectories, but one to three distinct
finding sets per pair. Many different investigations reach the same answer; six of twelve pairs are stable
despite never once repeating a trajectory.
**What this does to the noise architecture.** The floor is not measuring the detector's uncertainty about a
question. It is measuring how often an unconstrained search happens to reach the sink. A majority vote over
five runs is therefore not variance reduction — it is an unweighted ensemble of five arbitrary search
procedures — and a regression budget derived from it is a budget for the search's luck. K runs, the floor,
the budget and the sign test are all statistical treatment for a structural problem, and the 0% and 4.2%
rows of that table say what happens when the structure is fixed instead.
## Reflection on the approved specification
Read against that measurement, `learning-harness` divides cleanly into a part that stands and a part that
was answering the wrong question.
**Stands, and is worth more now than before.** The closed taxonomy; class-aware pair scoring; the split
enforced by construction; the unscorable vocabulary and its denominators; verifier-gated reporting; the
corpus repairs; the publication machinery; the per-pair archive. None of it depends on the noise framing,
and all of it is what made this experiment possible to run and cost $0.23. A measurement layer that can
answer "is our own instability structural?" in twenty minutes is the thing that was missing before.
**Was answering the wrong question.**
- **Req 3.5, K ≥ 3 runs per pair.** The premise is variance reduction over a repeated measurement. There is
no repeated measurement: 120 runs, 120 trajectories. K runs is an *ensemble of distinct search
procedures*, and a majority vote over it is an unweighted ensemble, not an estimate. It should either be
renamed and published as an ensemble — which changes what every family number means — or dropped once the
search is constrained.
- **Req 8.2, the noise floor as the regression budget.** The floor measures how often an unconstrained
search happens to reach the sink. Deriving a regression budget from it means the worse the search wanders,
the more collateral damage a round is licensed to do. That is backwards, and it is why injection's budget
is 11 of 20.
- **Req 9.5, the acceptance rule.** Already flagged for reading the holdout every round. Now also: it
compares deltas between two ensemble votes, and calls the difference an improvement.
- **The sign test and `reliable_change`.** Added this session in good faith; they test whether an ensemble's
vote moved, which is not the question either.
**What the data adds that neither spec anticipated.**
- **The instability is entirely one-sided.** All twelve pairs produced exactly one finding set on the fixed
side. Specificity is solid and recall is the noisy half, so the leak ceiling and the false-positive
machinery are sound and it is the recall numbers that carry the caveat.
- **`grep_repo` is the variance engine** — 555 calls against 181 `read_file` and 71 `entry_points`. The
single highest-leverage change in either spec is not a prompt, a lever or an optimiser: **stop asking the
model to locate the sink.** The CST, the entry-point mapper, `flows` and `obligations` already know. Give
the detector the candidate sites and ask it the question only a model can answer — does untrusted input
reach this one, and is there a guard.
- **The loop is partly self-correcting**, so this should be a large win rather than a marginal one: 78
distinct trajectories collapse to one to three finding sets per pair, and half the pairs are already
stable despite never repeating an investigation.
**Limits of the experiment, stated so the conclusion is not over-read.** Twelve pairs, one family, one
slice, one model. The high-similarity bucket holds only two run pairs, so its 0% is decoration; the
defensible number is the mid bucket's 4.2% over 24 run pairs against 30.9% over 94. And the direction of
causation is inferred, not established — similar trajectories may be a symptom of easy pairs rather than a
cause of agreement. The way to settle that is the constrained detector itself: fix the investigation, and
if the floor does not collapse, this reading was wrong.
## The constraint that outranks the algorithm
Both algorithms assume a dataset. GEPA splits `D_train` into `D_feedback` and `D_pareto`; SIMBA's default
batch size is 32. **Our injection family has 12 train teachers** — fewer than one SIMBA batch, and after a
GEPA split roughly six instances on each side. Every other family has one to three.
So the honest statement of what this spec can and cannot buy: GEPA and SIMBA are sample-efficient in
*rollouts*, not in *instances*. They will not shortcut a corpus of twelve. The optimiser is worth building
because the current loop is the wrong shape and will stay wrong at any corpus size, but the binding
constraint on results is the train split, and it will remain so after this spec ships. Two things move it,
neither of which is an algorithm: reviewing agent-vfc rows above tier `title` (29 rows, currently unable to
teach anything), and harvesting more pairs per family.
## Scope
In: the search over a family's text components, the feedback function, the candidate pool and its
selection, the budget accounting, and the artifacts that make a run reproducible.
Out, and deliberately: the taxonomy, the classifier, the pair scorer's *outcome* rules, the verifier
boundary, the write roots, redaction, the three gates. Those are the measurement, and a search whose
objective is not trustworthy is worse than no search. Nothing here relaxes them — in particular the
proposer stays blind to the holdout, verifier code and gold labels stay outside every write root, and
no pattern or policy is written by a model.
## Decisions for the maintainer
0. **Is the optimiser even the next thing to build?** Proposed no, and this reverses the order above.
Constraints at generation time, typed decomposition and best-of-N attack the variance at its source; an
optimiser only tunes text around a program whose freedom is the problem. Proposed sequence: measure where
the variance is, constrain generation, decompose the loop, then optimise — and at a trainset of twelve the
optimiser that fits is `BootstrapFewShot` (collect traces that scored well, install them as demonstrations
in a lever we already have) rather than an evolutionary search with nothing to search over.
0b. **Adopt DSPy itself?** Proposed no. Our detector is not a DSPy program — it is an agentic tool loop over
a repository with curated tools, redaction on every prompt, path clamping, a "no findings unless a tool
was called" rule and a final-answer turn, several of which are security properties rather than
conveniences. Handing that loop to `dspy.ReAct` and the calls to litellm would put them behind someone
else's abstraction, and DSPy could only ever be an optional extra under `dependencies = []`, which means
two code paths for the thing that *is* the product. What DSPy would genuinely buy — signatures, adapters,
multi-module composition — we barely need at one module per family. The optimisers are available without
the framework: `gepa` ships standalone under MIT, and SIMBA's two ideas are a hundred lines of logic.
Proposed instead: take the interface shape and the algorithms, not the framework.
1. **Reimplement Algorithms 1 and 2, take `gepa` as an extra, or both?** Both is proposed: ours by
default (zero-dependency, and the security properties are ours to own), `gepa` behind an extra for
comparison. The cost is one adapter and one conformance test that both backends must pass.
2. **Drop the holdout from the acceptance rule?** Proposed yes, and it amends approved Req 9.5. The
holdout becomes what its name says.
3. **Sample instances by disagreement (SIMBA) or uniformly (GEPA)?** Proposed SIMBA's ranking. Our measured
floor makes it the better fit, and it turns the noise floor from an obstacle into an instance sampler.
4. **K = 1 inside the search, K ≥ 3 only for published numbers?** Proposed yes. That is roughly a 5×
budget reclaim spent on more candidates, which is where the sample efficiency comes from.
5. **Does the reflection model differ from the detector model?** The published work uses a stronger
reflector than the task model. We have `deepseek-v4-pro` configured as the judge; proposed to reuse it.
6. **Is `access_control` still the family we care most about?** It has one train teacher. Until agent-vfc
rows are reviewed above tier `title`, the optimiser cannot work on it, whatever its algorithm.
## Open questions
- Does `gepa` run against an OpenAI-shaped base URL that is not OpenAI (our DeepSeek endpoint), and what
does it pull in? Both must be checked before the extra is offered, under the no-undeclared-dependency rule.
- Our "module" granularity: is a module a family, or a lever within a family? Round-robin over levers is
the closer analogue, but the levers are not independent — a checklist edit and a prompt edit interact.
- The feedback function's failure mode is a reflection model that writes plausible prose about a trace it
misread. What check makes a feedback string answerable rather than persuasive?
- GEPA's acceptance is a single noisy comparison. Our floor says a single comparison is close to a coin
flip on this corpus. Does the frontier really absorb that at n ≈ 20 instances, or is our corpus simply
too small for the algorithm's assumptions?
Sources: [GEPA (arXiv:2507.19457)](https://arxiv.org/abs/2507.19457) ·
[DSPy GEPA overview](https://dspy.ai/api/optimizers/GEPA/overview/) ·
[DSPy MIPROv2](https://dspy.ai/api/optimizers/MIPROv2/) ·
[gepa-ai/gepa](https://github.com/gepa-ai/gepa) ·
[DSPy SIMBA](https://dspy.ai/api/optimizers/SIMBA/) ·
[dspy/teleprompt/simba.py](https://github.com/stanfordnlp/dspy/blob/main/dspy/teleprompt/simba.py) ·
[DSPy Assertions (arXiv:2312.13382)](https://arxiv.org/pdf/2312.13382) ·
[dspy.Refine](https://dspy.ai/api/modules/Refine/) ·
[DSPy (arXiv:2310.03714)](https://arxiv.org/pdf/2310.03714)
-294
View File
@@ -1,294 +0,0 @@
# Design Document: constrained-detector
## Overview
The detector stops searching. A deterministic pass enumerates the candidate sites inside a labeled region from the
semantic IR; the model answers one bounded, typed question per candidate with no tools and no choice of what to look
at; the predicates that decide whether a finding counts are applied while it is being made, with one retry. The
unconstrained tool hunter stays in place as the generalist and as the baseline this is measured against.
Two measurements shape every decision below and neither was a guess. **120 runs produced 120 distinct trajectories**,
so the variance is the loop's freedom rather than the model's judgement. And the candidate generator was measured
before anything was built: the **pattern ruleset caps recall at 12.1%**, below the 64.9% the unconstrained loop
already reaches, while the **semantic IR caps it at 96.6%** with a median of ten sites per labeled function.
### Goals
- Two runs of the same detector on the same input make an identical sequence of model calls.
- The limit on what the detector can find is a published number, not an assumption.
- Real-project slices decide whether a change is kept; hand-written teaching code cannot justify one alone.
- The K-run and noise-floor machinery is re-measured and deleted if a constrained detector does not need it.
### Non-Goals
- Prompt optimisation, reflective evolution, any optimiser. Deferred to a later spec, on this feature's evidence.
- Changing the taxonomy, the classifier, the scorer's outcome rules, the split, the verifier boundary or the gates.
- Adopting DSPy as a dependency. The interface shape is borrowed; the framework is not.
## Boundary Commitments
### This Spec Owns
- `learning/candidates.py` — what a candidate is, how the IR enumerates one per family shape, and the ceiling report.
- `learning/judgments.py` — the typed answer, and the toolless call that produces it.
- `learning/predicates.py` — the checks that constrain a claim at generation time and score it afterwards, defined once.
- `learning/slices.py` — deciding versus development slices, the overfitting gap, and the cross-slice rejection rule.
- `run_constrained_detector` in `learning/detectors.py`, beside the existing `run_family_detector`.
- `ousast learning candidates`, and a detector selector on `score` and `baseline`.
### Out of Boundary
- `learning/families.py`, `classify.py`, `split.py`, `verifiers.py`, `canaries.py`, `journal.py`, `acceptance.py`,
`rounds.py`, `publish.py` keep their contracts. `scoring.py` is touched only to import predicates it already
implements inline, and a test asserts its behaviour is unchanged.
- `tool_hunter.run_tool_hunter` keeps its contract and its callers. It is not removed while it scores higher on any
deciding slice.
- The pair corpus, its labels, tiers and splits. The three gates.
### Allowed Dependencies
- Core: standard library only. `dependencies = []` is unchanged.
- Optional: the semantic extra for the IR. Without it, every family reports `unsupported_language` and scores nothing.
- The chat endpoint already resolved by `learning/endpoint.py`. No new provider, no new key.
### Revalidation Triggers
- The candidate ceiling on a deciding slice falling below the unconstrained loop's detection rate on that slice.
- The re-measured trajectory count per pair exceeding one.
- Any change to `scoring.py`'s outcome rules, which would invalidate the baseline this is compared against.
## Architecture
### Existing Architecture Analysis
`run_family_detector` composes `run_tool_hunter` with a family's prompt, checklist, curated tools and context files.
The hunter loop is a ReAct loop: the model calls tools until it stops, then emits findings, and a final turn asks for
JSON when it ended in prose. `_overlay_scan` already parses both sides of a pair and returns function ranges;
`semantic/ir.py` already exposes `FunctionIR` with `calls: tuple[CallSite, ...]` and `binds: tuple[Bind, ...]`,
each carrying argument texts, the names they mention and whether they are constant. `hunter_tools.obligations` and
`flows` already answer the guard and reachability questions deterministically. Nothing new has to be parsed.
### Architecture Pattern & Boundary Map
```mermaid
flowchart TD
R[Region: path, function, admitted families] --> E[candidates.enumerate_candidates]
IR[(semantic IR: calls, binds)] --> E
OB[(obligations, flows, entry_points)] --> E
E -->|"Candidate[] sorted, bounded"| J[judgments.judge_candidates]
J -->|"one typed call per batch, no tools"| M[[chat endpoint]]
M --> P[predicates.check]
P -->|violation| J
P -->|clean| F[StaticFinding: family, detector, verifier tags]
F --> S[learning.scoring.score_pair_family]
P -.->|same definition| S
E --> C[CandidateReport: ceiling per family per slice]
```
The dotted edge is the point of Requirement 4: one definition of the predicates, used to constrain and to score.
### Technology Stack
| Concern | Choice | Why |
|---|---|---|
| Candidate enumeration | `semantic/ir.py` `FunctionIR.calls` and `.binds` | 96.6% ceiling measured; the ruleset's 12.1% is below the current baseline |
| Guards in scope | `hunter_tools.obligations`, `mapping.analyze_entry_points` | already deterministic, already used by the obligations checker |
| Typed answer | `json_object` on the DeepSeek adapter, one array per batch | the provider has no schema mode; the predicates are the schema |
| Retry on violation | one bounded retry with the violation injected | DSPy Assertions' backtracking, without the framework |
| Determinism | candidates sorted, batches fixed, no tools | Req 2.4; the model's text may vary, the call sequence may not |
## File Structure Plan
### Directory Structure
```
src/openultrasast/learning/
├── candidates.py # NEW Candidate, CandidateKind, enumerate_candidates, ceiling, CandidateReport
├── judgments.py # NEW Judgment, judge_candidates, batching, the typed prompt
├── predicates.py # NEW PREDICATES, check_claim, violation reasons
├── slices.py # NEW DECIDING_SLICES, DEVELOPMENT_SLICES, overfitting_gap, cross_slice_verdict
├── detectors.py # MOD run_constrained_detector beside run_family_detector
└── scoring.py # MOD imports predicates; outcome rules unchanged, asserted by the recorded baseline
```
### Modified Files
- `src/openultrasast/learning/detectors.py` — `run_constrained_detector(root, region, config, *, client, model,
taxonomy, ranges)`; `FamilyConfig` gains nothing, because a constrained detector is configured by the same
prompt/checklist/limits and reads no tools.
- `src/openultrasast/learning/scoring.py` — `_hits`, `_others` and `_fabricated` delegate to `predicates`; the
family metrics gain nothing. A test asserts the recorded baseline numbers are reproduced exactly.
- `src/openultrasast/learning/publish.py` — the overfitting gap and the per-slice table (Req 3.1, 3.4).
- `src/openultrasast/config.py` — `[learning] detector = "hunter" | "constrained"`, default `hunter` until the
comparison says otherwise.
- `src/openultrasast/cli.py` — `learning candidates`; `--detector` on `score` and `baseline`.
- `pyproject.toml` — unchanged.
## System Flows
### One region, constrained
```mermaid
sequenceDiagram
participant D as run_constrained_detector
participant E as enumerate_candidates
participant J as judge_candidates
participant P as predicates
D->>E: root, region, family, ranges
E-->>D: Candidate[] (sorted by path, line, name; bounded)
alt no candidates
D-->>D: record no_candidate, return []
end
loop each fixed-size batch
D->>J: batch, config, client
J-->>D: Judgment[] (typed)
D->>P: judgment, candidate, ranges, taxonomy
alt violation
D->>J: retry once, violation injected
J-->>D: Judgment[]
D->>P: re-check
end
P-->>D: clean claims or a recorded discard
end
D-->>D: StaticFinding[] tagged family, detector, verifier
```
## Requirements Traceability
| Criteria | Delivered by |
|---|---|
| 1.1, 1.2 | `candidates.enumerate_candidates`, `CandidateKind` per family shape |
| 1.3 | `candidates.ceiling`, `ousast learning candidates`, `benchmarks/measurements/*-candidate-ceiling.json` |
| 1.4 | `enumerate_candidates` returns `Unsupported` when `parse_file(...).parse_ok` is false |
| 1.5 | `no_candidate` joins the unscorable vocabulary in `scoring.unscorable_reason` |
| 1.6 | `MAX_CANDIDATES_PER_CALL`, `CandidateReport.discarded` |
| 2.1, 2.2, 2.3 | `judgments.judge_candidates`, `Judgment`, `_typed_prompt`; findings built from `Judgment` only |
| 2.4 | `test_two_runs_make_an_identical_sequence_of_calls` over a recording client |
| 2.5 | `StaticFinding.tags` gains `candidate:<path>:<line>:<name>` |
| 3.1, 3.4 | `slices.overfitting_gap`, `publish._slice_table` |
| 3.2, 3.3 | `slices.DECIDING_SLICES`, `slices.cross_slice_verdict` |
| 3.5 | `learning candidates --slice` over every slice; the artifact carries per-slice ceilings |
| 3.6 | `slices.teachable(slice)` and a note emitted with every published number |
| 3.7 | `NON_GATING_FAMILIES` unchanged; `publish` reports memory separately, never inside a web aggregate |
| 4.1, 4.2, 4.3 | `predicates.PREDICATES`, `check_claim`, one retry in `run_constrained_detector`, `discarded` tally |
| 4.4 | `scoring._hits`/`_fabricated` call `predicates`; the baseline-reproduction test pins no drift |
| 5.1, 5.2 | the trajectory script and `run_baseline` re-run against the constrained detector |
| 5.3, 5.4 | the published floor names the `learning-harness` requirements it would amend; K is labelled |
| 6.1 | `score_pair_family` unchanged; the constrained detector only produces findings |
| 6.2, 6.3 | the comparison artifact, per slice, against 64.9% / 12.3% / 52.6% and $0.030 / $0.136 |
| 6.4, 6.5 | the comparison's headline rule; `[learning] detector` keeps `hunter` selectable |
| 7.1 | `resolve_chat_endpoint` unchanged; `learning_endpoint_unavailable` recorded |
| 7.2 | the client's `cost_usd` meter, already wired |
| 7.3 | the committed gate baseline test |
| 7.4 | `has_semantic_extra` guard; `unsupported_language` without it |
| 7.5 | `redact_secrets` on every prompt and persisted trajectory |
## Components and Interfaces
### candidates.py
```python
CandidateKind = Literal["call", "bind", "operation", "config"]
@dataclass(frozen=True)
class Candidate:
path: str
line: int
kind: CandidateKind
name: str # the callee, the bound name, or the operation
text: str # the site as written, clamped
arg_texts: tuple[str, ...]
arg_names: tuple[tuple[str, ...], ...]
binding_chain: tuple[str, ...] # what the IR resolved for the arguments, nearest first
guards: tuple[str, ...] # decorators and in-body guards in scope, from obligations
function: str
@property
def id(self) -> str: ... # f"{path}:{line}:{name}", the tag a finding carries
FAMILY_SHAPE: dict[str, tuple[CandidateKind, ...]] = {
"injection": ("call",), "path": ("call",), "deserialization": ("call",),
"untrusted_destination": ("call",), "prototype": ("call", "bind"),
"access_control": ("operation",), "config_secrets": ("bind", "config"),
"output_encoding": ("call",), "memory": ("call",), "unknown": ("call", "bind"),
}
MAX_CANDIDATES_PER_CALL = 12 # p90 of the measured distribution is 22; two batches covers all but the tail
def enumerate_candidates(root, region, family, *, taxonomy, limit=MAX_CANDIDATES_PER_CALL) -> CandidateSet
def ceiling(cases, *, taxonomy) -> CandidateReport # offline, no model
```
`CandidateSet` carries `candidates`, `discarded`, and `reason` (`""`, `unsupported_language`, `no_candidate`).
Ordering is `(path, line, name)`, which makes the batches and therefore the call sequence deterministic.
### judgments.py
```python
@dataclass(frozen=True)
class Judgment:
candidate_id: str
verdict: bool
source: str # the expression carrying untrusted input, or ""
guard: str # the guard found, or "" meaning none
family: str
rationale: str
def judge_candidates(candidates, config, *, client, model, taxonomy, violation=None) -> tuple[Judgment, ...]
```
One call per batch, `tools=[]`, `json_object=True`. The prompt is the family's prompt and checklist plus the batch
rendered as data; `violation` is the single retry's injected text. Answers that name a candidate id not in the batch
are dropped before the predicates run, because a fabricated id is not a claim about anything.
### predicates.py
```python
@dataclass(frozen=True)
class Violation:
predicate: str
detail: str
def check_claim(judgment, candidate, *, ranges, taxonomy) -> Violation | None
def inside_labeled_function(finding, spans) -> bool # used by scoring._hits
def is_known_family(tag, taxonomy) -> bool # used by scoring._fabricated
```
Named predicates: `line_in_parsed_function`, `sink_at_line`, `family_in_taxonomy`, `source_in_function`. The last
two of the three helpers are the ones `scoring.py` already implements inline; moving them here is a refactor whose
only observable effect must be none, which the recorded baseline pins.
### slices.py
```python
DECIDING_SLICES = ("agent-vfc", "vfc-js", "vfc", "github") # harvested from real projects
DEVELOPMENT_SLICES = ("vibe-py",) # hand-written; cannot justify a change alone
def overfitting_gap(per_slice: Mapping[str, FamilyMetrics]) -> float
def cross_slice_verdict(before, after) -> Verdict # rejects an improvement that regresses a deciding slice, by name
def teachable(slice_name: str, cases) -> bool # False while every row of a slice is below a gating tier
```
## Error Handling
| Condition | Behaviour | Recorded as |
|---|---|---|
| IR cannot parse the side | no candidates, no model call | `unsupported_language` |
| Labeled function has no call or bind | no model call | `no_candidate` |
| Region exceeds the batch bound | judge the first `limit`, count the rest | `CandidateReport.discarded` |
| Predicate fails twice | claim discarded | `predicates.discarded[<name>]`, published |
| Model names a candidate id not in the batch | answer dropped before predicates | `fabricated_candidate` tally |
| Endpoint unavailable | score nothing | `learning_endpoint_unavailable` |
| Cost cap crossed | stop, keep what was measured | `cost_cap_exceeded` |
## Migration and Phases
1. **Candidates and the ceiling, offline.** `candidates.py`, `learning candidates`, the per-slice ceiling artifact
including the real-world slices (Req 3.5). No model call, so the phase is free and its result can still cancel
the design — if the ceiling on a deciding slice is below that slice's current detection rate, stop here.
2. **Predicates extracted.** `predicates.py`, `scoring.py` delegating, the baseline-reproduction test.
3. **Judgment and the constrained detector.** `judgments.py`, `run_constrained_detector`, the determinism test.
4. **Slices and reporting.** `slices.py`, the overfitting gap, `publish` per slice.
5. **The comparison.** Constrained against unconstrained on every slice, the re-measured trajectory count and noise
floor, and the named `learning-harness` amendments if the floor collapses.
Phase 1 is deliberately able to kill the design before any money is spent on it.
@@ -1,172 +0,0 @@
# Requirements Document
> **Status 2026-10-02:** superseded by `model-grounded-detection` (spec.json `superseded_by`). 4 of 20 tasks were done and the tasks were never approved; none will be. Kept as decision history; nothing in src, tests or CI points here.
## Introduction
The detector's instability is structural, not statistical. Measured on 2026-09-06 over twelve injection pairs at
temperature 0 with thinking disabled, **120 runs produced 120 distinct tool-call trajectories** — not one repeat —
and outcome disagreement tracks trajectory divergence (30.9% at low similarity against 4.2% at mid). The model
spends its budget searching: 555 `grep_repo` calls against 71 `entry_points`, guessing where sinks are while the
semantic IR already knows. So `learning-harness`'s K runs are an ensemble of distinct search procedures rather than
repeated measurements, and the noise floor it derives a regression budget from measures how often an unconstrained
search happens to reach the sink.
This feature removes the freedom instead of averaging over it. A deterministic pass enumerates the candidate sites
inside the labeled region from the IR; the model is asked one bounded, typed question per candidate with no tools
and no choice of what to look at; and the predicates the scorer already applies after the fact are enforced at
generation time with a bounded retry. The candidate generator was measured before anything was built: the pattern
ruleset caps recall at 12.1%, below the 64.9% the unconstrained loop already reaches, while the semantic IR caps it
at 96.6% with a median of ten sites per labeled function.
Baseline to beat, `deepseek-v4-flash`, K = 5, 57 scorable web pairs: **detection 64.9%, leak 12.3%, pair-correct
52.6%** — and 83.3% detection on hand-written vibe-py against 44.4% on agent-written agent-vfc, which is the gap
this feature must close rather than widen. Context and measurements: `brief.md`,
`benchmarks/measurements/2026-09-06-trajectory-divergence.json`, `2026-09-06-candidate-ceiling.json`.
## Boundary Context
- **In scope**: candidate enumeration from the semantic IR, per family shape; the typed, toolless judgment step;
constraint predicates enforced at generation time with a bounded retry; the evidence that decides whether the
K-run and noise-floor machinery still earns its place; per-slice reporting that keeps real-world code in front.
- **Out of scope**: the family taxonomy, the classifier, the pair scorer's outcome rules, the split, the verifier
boundary and write roots, redaction, the three gates, and the publication machinery — all of which stand and are
what made this feature's measurements possible. Also out: prompt optimisation, reflective evolution and any
optimiser (deferred to a later spec, on this feature's evidence); finding bugs in JavaScript interpreters, WebGL
renderers or browser engines; memory-safety fuzzing for C and C++; fine-tuning model weights; adopting DSPy as a
dependency.
- **Adjacent expectations**: `learning-harness` keeps owning the taxonomy, the scorer, the split and publication;
its Req 3.5 (K runs), 8.2 (noise floor) and 9.5 (acceptance) are amended by this feature only on evidence it
produces, never on argument. The pair corpus keeps owning labels, tiers and splits. The unconstrained tool hunter
stays as the generalist and as the baseline this feature is measured against.
## Requirements
### Requirement 1: Candidates come from the semantic IR, and the enumerator's ceiling is published
**Objective:** As a maintainer, I want the sites a detector judges to be enumerated deterministically, so that the
model stops searching and the limit of what it can possibly find is a number rather than a hope.
#### Acceptance Criteria
1. The Constrained Detector shall enumerate candidate sites for a region from the semantic IR — call sites and
bindings — and shall not use the pattern ruleset as its candidate generator.
2. The Constrained Detector shall shape candidates by family: a call site for injection, path, deserialization,
untrusted destination and prototype; an operation on a protected resource with the guards in scope for access
control; a configuration assignment or literal for permissive configuration and secrets.
3. The Constrained Detector shall publish, per family and per slice, the fraction of labeled functions for which at
least one candidate was enumerated, and shall name that fraction as the ceiling on its recall.
4. If the semantic IR cannot parse a side, then the Constrained Detector shall record `unsupported_language` and
shall score nothing for that pair rather than falling back to a search.
5. If a labeled function yields no candidate site, then the Constrained Detector shall record `no_candidate` and
report that pair as unscorable with that reason rather than as a miss.
6. While enumerating, the Constrained Detector shall bound the candidates handed to one judgment call and shall
record the bound and the number discarded when a region exceeds it.
### Requirement 2: One bounded, typed question per candidate, with no search
**Objective:** As a maintainer, I want the model to answer only what a model is needed for, so that two runs of the
same detector on the same input take the same path.
#### Acceptance Criteria
1. The Constrained Detector shall call the model with the candidate site, its enclosing function, the binding chain
the IR resolved for its arguments, and the guards in scope, and shall offer it no tools.
2. The Constrained Detector shall require a typed answer per candidate: a verdict, the expression it believes
carries untrusted input, the guard it found or the statement that there is none, and the family.
3. The Constrained Detector shall derive every finding from a typed answer and shall never parse a finding out of
free prose.
4. When the detector is run twice on the same input with the same configuration, the sequence of model calls shall
be identical, and a test shall assert this.
5. The Constrained Detector shall record, for every claim, the candidate that produced it, so that a claim is
traceable to the site it was asked about.
### Requirement 3: Real-world code decides, and the gap to teaching code is published
**Objective:** As a maintainer, I want changes judged on code from real projects rather than on hand-written
teaching examples, so that the tool is not tuned for cases nobody has.
#### Acceptance Criteria
1. The Constrained Detector shall report every number per slice and shall never publish an aggregate across slices
without the per-slice numbers beside it.
2. The Constrained Detector shall designate the slices harvested from real projects — agent-vfc, vfc-js, vfc and
github — as the deciding slices, and the hand-written vibe-py slice as a development slice that cannot on its
own justify accepting a change.
3. If a change improves the development slice and regresses any deciding slice, then the Constrained Detector shall
reject it, and the rejection shall name the slice that regressed.
4. The Constrained Detector shall publish the **overfitting gap** — development-slice detection minus deciding-slice
detection — beside every headline number, and shall state that the baseline gap is 83.3% against 44.4%.
5. The Constrained Detector shall measure and publish the candidate ceiling separately for the real-world slices,
because the functions there are larger than the teaching corpus's and the enumerator's behaviour on them is not
implied by its behaviour on vibe-py.
6. While the deciding slices carry no pair a learning path may teach from, the Constrained Detector shall state that
with every number it publishes rather than presenting the development slice as if it were the corpus.
7. The Constrained Detector shall keep memory safety measured and non-gating, and shall not let the 176 vfc rows
dominate an aggregate that a reader would take as the tool's web-taxonomy performance.
### Requirement 4: The predicates that score a finding also constrain it
**Objective:** As a maintainer, I want the checks that decide whether a finding counts applied while it is being
made, so that a violation is corrected rather than silently discarded.
#### Acceptance Criteria
1. The Constrained Detector shall express as predicates the checks the scorer already applies: the reported line
falls inside a parsed function, the named sink occurs at that line, the claimed family is in the taxonomy, and
the claimed source expression appears in the enclosing function.
2. When a predicate fails, the Constrained Detector shall retry the judgment once with the failed answer and the
predicate that rejected it, and shall not retry more than once per candidate.
3. If a retry also fails a predicate, then the Constrained Detector shall discard the claim and record the predicate
that rejected it, and shall count that discard in a published tally.
4. The Constrained Detector shall apply the same predicates at generation time and at scoring time from one
definition, so the two cannot drift apart.
### Requirement 5: The noise machinery is re-measured, and kept only if it earns its place
**Objective:** As a maintainer, I want the K runs, the noise floor and the regression budget re-measured against a
constrained detector, so that machinery built for an unconstrained one is deleted rather than maintained.
#### Acceptance Criteria
1. The Constrained Detector shall re-run the trajectory measurement against itself and shall publish the resulting
distinct-trajectory count per pair.
2. The Constrained Detector shall re-measure the per-family noise floor at the same K as the baseline and shall
publish it beside the baseline's floor.
3. If the re-measured floor is zero for a family, then the Constrained Detector shall report that family's K-run
requirement and its regression budget as unnecessary and shall name the `learning-harness` requirements that
would be amended.
4. The Constrained Detector shall state, wherever a K-run number is published, whether the runs were repeated
measurements or an ensemble of distinct procedures.
### Requirement 6: The constrained detector is measured against the unconstrained one, on the same corpus
**Objective:** As a maintainer, I want one comparison that says whether this was worth building.
#### Acceptance Criteria
1. The Constrained Detector shall be scored by the existing class-aware pair scorer, with the same denominators,
the same unscorable vocabulary and the same split, and shall introduce no scoring rule of its own.
2. The Constrained Detector shall publish detection, leak and pair-correct rates against the recorded baseline of
64.9%, 12.3% and 52.6%, per slice and in total.
3. The Constrained Detector shall publish its cost per correct pair against the baseline's $0.030 on vibe-py and
$0.136 on agent-vfc.
4. If the constrained detector's detection rate is below the unconstrained loop's on any deciding slice, then that
shall be published as the headline of the comparison rather than as a footnote.
5. The Constrained Detector shall keep the unconstrained tool hunter available and shall not remove it while it
scores higher on any deciding slice.
### Requirement 7: Degradation, budget and boundaries
**Objective:** As a maintainer, I want the same operational contract the rest of the tool has.
#### Acceptance Criteria
1. The Constrained Detector shall run behind the existing chat endpoint resolution and shall record
`learning_endpoint_unavailable` and score nothing rather than fall back to another provider.
2. The Constrained Detector shall meter its own spend through the client's meter and shall stop when a configured
cap is crossed, recording what it had spent.
3. The Constrained Detector shall leave the detection, map and local pair gate outputs byte-identical.
4. The Constrained Detector shall keep the core install at zero dependencies, and every optional capability it uses
shall degrade to a recorded reason without its extra.
5. The Constrained Detector shall pass every prompt and every persisted trajectory through redaction.
@@ -1,27 +0,0 @@
{
"feature_name": "constrained-detector",
"created_at": "2026-09-06T14:30:00Z",
"updated_at": "2026-09-08T00:00:00Z",
"language": "en",
"phase": "tasks",
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": false
}
},
"ready_for_implementation": false,
"depends_on": [
"learning-harness"
],
"approval_note": "2026-09-06 brief drafted at the maintainer's direction ('I'd rather spend time to design the exact DSPy style interface and evolutionary / statistical prompt optimization than fixing piece by piece'). Not approved; requirements not generated.; 2026-09-06 renamed from detector-optimizer to constrained-detector after the trajectory measurement: the brief's own conclusion is that the optimiser is not the next thing to build, and the spec should be named for what it delivers; 2026-09-06 requirements generated at the maintainer's direction, with the maintainer's emphasis recorded as Requirement 3 ('don't forget the emphasis on the real world bugs from the larger projects to avoid overfitting. we otherwise benchmark for cases that are not relevant'); 2026-09-06 requirements approved by the maintainer in session ('approve the requirements and run the design phase'); 2026-09-06 design approved by the maintainer in session ('approve the design and generate the tasks'); 2026-09-08 SUPERSEDED by execution-grounded-detection: this session's measurements showed the noise architecture (K-runs, per-family floor, regression budget, sign test, evolve loop) is a statistical treatment for a structural problem, and that the field (Big Sleep, AIxCC finals, Clearwing) grounds detection in execution rather than in scoring against a labeled corpus. The taxonomy, the IR candidate enumerator (96.6% ceiling) and the obligation checker survive as the web/logic detection path to contribute to a Clearwing-based substrate.",
"superseded_by": "model-grounded-detection"
}
-122
View File
@@ -1,122 +0,0 @@
# Implementation Plan
- [x] 1. The ceiling, offline: what a candidate detector could possibly find
- [x] 1.1 Candidate enumeration from the semantic IR, shaped by family
- `learning/candidates.py`: `Candidate` (path, line, kind, name, text, argument texts and names, the binding chain the IR resolved, the guards in scope, the enclosing function, and an `id` of `path:line:name`); `CandidateKind` of call, bind, operation, config; `FAMILY_SHAPE` mapping each family to the kinds that can carry its bug; `enumerate_candidates` returning a `CandidateSet` with the candidates, the count discarded past the bound, and a reason of empty, `unsupported_language` or `no_candidate`.
- Call and bind candidates come from `FunctionIR.calls` and `.binds`; operation candidates for access control come from the obligations facts with the decorators and in-body guards in scope; config candidates are bindings whose value is a literal in a configuration position. The pattern ruleset is never the generator.
- Ordering is `(path, line, name)` and the bound is a constant, so the batches a later task forms are determined by the input alone.
- Observable: on the shared obligations fixture every family shape yields the sites its docstring names; a file the IR cannot parse yields `unsupported_language` and no candidates; a labeled function with no calls or binds yields `no_candidate`; enumerating twice returns an identical sequence.
- _Requirements: 1.1, 1.2, 1.4, 1.5, 1.6_
- [x] 1.2 The ceiling report and the `learning candidates` command
- `candidates.ceiling(cases, taxonomy)` returns, per family and per slice, the number of labeled functions for which at least one candidate was enumerated, the sites-per-function distribution, and the rows that yielded nothing with their reason; `ousast learning candidates --catalog --slice --json` prints it and writes the artifact. No model is called at any point.
- Observable: the command runs offline with no endpoint configured; the artifact reproduces the recorded web-slice figures (ruleset 12.1%, IR 96.6%, median 10 sites) and adds the deciding slices; running it twice produces a byte-identical artifact.
- _Requirements: 1.3, 3.5_
- _Depends: 1.1_
- [x] 1.3 The stop-or-go reading of the ceiling
- The artifact states, per deciding slice, the ceiling against that slice's recorded detection rate from round zero, and names the comparison as the condition on which this design continues. The reading is written into the spec's Implementation Notes with the numbers, whichever way it goes.
- Observable: the artifact carries agent-vfc, vfc-js, vfc and github ceilings beside 44.4% and the other recorded rates, and the notes state whether any deciding slice fails the condition.
- _Requirements: 1.3, 3.5, 3.7_
- _Depends: 1.2_
- [ ] 2. One definition of the checks, used to constrain and to score
- [ ] 2.1 (P) Predicates extracted from the scorer without changing it
- `learning/predicates.py`: `Violation`, the named predicates `line_in_parsed_function`, `sink_at_line`, `family_in_taxonomy` and `source_in_function`, and `check_claim`; `scoring._hits`, `_others` and `_fabricated` delegate to them.
- Observable: the scorer's unit tests pass unchanged, and a test reproduces the recorded round-zero numbers for vibe-py and agent-vfc from the committed artifacts exactly, so the refactor's observable effect is none.
- _Requirements: 4.1, 4.4, 6.1_
- [ ] 2.2 The unscorable vocabulary gains `no_candidate`
- `no_candidate` joins the reasons `unscorable_reason` can return, distinct from `unresolved_label` (the corpus named a function nothing parses) and from `unsupported_language` (the tooling could not parse at all); it leaves every denominator and is listed by reason.
- Observable: a pair whose labeled function has no call or bind is reported unscorable with `no_candidate` and appears in the per-family unscorable tally, not as a miss.
- _Requirements: 1.5, 6.1_
- _Depends: 1.1, 2.1_
- [ ] 3. The detector
- [ ] 3.1 Typed judgment, one bounded question per candidate, no tools
- `learning/judgments.py`: `Judgment` (candidate id, verdict, source expression, guard or empty, family, rationale) and `judge_candidates(candidates, config, client, model, taxonomy, violation=None)` making one call per fixed-size batch with `tools=[]` and JSON-object mode, the family's prompt and checklist as the system message, and the batch rendered as data. An answer naming a candidate id absent from the batch is dropped before any predicate runs.
- Observable: a scripted client shows one call per batch and no tool schema offered; a reply with a fabricated candidate id yields no judgment and increments the `fabricated_candidate` tally; a reply that is prose yields no judgment rather than a parsed guess.
- _Requirements: 2.1, 2.2, 2.3_
- _Depends: 1.1_
- [ ] 3.2 `run_constrained_detector`, with the predicates enforced and one retry
- `detectors.run_constrained_detector` enumerates, batches, judges, checks each judgment with `check_claim`, retries a violating batch exactly once with the violation injected, discards what still violates, and returns `StaticFinding`s tagged `family:`, `detector:` and `candidate:<id>`. `run_family_detector` is untouched and stays selectable.
- Observable: a judgment naming a line outside the parsed function is retried once and then discarded, and the discard is counted by predicate name; every finding carries the candidate id it came from; the unconstrained hunter still runs from the same configuration.
- _Requirements: 2.5, 4.1, 4.2, 4.3, 6.5_
- _Depends: 2.1, 3.1_
- [ ] 3.3 Two runs make the same calls
- A recording client captures the full sequence of `(model, messages, tools)` for a run; two runs over the same region and configuration produce an identical sequence.
- Observable: the test fails if candidate ordering, batching or the retry condition becomes input-dependent in a way the input does not determine; it is run repeatedly in the suite to catch order instability.
- _Requirements: 2.4_
- _Depends: 3.2_
- [ ] 3.4 Selecting the detector, and its degradations
- `[learning] detector = "hunter" | "constrained"` defaulting to `hunter`; `--detector` on `learning score` and `learning baseline`; without the semantic extra the constrained detector reports `unsupported_language` and scores nothing; without an endpoint it records `learning_endpoint_unavailable`; its spend goes through the client meter and stops at the cap.
- Observable: both detectors run from the same command over the same catalog; the extra-free matrix reports the recorded reason rather than failing; a run with no endpoint still writes its artifact.
- _Requirements: 7.1, 7.2, 7.4, 7.5_
- _Depends: 3.2_
- [ ] 4. Real-world code decides
- [ ] 4.1 (P) Deciding and development slices, and the overfitting gap
- `learning/slices.py`: `DECIDING_SLICES` of agent-vfc, vfc-js, vfc and github; `DEVELOPMENT_SLICES` of vibe-py; `overfitting_gap` over per-slice metrics; `cross_slice_verdict(before, after)` rejecting by name any change that improves a development slice and regresses a deciding one; `teachable(slice, cases)` false while every row of a slice sits below a gating tier.
- Observable: a change that improves vibe-py and regresses agent-vfc is rejected with agent-vfc named; the gap over the recorded baseline reads 83.3% against 44.4%; `teachable("agent-vfc", …)` is false today and the reason is the tier.
- _Requirements: 3.2, 3.3, 3.4, 3.6_
- [ ] 4.2 Per-slice publication, and memory kept out of the web aggregate
- `publish` prints a per-slice table, the overfitting gap beside every headline number, the statement that the deciding slices teach nothing today, and the memory family separately from any figure a reader would take as web-taxonomy performance; no aggregate is printed without its per-slice rows.
- Observable: the published section carries a row per slice and refuses to print a cross-slice aggregate alone; the 176 vfc rows appear under their own heading; the regenerated roadmap section is byte-identical on a second run.
- _Requirements: 3.1, 3.4, 3.6, 3.7_
- _Depends: 4.1_
- [ ] 5. The comparison, and what it retires
- [ ] 5.1 Constrained against unconstrained, per slice
- Both detectors scored by the same scorer over the same catalog at the same K, on every slice; detection, leak and pair-correct rates published against the recorded 64.9%, 12.3% and 52.6%, with cost per correct pair against $0.030 and $0.136. If the constrained detector is behind on any deciding slice, that is the headline of the artifact.
- Observable: one artifact holds both detectors' numbers per slice with the deltas, and its summary line names the deciding-slice result rather than the aggregate.
- _Requirements: 6.2, 6.3, 6.4_
- _Depends: 3.4, 4.2_
- [ ] 5.2 The trajectory and the floor, re-measured
- The trajectory experiment re-run against the constrained detector, publishing distinct trajectories per pair; the per-family noise floor re-measured at the same K as the baseline and published beside it; every published K-run number labelled as repeated measurements or as an ensemble of distinct procedures.
- Observable: the artifact reports the distinct-trajectory count per pair for both detectors on the same twelve pairs, and the floors side by side.
- _Requirements: 5.1, 5.2, 5.4_
- _Depends: 5.1_
- [ ] 5.3 What the floor retires, named
- If a family's re-measured floor is zero, the artifact states that its K-run requirement and its regression budget are unnecessary and names the `learning-harness` requirements that would be amended — 3.5, 8.2 and 9.5 — without amending them here.
- Observable: the artifact carries the per-family verdict and the named requirements, and the spec's Implementation Notes record the escalation to the maintainer.
- _Requirements: 5.3_
- _Depends: 5.2_
- [ ] 5.4 Gates, matrices and the boundary
- Detection, map and local pair gate outputs byte-identical to the committed baseline; both test matrices green; the core install still resolving with no dependencies; every prompt and persisted trajectory redacted.
- Observable: the committed gate-baseline test passes in both matrices, and a planted key in a judgment prompt does not survive into the artifact.
- _Requirements: 7.3, 7.4, 7.5_
- _Depends: 5.1_
## Task-plan review notes
- Phase 1 is deliberately able to end the feature. Tasks 1.1 to 1.3 call no model, so the condition in 1.3 — a deciding slice whose ceiling is below its recorded detection rate — is reachable before any budget is spent. The ruleset variant of this design would have failed that condition at 12.1% against 64.9%.
- Task 2.1 is the only task that touches `scoring.py`, and its observable is that nothing changes. The recorded round-zero artifacts are the pin; if they cannot be reproduced exactly, the refactor is wrong and not the numbers.
- `(P)` marks the two tasks with no dependency on a model call: 2.1 and 4.1 can be built and reviewed while phase 1 runs.
- Task 3.3 is the one that would have caught the defect this whole spec exists for. It belongs in the suite, run repeatedly, not in a one-off measurement.
## Implementation Notes
- Task 1.1 (2026-09-06, RED first: 13 failing tests in `tests/test_candidate_enumeration.py`; then green). `learning/candidates.py`. Two things the fixtures taught: the IR emits a chained call twice at one line, once bare and once carrying the keyword arguments, so deduplication keeps the richest — a bare `filter_by` and `filter_by(user_id=owner)` are the same site and only one of them says what the constraint was. And `leaky` and `constrained` in the shared obligations fixture **both** discharge through `filter_by`; what separates them is that one binding chain ends at `request.args.get('owner')` and the other at `request.user.id`. The test asserts that contrast, which is the judgement the model is being narrowed to.
- Task 1.2 (2026-09-06, RED first: 3 failing tests; then green). `candidates.ceiling(cases, taxonomy, generator="ir"|"ruleset")` and `ousast learning candidates`. The `ruleset` arm exists so the comparison that rejected it stays a measurement rather than a remembered number. The command calls no model and its artifact is byte-identical on a second run.
- Task 1.3 (2026-09-06). **Verdict: go.** Measured offline across every slice, `benchmarks/measurements/2026-09-06-candidate-ceiling-all-slices.json`:
| slice | ceiling | round-zero detection | headroom |
|---|---|---|---|
| agent-vfc (deciding) | **89.7%** | 44.4% | +45.3 |
| vfc (deciding, non-gating) | 84.7% | pending | — |
| vfc-js (deciding) | 52.9% | none recorded | see below |
| github (deciding) | 100.0% | none recorded | — |
| vibe-py (development) | 100.0% | 83.3% | +16.7 |
The margin is largest on agent-vfc, which is the slice that matters most and the one the tool is actually for. Per family: config_secrets, deserialization, output_encoding and untrusted_destination 100%; access_control 90%; path 87.5%; memory 86.4%; injection 84.6%; prototype 71.4%; `unknown` 0% of two rows, which is what abstention looks like.
- **A shape correction the measurement forced, before the detector existed.** `config_secrets` first read 63.6% with a bind-and-literal shape. A permissive default is as often an argument to a call — `cors({origin: '*'})`, `app.use(session({secure: false}))` — as it is a binding; adding call sites to that family's shape took it to 100% and recovered `new-erp-final-app-6e9644`, `zoonk-origin-bfbf67`, `melodix-playerprovider-089931` and `registry-proxy-worker-426847`.
- **Carry-forward, and it is not this feature's to fix.** vfc-js sits at 52.9% because eight of seventeen rows label a function the JavaScript CST names `<anon>`: `module.exports.publish = function () {}` and its relatives. The gap is in function *naming*, not in candidate generation, and it caps the existing hunter on that slice exactly as much. Same class as the `_declarator_start` defects found during learning-harness task 4.1.
- vfc's 21 `unsupported_language` rows are C and C++ files the IR declines; they are the non-gating memory slice and are already excluded from every web number.
+9 -9
View File
@@ -1,4 +1,4 @@
# Brief: contributor-scan — make the model layer a tool someone runs
# Brief: contributor-scan, make the model layer a tool someone runs
**Status:** brief only, unapproved. Successor to `model-grounded-detection`, which is complete (16/16 tasks)
and is not reopened by this.
@@ -22,7 +22,7 @@ is wired. The spec is complete and the feature is unshipped, both at once.
> "a real world tool that helps developers when they want to contribute"
That reframes the work. A contributor does not have a labelled function and a known weakness class — they
That reframes the work. A contributor does not have a labelled function and a known weakness class; they
have a repository they are about to touch and a question about what is risky in it. Everything measured so
far is function-level pairs: a labelled function, a declared family, a known target. None of it exercises the
questions a whole repository asks.
@@ -33,11 +33,11 @@ questions a whole repository asks.
Without this nothing else matters.
2. **Whole-repo scanning.** Region selection from entry points instead of a labelled function; ranking across
thousands of candidates rather than one; a budget policy (the enumerator caps at 8 candidates per region,
which is meaningless at repo scale); and the performance envelope — a CPG build on a 100k-line tree, not a
which is meaningless at repo scale); and the performance envelope: a CPG build on a 100k-line tree, not a
40-line excerpt. **This has never been run.**
3. **A known-vulnerable repository as the target.** The 176 real-world CVE pairs already in the corpus are
single-function excerpts and do not exercise any of the above. Running against a real checkout — curl,
libpng, a WordPress plugin — is the only way to find what breaks at scale.
single-function excerpts and do not exercise any of the above. Running against a real checkout (curl,
libpng, a WordPress plugin) is the only way to find what breaks at scale.
4. **The memory decision.** 173 of those 176 pairs are `memory` family, routed to a deferred execution tier
that is a stub. Either adopt Clearwing for them or scope C memory-safety out explicitly. Right now it is
neither, and a sixth of the corpus sits idle.
@@ -49,12 +49,12 @@ questions a whole repository asks.
These were delivered during `model-grounded-detection` without being in any spec. They belong here so the
record is not scattered:
- **The pipeline itself** (`model/pipeline.py`) — Req 8 existed, task 3.2 built `judge()`, and nothing ever
- The pipeline itself (`model/pipeline.py`): Req 8 existed, task 3.2 built `judge()`, and nothing ever
said to connect an enumerator to it. Built, tested, measured, unspecified.
- **Ops** — a single image carrying tool and engine, a compose file with read-only target and tmpfs scratch,
- Ops: a single image carrying tool and engine, a compose file with read-only target and tmpfs scratch,
bash/zsh/PowerShell wrappers that make the container invisible, and a pyinfra deploy for host installs.
The Docker path is written and syntax-validated but **the image has never been built**.
- **Corpus** — the `owasp` slice (40 validated xss pairs; `output_encoding` went from 1 pair to 41) and
- Corpus: the `owasp` slice (40 validated xss pairs; `output_encoding` went from 1 pair to 41) and
authored sink/sanitizer facts for Java and JavaScript.
## Known limits to carry forward
@@ -63,7 +63,7 @@ record is not scattered:
`debug=True` is dangerous where `httponly=True` is safe and the model cannot tell them apart.
- Interprocedural resolution is the most repeated miss cause across every experiment: OWASP's helper classes,
`angular-http-server`'s `resolveUrl`. Joern supports it; our queries may not be configured for it.
- Five bugs in `model-grounded-detection` shared one shape — a taint-specific assumption applied to every
- Five bugs in `model-grounded-detection` shared one shape: a taint-specific assumption applied to every
family, each found by a measurement rather than a check. Any new family-specific path should be audited for
siblings before it is measured, not after.
@@ -1,4 +1,4 @@
# Task 2.19 — measured input retention and census completeness
# Task 2.19, measured input retention and census completeness
The maintainer accepted the proposed contributor-scan follow-up after the pre-push
NO-GO report. The Kiro root-cause-first workflow identified a concrete shared-boundary
+18 -18
View File
@@ -1,6 +1,6 @@
# Design Document: contributor-scan
## Integration amendment — 2026-09-12 (draft, unapproved)
## Integration amendment, 2026-09-12 (draft, unapproved)
The maintainer has clarified the product as an actionable pre-push safety net across supported
ecosystems. The proposed successor `../pre-push-safety-net/design.md` consumes this driver/backend
@@ -9,7 +9,7 @@ of both targets and graphs. Existing approvals below apply to the prior design,
Proposed shared seams needing requirements/design review before implementation:
- Accept an absolute deadline across graph build, overlays, retries and queries, not only region/call caps.
- Accept an absolute deadline across graph build, overlays, retries and queries as well as region/call caps.
- Accept an optional validated reusable graph lease while preserving ordinary fresh-build scans;
backend remains authoritative for artifact identity, census, compatibility and cleanup.
- Return exact selected/deferred question IDs and context boundaries from ranker decisions;
@@ -45,13 +45,13 @@ for someone who does not have Joern.
### Non-Goals
- New arbiters or families — the three forms exist and are measured.
- New arbiters or families; the three forms exist and are measured.
- Any optimiser, prompt search, or K-run averaging. The predecessor deleted that machinery; this does not
reintroduce it by another name.
- Building a sandbox. Req 10 of the predecessor stands: adopt, never build.
- Changing the gates, the taxonomy, or the fact-table schema.
## The fact tables are an ontology, and it is general-purpose — 2026-09-21
## The fact tables are an ontology, and it is general-purpose, 2026-09-21
The `[[source]]`, `[[sink]]`, `[[sanitizer]]`, `[[operation]]`, `[[discharger]]` and `[[layout]]` rows
are not a pattern list. They are an ontology of program elements and their security relations: what
@@ -87,18 +87,18 @@ repository is in front of the maintainer, which is what task 7.1 exists to make
### This Spec Owns
- `model/regions.py` — entry points and file fallbacks to regions, with the family routing for each.
- `model/scan.py` — the repository-level driver: rank, budget, dispatch to `model/pipeline`, collect.
- `model/regions.py`: entry points and file fallbacks to regions, with the family routing for each.
- `model/scan.py`: the repository-level driver: rank, budget, dispatch to `model/pipeline`, collect.
- The `Stage.MAP` wiring in `cli.py` and the scan's model-layer payload and report section.
- `benchmarks/repos/` — pinned known-vulnerable checkouts and their baselines.
- `benchmarks/repos/`: pinned known-vulnerable checkouts and their baselines.
- The decisions in Req 7 and their record.
### Out of Boundary
- `model/pipeline`, `taint`, `dominance`, `config_value`, `ladder`, `specs`, `candidates` — built, measured,
- `model/pipeline`, `taint`, `dominance`, `config_value`, `ladder`, `specs`, `candidates`: built, measured,
used unchanged. If this feature needs to change one, that is a signal the change belongs in its own commit
with its own measurement.
- `mapping.analyze_entry_points`, the overlay, the obligations checker, the gates — untouched.
- `mapping.analyze_entry_points`, the overlay, the obligations checker, the gates: untouched.
### Allowed Dependencies
@@ -117,8 +117,8 @@ repository is in front of the maintainer, which is what task 7.1 exists to make
### Existing Architecture Analysis
`ousast scan` runs `policy_load → ruleset_load → preprocess → entry_point_mapping → rank → calibrate →
STATIC → MAP → REGRESS → report`. Inside MAP it already builds a complexity map, runs the overlay, and — the
closest analogue to what we are adding — runs `_check_obligations(root, targets, entries, facts, settings,
STATIC → MAP → REGRESS → report`. Inside MAP it already builds a complexity map, runs the overlay, and (the
closest analogue to what we are adding) runs `_check_obligations(root, targets, entries, facts, settings,
runtime, hunter_model)`, which takes entry points, produces findings, and merges them by re-ranking.
The model layer slots in exactly there, with the same shape and the same failure discipline. `entry_points`
@@ -167,10 +167,10 @@ benchmarks/repos/
### Changed
- `cli.py` — a `model` stage inside MAP, mirroring `_check_obligations`; its payload into the manifest and a
- `cli.py`: a `model` stage inside MAP, mirroring `_check_obligations`; its payload into the manifest and a
report section.
- `model/report.py` — a per-rung summary block for a scan (it currently reports per-slice for corpus runs).
- `config.py` — a `ModelConfig`-style block: enabled, budget, max regions. Zero-dep, defaults conservative.
- `model/report.py`: a per-rung summary block for a scan (it currently reports per-slice for corpus runs).
- `config.py`: a `ModelConfig`-style block: enabled, budget, max regions. Zero-dep, defaults conservative.
### Unchanged and load-bearing
@@ -194,8 +194,8 @@ class ScanRegion:
def regions_for(entries, targets, *, taxonomy, language_of) -> tuple[ScanRegion, ...]
```
Ranking uses what `EntryPointRecord` already carries — an externally-reachable entry point at a trust
boundary outranks an internal one — rather than inventing a new signal.
Ranking uses what `EntryPointRecord` already carries (an externally-reachable entry point at a trust
boundary outranks an internal one) rather than inventing a new signal.
### model/scan.py
@@ -264,10 +264,10 @@ each; everything past exhaustion counted into `regions_unjudged`.
1. **Wire it (Req 1, 2).** Regions, the MAP stage, findings with rungs reaching a report. Gate-protected.
Nothing else matters until a user can see a rung.
2. **Scale it (Req 3, 4).** Budget and ranking, then the first run against a pinned known-vulnerable
checkout — the measurement that has never been taken and can reshape the rest.
checkout, the measurement that has never been taken and can reshape the rest.
3. **Ship it (Req 5, 6).** Contributor output; build and verify the image that is currently only defined.
4. **Close the deferred (Req 7).** The memory decision; retire `evidence_level`.
5. **Grow the corpus (Req 9).** PHP/WordPress first — the families the model arbitrates best. C CVEs only
5. **Grow the corpus (Req 9).** PHP/WordPress first: the families the model arbitrates best. C CVEs only
after 7.1.
6. **Regression baselines (Req 10).**
@@ -1,4 +1,4 @@
# Release remediation research — 2026-09-14
# Release remediation research, 2026-09-14
Status: investigation under the maintainer's instruction to continue towards release.
This record does not approve rollout, replace the existing design, or mark a production
+19 -19
View File
@@ -12,7 +12,7 @@ hunter's 76.7% / 12.3%, at a fifth of the model calls. All 16 of its tasks are c
from a measurement harness, not from the tool. This feature closes that gap and answers the question the
maintainer actually posed: *a real world tool that helps developers when they want to contribute.*
That framing changes the work. A contributor has no labelled function and no declared weakness class — they
That framing changes the work. A contributor has no labelled function and no declared weakness class; they
have a repository they are about to touch. Everything measured so far is function-level pairs, which
exercise none of the questions a whole repository asks: which regions to look at, how to rank thousands of
candidates, what to spend, and whether a CPG build on a 100k-line tree finishes at all.
@@ -22,13 +22,13 @@ Context: `brief.md`, `.kiro/specs/model-grounded-detection/`, and the committed
## Boundary Context
- **In scope**: wiring `model/pipeline` into `ousast scan`; region selection from entry points; ranking and
- In scope: wiring `model/pipeline` into `ousast scan`; region selection from entry points; ranking and
budget at repository scale; the performance envelope; running against known-vulnerable checkouts;
contributor-facing output; the packaging already built (image, compose, shell wrappers, pyinfra); the
deferred memory-family decision; retiring `evidence_level`.
- **Out of scope**: new arbiters or families (the three forms exist); prompt optimisation of any kind;
building a sandbox (Req 10 of the predecessor stands — adopt, never build); changing the gates.
- **Carried forward, not re-litigated**: the evidence ladder, the taxonomy, the fact tables, and the finding
- Out of scope: new arbiters or families (the three forms exist); prompt optimisation of any kind;
building a sandbox (Req 10 of the predecessor stands: adopt, never build); changing the gates.
- Carried forward, not re-litigated: the evidence ladder, the taxonomy, the fact tables, and the finding
that the model supplies precision while the LLM supplies recall.
## Requirements
@@ -45,7 +45,7 @@ built is something I can run rather than something a harness demonstrates.
2. The system shall report model findings alongside the existing pattern and overlay findings, without
removing either.
3. When no CPG engine is available, the scan shall behave exactly as it does today and record
`cpg_unavailable` — the model layer is additive, never a precondition.
`cpg_unavailable`; the model layer is additive, never a precondition.
4. The detection, map and local pair gates shall remain byte-identical to the committed baseline.
5. The system shall expose the model layer's per-rung counts in the scan's own output, so a user can see how
much of the result the model arbitrated rather than inferring it.
@@ -109,7 +109,7 @@ in the corpus exercises it.
#### Acceptance Criteria
1. Every finding shall carry its rung, its witness, and the file and line it concerns.
2. The system shall state plainly what a `suspicion` means — that the model could not confirm it — so a
2. The system shall state plainly what a `suspicion` means (that the model could not confirm it) so a
reader does not treat it as established.
3. The system shall write findings to the writable output location, never into the repository under analysis.
4. The system shall summarise a scan in a form readable without opening a JSON file.
@@ -121,7 +121,7 @@ in the corpus exercises it.
throughout, and no criterion here forbade it.)*
6. Where a family's abstraction cannot decide a weakness class, the system shall state that limit in the
report, and no scan shall present an absence of findings as evidence of absence. *(Added 2026-09-09.
`c/memory` scored zero true positives across 89k lines of C for a structural reason — it models string
`c/memory` scored zero true positives across 89k lines of C for a structural reason: it models string
functions and libpng overflows through arithmetic. A silent scan reading as a clean bill of health is
worse than noise because it is quiet, and it is the one failure mode the rung ladder does not guard.)*
@@ -148,7 +148,7 @@ silently caps what the corpus can tell us.
#### Acceptance Criteria
1. The system shall either reach the `memory` family through an adopted execution tier, or record explicitly
that C memory-safety is out of scope — 173 of the 176 real-world CVE pairs currently sit idle behind a
that C memory-safety is out of scope; 173 of the 176 real-world CVE pairs currently sit idle behind a
stub.
2. The system shall retire `evidence_level` in favour of `rung`, or record why both are kept.
3. Each decision shall be recorded with its reason where a later reader will find it.
@@ -167,7 +167,7 @@ this one, because each exists due to a specific past error.
4. The core install shall remain zero-dependency, with the CPG engine, the LLM endpoint and any execution
tier optional and degrading to a recorded reason.
5. Any new family-specific code path shall be checked for sibling paths that need the same change, before it
is measured — five bugs in the predecessor shared exactly that shape.
is measured; five bugs in the predecessor shared exactly that shape.
### Requirement 9: Grow the corpus where the engine can use it
@@ -177,14 +177,14 @@ what developers actually contribute to rather than against 30 pairs of one slice
#### Acceptance Criteria
1. The system shall add a **PHP/WordPress** slice covering plugin and core weaknesses. WordPress plugin CVEs
are overwhelmingly injection, XSS and authorization bypass — the families the model layer already
arbitrates well — and `php2cpg` works once a PHP interpreter is present, which the shipped image now
are overwhelmingly injection, XSS and authorization bypass (the families the model layer already
arbitrates well) and `php2cpg` works once a PHP interpreter is present, which the shipped image now
provides.
2. The system shall add **real-world C library CVEs** beyond the existing curl and OpenSSL rows — libpng and
2. The system shall add **real-world C library CVEs** beyond the existing curl and OpenSSL rows: libpng and
the image libraries, OpenSSH, libpam, and vulnerable kernel modules were named. These are gated on Req 7.1:
until the memory-family decision is closed they would join 173 pairs already sitting idle, so the decision
comes first and the harvest follows it.
3. The system shall add further **vibe-code** pairs — agent-written and human-written web application bugs —
3. The system shall add further **vibe-code** pairs (agent-written and human-written web application bugs)
because that slice is where the tool's stated audience actually works, and it is currently 35 pairs.
4. Before any slice is vendored at scale, the system shall verify on a sample that the model can **read** its
pairs, and shall vendor only the categories that a measurement shows it can arbitrate. The OWASP harvest
@@ -194,7 +194,7 @@ what developers actually contribute to rather than against 30 pairs of one slice
contiguous key-shaped literal.
6. Each new slice shall report as its own row and shall never be blended into a headline, and a synthetic
slice shall say in its catalog that it measures breadth rather than real-world performance.
7. The system shall record, per slice, how many pairs the model can arbitrate — so corpus growth is measured
7. The system shall record, per slice, how many pairs the model can arbitrate, so corpus growth is measured
by what it teaches rather than by how many rows it adds.
### Requirement 10: Regression tuning over a large codebase
@@ -206,7 +206,7 @@ tables and queries can be checked for regressions that pair scoring cannot see.
1. The system shall support a repeatable scan over a pinned checkout of a large repository, recorded as a
committed baseline.
2. When the fact tables or queries change, the system shall report the delta against that baseline —
2. When the fact tables or queries change, the system shall report the delta against that baseline:
findings gained, findings lost, and rung movements.
3. The system shall treat an unexplained loss of a previously-entailed finding as a regression to be
investigated, not a number to be accepted.
@@ -214,7 +214,7 @@ tables and queries can be checked for regressions that pair scoring cannot see.
---
## Amendment record — group 2 (approved 2026-09-09)
## Amendment record, group 2 (approved 2026-09-09)
Group 2 was specified as the phase that can reshape the rest, and it did. Three amendments were drafted from
what it measured and **approved by the maintainer on 2026-09-09**; they are folded into the numbered
@@ -227,7 +227,7 @@ requirements above and marked there with the evidence that produced them.
| C — a checkout that can demonstrate detection | Req 4.6 | libpng satisfies 4.1 and cannot demonstrate detection |
**A is only half implemented, deliberately.** Task 2.11 made pattern matches proportionate. The same argument
applies to the model layer's own suspicion band — 38 of vampi's 60 findings — which is an enumerated
applies to the model layer's own suspicion band (38 of vampi's 60 findings), which is an enumerated
candidate the graph could not decide and an LLM affirmed: a pattern match with an opinion attached, not a
reasoned claim. Task 2.17 carries that half.
@@ -238,5 +238,5 @@ reasoned claim. Task 2.17 carries that half.
(2.6), where the leak fell 42.9% → 0.0% with pair-correct unchanged.
* **Req 3.3's honesty counters were the load-bearing part.** `regions_unjudged` is what made a budget that
stopped free work visible at all.
* **Req 11.2's byte-identical gates held through every one of nine defect fixes** — which says the same thing
* **Req 11.2's byte-identical gates held through every one of nine defect fixes**, which says the same thing
from the other side: the corpus could not have caught any of them.
@@ -18,8 +18,8 @@ Derive a mechanism record from a pair by parsing the vulnerable excerpt with the
## Scope
- **In**: mechanism export from pairs; shape normalization; variant search as a proposer in MAP; leave-one-out measurement command and payload; `mechanisms` improve lever with validator rules; store hygiene (tier, provenance, dedupe, version); offline tests.
- **Out**: LLM-authored shapes; changing dispositions or evidence rungs; embedding retrieval changes (existing prove-order reuse stays); inter-file flow (reachability spec); any merge gate; vendoring more pairs (corpus spec).
- In: mechanism export from pairs; shape normalization; variant search as a proposer in MAP; leave-one-out measurement command and payload; `mechanisms` improve lever with validator rules; store hygiene (tier, provenance, dedupe, version); offline tests.
- Out: LLM-authored shapes; changing dispositions or evidence rungs; embedding retrieval changes (existing prove-order reuse stays); inter-file flow (reachability spec); any merge gate; vendoring more pairs (corpus spec).
## Boundary Candidates
@@ -35,13 +35,13 @@ Derive a mechanism record from a pair by parsing the vulnerable excerpt with the
## Upstream / Downstream
- **Upstream**: `PairCase`/`load_pair_catalog` with `review_tier`; `FileIR`; `MechanismStore`; `adjudicate`; `run_improvement` gate with per-profile holdout.
- **Downstream**: `reachability-flow-model` attaches paths to variant findings; `guard-dominance-regime` consumes the guard shapes as sanitizer candidates; reports show mechanism ids on findings.
- Upstream: `PairCase`/`load_pair_catalog` with `review_tier`; `FileIR`; `MechanismStore`; `adjudicate`; `run_improvement` gate with per-profile holdout.
- Downstream: `reachability-flow-model` attaches paths to variant findings; `guard-dominance-regime` consumes the guard shapes as sanitizer candidates; reports show mechanism ids on findings.
## Existing Spec Touchpoints
- **Extends**: `propose-adjudicate-prove` mechanism memory (additive fields, second write path), `three-stage-scan` MAP stage (one more proposer), `harnessx-self-improving-rulesets` improve loop (one more bounded lever).
- **Depends**: `pair-corpus-honesty` Req 9 (tiers) for which pairs may seed.
- Extends: `propose-adjudicate-prove` mechanism memory (additive fields, second write path), `three-stage-scan` MAP stage (one more proposer), `harnessx-self-improving-rulesets` improve loop (one more bounded lever).
- Depends: `pair-corpus-honesty` Req 9 (tiers) for which pairs may seed.
## Constraints
@@ -107,9 +107,9 @@ tests/
### Modified Files
- `semantic/overlay.py` — `OverlayRecord.mechanism_id: str | None = None` (additive).
- `regress/candidate.py` — exposes `hotspot_from_finding`; variant findings enter the ranked candidate slice as zero-score hotspots from `cli.py` (after every scored promotion, inside the cap, through the safety check).
- `config.py` — `[variants] enabled=true, max_mechanisms=500`.
- `semantic/overlay.py`: `OverlayRecord.mechanism_id: str | None = None` (additive).
- `regress/candidate.py`: exposes `hotspot_from_finding`; variant findings enter the ranked candidate slice as zero-score hotspots from `cli.py` (after every scored promotion, inside the cap, through the safety check).
- `config.py`: `[variants] enabled=true, max_mechanisms=500`.
## System Flows
@@ -8,15 +8,15 @@ OpenUltraSAST's labeled pair corpus measures the detector but does not improve i
## Boundary Context
- **In scope**: mechanism export from pairs; shape normalization per language; variant search proposer in MAP; leave-one-out evaluation; `mechanisms` improve lever; store schema additions; reports showing mechanism ids; offline tests.
- **Out of scope**: taint, facts, walker, dispositions, evidence rungs; embedding retrieval changes; inter-file flow; LLM-authored shapes; catalogs and scorer changes beyond reading `PairCase`; any merge gate.
- **Adjacent expectations**: `Mechanism` gains additive fields only; `order_promotions` keeps working on old records; the improve loop's existing clauses and the per-profile holdout clause apply unchanged; `local` remains the only slice that can fail CI.
- In scope: mechanism export from pairs; shape normalization per language; variant search proposer in MAP; leave-one-out evaluation; `mechanisms` improve lever; store schema additions; reports showing mechanism ids; offline tests.
- Out of scope: taint, facts, walker, dispositions, evidence rungs; embedding retrieval changes; inter-file flow; LLM-authored shapes; catalogs and scorer changes beyond reading `PairCase`; any merge gate.
- Adjacent expectations: `Mechanism` gains additive fields only; `order_promotions` keeps working on old records; the improve loop's existing clauses and the per-profile holdout clause apply unchanged; `local` remains the only slice that can fail CI.
## Requirements
### Requirement 1: Mechanisms seeded from trusted pairs
**Objective:** As a maintainer, I want each reviewed pair to become a mechanism record, so that the corpus is a library the detector can search, not only a ruler.
**Objective:** As a maintainer, I want each reviewed pair to become a mechanism record, so that the corpus is both a library the detector can search and a ruler.
#### Acceptance Criteria
@@ -64,7 +64,7 @@ OpenUltraSAST's labeled pair corpus measures the detector but does not improve i
### Requirement 5: A recall-raising lever in the improve loop
**Objective:** As a maintainer, I want the improve loop to be able to admit mechanisms, so that it can raise recall and not only shadow noisy rules.
**Objective:** As a maintainer, I want the improve loop to be able to admit mechanisms, so that it can raise recall as well as shadow noisy rules.
#### Acceptance Criteria
@@ -2,9 +2,9 @@
## Summary
- **Feature**: `corpus-seeded-mechanisms`
- **Discovery Scope**: Extension of mechanism memory, MAP stage, pair eval, and the improve loop
- **Key Findings**:
- Feature: `corpus-seeded-mechanisms`
- Discovery Scope: Extension of mechanism memory, MAP stage, pair eval, and the improve loop
- Key Findings:
- `semantic/mechanisms.py` already has `Mechanism`, `MechanismStore` (append-only JSONL), `append_mechanism`, embedding cache, hard filters by language/CWE/tags, and `order_promotions`. It is written only by `cli._append_proven_mechanisms` after a sandbox trigger, so it is empty on every real tree.
- Clearwing's recall lever is the variant loop: a proven mechanism becomes a structural search over the messy tree. OpenUltraSAST copied the store and the retrieval, not the loop.
- `FileIR` call sites already carry callee name, argument texts, argument identifier lists, and constant flags; binds carry source text and names. That is enough for a shape without new parsing.
@@ -16,30 +16,30 @@
### Mechanism store today
- **Context**: Can the existing store carry corpus-derived records?
- **Sources Consulted**: `semantic/mechanisms.py`, `cli.py` `_append_proven_mechanisms`, `propose-adjudicate-prove` design §Mechanism.
- **Findings**: `Mechanism` fields are summary, cwe, language, tags, keywords, what_made_it_exploitable, source_finding_id, source_repo. Identity is the pattern. JSONL is authoritative; vectors are a rebuildable cache keyed by id and model.
- **Implications**: add `origin` (`sandbox` | `corpus`), `review_tier`, `pairs`, `shape` (structured), `guard` as additive fields with defaults so old rows load. A second writer (`append_from_pair`) beside the sandbox writer.
- Context: Can the existing store carry corpus-derived records?
- Sources Consulted: `semantic/mechanisms.py`, `cli.py` `_append_proven_mechanisms`, `propose-adjudicate-prove` design §Mechanism.
- Findings: `Mechanism` fields are summary, cwe, language, tags, keywords, what_made_it_exploitable, source_finding_id, source_repo. Identity is the pattern. JSONL is authoritative; vectors are a rebuildable cache keyed by id and model.
- Implications: add `origin` (`sandbox` | `corpus`), `review_tier`, `pairs`, `shape` (structured), `guard` as additive fields with defaults so old rows load. A second writer (`append_from_pair`) beside the sandbox writer.
### Shape derivation from FileIR
- **Context**: Avoid regex and LLM authoring.
- **Sources Consulted**: `semantic/ir.py` (`CallSite.arg_names`, `arg_is_constant`, `Bind.names`, `FunctionIR.params`), `semantic/taint.py` (`_source_in`, parameter seeding).
- **Findings**: for the labeled function, the sink call site is the call whose trailing name equals the label's sink or whose line equals the label's line; source positions are argument indexes whose identifiers trace by binds to a fact source, a parameter, or a container read. Guard kind comes from statements on the fixed side absent on the vulnerable side: `if` with `== NULL`/`is None`/`!x` → `null_test`; comparison with `<`, `>`, `len(` → `bounds_test`; membership in a literal collection or `startswith`/regex → `allowlist_test`; identity read from an auth context (`request.state`, `req.user`, `session`) → `auth_check`; parameterized second argument → `parameterized_call`; changed declared type → `type_change`.
- **Implications**: `semantic/variants.py` implements `derive_shape(pair_ir_vuln, pair_ir_fixed, label)` and `match_shape(file_ir, shape)`. Guard classification is heuristic and closed; unknown → `none`.
- Context: Avoid regex and LLM authoring.
- Sources Consulted: `semantic/ir.py` (`CallSite.arg_names`, `arg_is_constant`, `Bind.names`, `FunctionIR.params`), `semantic/taint.py` (`_source_in`, parameter seeding).
- Findings: for the labeled function, the sink call site is the call whose trailing name equals the label's sink or whose line equals the label's line; source positions are argument indexes whose identifiers trace by binds to a fact source, a parameter, or a container read. Guard kind comes from statements on the fixed side absent on the vulnerable side: `if` with `== NULL`/`is None`/`!x` → `null_test`; comparison with `<`, `>`, `len(` → `bounds_test`; membership in a literal collection or `startswith`/regex → `allowlist_test`; identity read from an auth context (`request.state`, `req.user`, `session`) → `auth_check`; parameterized second argument → `parameterized_call`; changed declared type → `type_change`.
- Implications: `semantic/variants.py` implements `derive_shape(pair_ir_vuln, pair_ir_fixed, label)` and `match_shape(file_ir, shape)`. Guard classification is heuristic and closed; unknown → `none`.
### Where variant search sits
- **Context**: Propose, adjudicate, prove roles must stay separate.
- **Sources Consulted**: `three-stage-scan` design (MAP stage, hunter suspicion), `propose-adjudicate-prove` (overlay records, coverage), `regress/candidate.py`.
- **Findings**: MAP already runs overlay and the tool hunter; variant search is a third proposer that reads `FileIR` and the store. Findings at `suspicion`, origin `variant`; when the overlay has a flow at that call site, merge (attach mechanism id to the overlay record). Candidate eligibility follows the hunter-eligibility path being added in `reachability-flow-model` or the sev-5 forced-hotspot path today.
- **Implications**: no disposition change; one new finding origin; manifest counters.
- Context: Propose, adjudicate, prove roles must stay separate.
- Sources Consulted: `three-stage-scan` design (MAP stage, hunter suspicion), `propose-adjudicate-prove` (overlay records, coverage), `regress/candidate.py`.
- Findings: MAP already runs overlay and the tool hunter; variant search is a third proposer that reads `FileIR` and the store. Findings at `suspicion`, origin `variant`; when the overlay has a flow at that call site, merge (attach mechanism id to the overlay record). Candidate eligibility follows the hunter-eligibility path being added in `reachability-flow-model` or the sev-5 forced-hotspot path today.
- Implications: no disposition change; one new finding origin; manifest counters.
### Leave-one-out
- **Context**: Prove that corpus growth raises recall, honestly.
- **Findings**: pair eval materializes each side into a temp tree already; a leave-one-out runner seeds a temp store from the other trusted pairs and runs variant search on both sides, scoring with the pair scorer's detection and leak rules.
- **Implications**: `pairs.py` gains a read-only consumer path (`evaluate_loo`) or a sibling module `semantic/loo.py` to keep the scorer's boundary; report per slice, profile, mechanism; artifact only.
- Context: Prove that corpus growth raises recall, honestly.
- Findings: pair eval materializes each side into a temp tree already; a leave-one-out runner seeds a temp store from the other trusted pairs and runs variant search on both sides, scoring with the pair scorer's detection and leak rules.
- Implications: `pairs.py` gains a read-only consumer path (`evaluate_loo`) or a sibling module `semantic/loo.py` to keep the scorer's boundary; report per slice, profile, mechanism; artifact only.
## Architecture Pattern Evaluation
@@ -54,27 +54,27 @@
### Decision: Seed only from seeded and reviewed tiers
- **Context**: `title` pairs are unreviewed guesses; `advisory` pairs have correct fixes but unlabeled mechanisms.
- **Selected Approach**: export reads `review_tier`; `advisory`/`title` may be scored leave-one-out as held-out targets but never seed.
- **Trade-offs**: fewer seeds at first; honest about what the detector was taught.
- Context: `title` pairs are unreviewed guesses; `advisory` pairs have correct fixes but unlabeled mechanisms.
- Selected Approach: export reads `review_tier`; `advisory`/`title` may be scored leave-one-out as held-out targets but never seed.
- Trade-offs: fewer seeds at first; honest about what the detector was taught.
### Decision: Variant is a proposer at suspicion
- **Context**: Evidence honesty.
- **Selected Approach**: origin `variant`, `suspicion`; merge with overlay flow when present; sandbox may raise.
- **Trade-offs**: variant-only hits do not fail CI; correct.
- Context: Evidence honesty.
- Selected Approach: origin `variant`, `suspicion`; merge with overlay flow when present; sandbox may raise.
- Trade-offs: variant-only hits do not fail CI; correct.
### Decision: Lever admits records, not shapes
- **Context**: Keep the validator closed.
- **Selected Approach**: `mechanisms` lever edits reference record ids produced by the exporter; free-form shapes are rejected.
- Context: Keep the validator closed.
- Selected Approach: `mechanisms` lever edits reference record ids produced by the exporter; free-form shapes are rejected.
## Risks & Mitigations
- Coarse shapes over-match (`exec(x)` everywhere) — source-position requirement plus fact/parameter/container check; report leak rate on fixed sides leave-one-out; the per-profile gate rejects admissions that leak.
- Guard heuristics misclassify — closed set with `none`; reported, not used for demotion.
- Store growth — dedupe by shape; one record per shape with pair provenance list.
- Leave-one-out cost — one temp store per pair over tens of pairs; fine; cache parsed IR per excerpt.
- Coarse shapes over-match (`exec(x)` everywhere): source-position requirement plus fact/parameter/container check; report leak rate on fixed sides leave-one-out; the per-profile gate rejects admissions that leak.
- Guard heuristics misclassify: closed set with `none`; reported, not used for demotion.
- Store growth: dedupe by shape; one record per shape with pair provenance list.
- Leave-one-out cost: one temp store per pair over tens of pairs; fine; cache parsed IR per excerpt.
## References
+95
View File
@@ -0,0 +1,95 @@
# Brief: detection-core
## Problem
Detection in this project is specified as a set of per-family, per-arm runners (prover,
hunter, hook, proof arm). Corrections patch one arm and do not compose: v1→v2 retunes
transferred 0/17 on untouched cases, and the 2026-10-30 admission run returned NO-GO
(0/12 pairs, in-family 0/3, 9/12 pilot repositories with zero framework recognition).
Learned components have nothing constraining them, so DSPy fitted a shape shortcut
(deletion count separates reversed fixes at 0.90). The root cause: the core security
object — one deterministic, checkable artifact per change that makes a defect claim
provable (with witness) and a non-claim bounded (with coverage) — was implicit, never
specified.
## Current State
- The ordered trace exists for one family: `access-control-prover` implements entry →
hops → operation → witnesses → dominance → three-way judgment, with digests and the
Req 12 admission workflow (dev pool + untouched admission pool). 13.3's four-arm run
proved attribution works around the trace (analyzer_only −246 unguarded).
- Measured ceilings of the chain: entry recognition (9/12 pilot repos unrecognized),
call resolution (11.9% complete), guard dominance coarseness, 120s pre-push budget
(whole-repo CPGs do not fit: libpng 446s, 637-file PMPro does not load).
- `valid-detection-benchmark` provides the measurement instrument; `detection-quality.md`
(steering, 2026-10-30) holds standing rules and pre-registered stop conditions.
## Desired Outcome
One specified core object — the security trace — with a contract that every detection arm,
fact pack, and consumer reuses: given a push (base, head) within a fixed budget, output
proven defect claims (each with its witness) plus explicit coverage of what was not
decided, on repositories never seen. No fitted component ever sees pool labels.
## Approach
Specify the trace as the core: scope (change regions, hard budget) → recognize (counted
entry/operation coverage) → resolve (every call in one completeness bucket) → trace
(entry → hops → operation → witnesses → dominance → judgment → digests) → verdict (proven
claim | bounded silence). Models are bounded to residuals: zero-shot judge on the trace,
hunter proposes trust boundaries, proof arm demonstrates (only BLOCK evidence). Admission
on an untouched population gates the trace itself.
## Scope
- **In**: trace schema and witness algebra; three-way judgment; budget/scoping contract;
coverage accounting (funnel, resolution buckets, named incompleteness reasons); digest
and provenance fields; admission-gate contract (Req 12 workflow generalized); the
anti-shortcut rule for any fitted component (reversed-pair probes, per-repo holdout,
untouched-population admission); re-parenting map of existing specs onto the core.
- **Out**: new vulnerability families; per-framework fact content (that is fact-pack work,
e.g. recognition-coverage table task 7.1 in valid-detection-benchmark); the pre-push
transaction itself (pre-push-safety-net consumes the core); engine internals.
## Boundary Candidates
- Core trace object and verdict contract (this spec).
- Fact packs: per-family recognition/operation/guard facts, admission-gated (extension
point, starting with access_control from access-control-prover).
- Consumers: pre-push-safety-net (push transaction), search-with-proof (BLOCK evidence),
hunter-learning-loop (proposal lane only).
## Out of Boundary
- The pre-push hook transaction and rollout policy.
- New detection families beyond what the trace contract enables.
- Fitted models of any kind in the core path.
- Engine/CPG internals (contributor-scan owns those).
## Upstream / Downstream
- **Upstream**: `valid-detection-benchmark` (measurement instrument, dev + admission
pools), `access-control-prover` (trace implemented for access_control), steering
`detection-quality.md` (standing rules and stop conditions).
- **Downstream**: `pre-push-safety-net` (M-series milestones), `search-with-proof`,
`hunter-learning-loop`, future fact-pack specs per family/ecosystem.
## Existing Spec Touchpoints
- **Extends**: `access-control-prover` — its trace schema, judgment vocabulary and
admission workflow become the core contract, generalized across families.
- **Adjacent**: `pre-push-safety-net`, `search-with-proof`, `hunter-learning-loop`,
`valid-detection-benchmark`, `contributor-scan` — consumers/instruments; must not
duplicate the trace contract.
## Constraints
- Pool measures, never trains: no fitted prompt/threshold/model on pool labels
(detection-quality.md rules 4-5, 13-proposal).
- Fixed measurement schema: population + freeze digests, judgment vocabulary, rates with
n and Wilson 95%, coverage, instrument failures, runtime.
- Budget is measured, not static: per-change reply time and complexity are recorded, and
the budget is derived from the measured distribution per workload class (sample, percentile,
hardware disclosed); scoping to the change is part of the core contract.
- Instrument rule: prove nonzero input reads before interpreting any zero.
- Plain English in all documents; standard metrics only.
File diff suppressed because it is too large Load Diff
+392
View File
@@ -0,0 +1,392 @@
# Requirements Document
## Project Description (Input)
**Who has the problem:** the maintainer of OpenUltraSAST. The intended detection loop is
to augment family-specific model programs with deterministic program-analysis evidence.
Today that loop cannot be extended safely because the shared core embeds authorization
vocabulary, semantic gaps surface only during repository admission, and a clean-looking
zero can still hide a failure before the model receives useful evidence.
**Current situation:** the instrument-verified 2026-10-31 authorization diagnosis showed
two different failures. Framework-mediated reachability stopped before protected
operations, and paths that survived still lacked the constraint semantics needed to
distinguish vulnerable and fixed code. These are useful limits of the deterministic
evidence producer, not a reason to require it to become a complete vulnerability prover.
The core must expose such limits early and give each vulnerability family a stable way to
interpret the evidence without translating its semantics into authorization terms.
**What should change:** the detection core shall provide a family-neutral evidence and
verdict envelope. Deterministic analysis shall contribute typed transitions, effects,
provenance, and explicit completeness. Each vulnerability family shall define its security
object and decision vocabulary and may use a bounded model program to answer residual
questions over that evidence. A staged readiness contract shall stop a family at the first
failing semantic layer, before population-level admission is attempted.
**Evaluation-admission V3 amendment, 2026-11-06:** the validated version-two admission
contract pools all claimed scopes and represents one arm. The approved replacement benchmark
requires a new contract that evaluates multiple component and combined arms under one population
spend while preserving an independently interpretable result for every exact vulnerability-family
and ecosystem scope. Existing version-two evidence remains historical and unchanged.
## Boundary Context
- **In scope**: the family-neutral evidence, outcome, trace, coverage, provenance, budget,
staged-readiness, admission, measurement, fact-pack, model-handoff, and consumer
contracts; family conformance requirements; failure-visible semantic-stage accounting;
and the versioned exact-scope, multi-arm evaluation-admission validation and replay contract.
- **Out of scope**: vulnerability-family detection algorithms and prompt content;
framework-specific fact content; model optimization; analysis of vendored dependency
source; the pre-push transaction and rollout policy; proof execution; enabling a new
vulnerability family for users; population sourcing, curation, reservation, licensing,
and lifecycle; durable object-store or compare-and-swap adapters and bucket administration;
detector execution or isolation; observation storage, access enforcement, and retry
orchestration; workflow heartbeat and status presentation; and operator notifications.
- **Adjacent expectations**: the existing deterministic analysis is an evidence producer,
not a required complete prover; family-specific DSPy programs may consume the evidence
packet under the model-handoff contract; proof-based blocking remains a separate
consumer. Benchmark-owned workflows supply the reviewed population, lifecycle, custody,
sealed-observation, access-control, and post-spend release evidence that the core validates;
the core does not create or operate those workflows. A parked arm remains parked unless a
separately reviewed strategy creates a new arm, baseline, and admission population.
## Requirements
### Requirement 1: Family-neutral evidence record
The detection core shall define one canonical evidence record for every examined security
question within an analyzed change.
1. When a security question is analyzed, the detection core shall produce a canonical
evidence record carrying the family identity, security-object identity, analyzed scope,
ordered semantic transitions, observations with provenance, completeness reasons,
family decision, neutral outcome, evidence basis, and input digests.
2. When a transition or observation is derived from product code or reviewed semantic
knowledge, the detection core shall record its semantic role, source location,
provenance, and completeness independently of every other transition or observation.
3. When a family supplies family-specific evidence fields, the detection core shall
preserve those fields under the declared family schema without requiring them to use
another family's vocabulary.
4. The detection core shall serialize every evidence record deterministically, so that
identical inputs produce byte-identical records.
### Requirement 2: Neutral outcome and evidence basis
The detection core shall separate a family-specific decision from the neutral outcome and
the basis supporting that outcome.
1. When a family reaches a decision, the detection core shall record exactly one neutral
outcome of violation, satisfied, or indeterminate and shall retain the family's own
decision code separately.
2. When an outcome is satisfied, the detection core shall report the exact bounded scope
over which satisfaction was established and shall not present it as repository-wide
safety.
3. When evidence is incomplete for the family decision, the detection core shall return
indeterminate with every named incompleteness reason.
4. If a required semantic relation is unavailable or ambiguous, the detection core shall
prevent that missing relation from creating a violation or satisfied outcome.
5. When an outcome is reported, the detection core shall identify whether its basis is
deterministic analysis, model corroboration, or execution-backed proof.
### Requirement 3: Vulnerability-family conformance contract
The detection core shall require every vulnerability family to declare the meaning and
limits of the evidence it consumes and the decisions it produces.
1. When a family is registered, the detection core shall require its family and schema
versions, claimed ecosystems, security-object definition, accepted semantic roles,
decision vocabulary, neutral-outcome mapping, completeness conditions, and named
indeterminate reasons.
2. When a family claims a violation or satisfied outcome, the detection core shall require
the family contract to identify the minimum evidence and completeness conditions for
that outcome.
3. When a family receives evidence outside its declared ecosystem or schema scope, the
detection core shall report the question as unsupported rather than applying a
different family's interpretation.
4. If a family decision code, evidence field, or semantic role is not declared by the
family contract, the detection core shall refuse the result.
5. The detection core shall demonstrate conformance with at least two reference families
that use materially different security objects before the family-neutral contract is
declared ready for downstream family specifications.
### Requirement 4: Typed semantic transitions and effects
The detection core shall preserve reviewed semantic knowledge needed to connect product
code across framework and dependency boundaries without analyzing vendored source.
1. When reviewed semantic knowledge supplies a registration, invocation, call, return,
value transformation, operation effect, or constraint effect, the detection core shall
record the typed relation and the supplying fact identity in the evidence record.
2. When a typed relation is complete only under stated conditions, the detection core shall
retain those conditions and shall mark the relation incomplete when they are not met.
3. If multiple reviewed relations conflict or match ambiguously, the detection core shall
report the ambiguity and shall not choose a complete relation silently.
4. When vendored source is excluded, the detection core shall require a reviewed semantic
relation for any dependency behavior needed by a determinate family decision.
### Requirement 5: Coverage and instrument accounting
The detection core shall account for every input and semantic stage it did or did not use,
so a zero result is never indistinguishable from unread or unmodeled input.
1. When a change is analyzed, the detection core shall report counts for inputs read,
security questions formed, semantic sites recognized, transitions materialized,
security objects constructed, constraints evaluated, determinate outcomes, and findings
emitted.
2. When product-code or fact-derived relations are considered, the detection core shall
assign each relation exactly one complete, incomplete, or unresolved outcome.
3. If a semantic site, transition, security object, or constraint cannot be modeled, the
detection core shall report the specific missing relation rather than interpreting its
absence as safe.
4. If any tracked input file is read as zero bytes or cannot be opened, the detection core
shall fail the run as an instrument failure rather than reporting a clean result.
5. When a stage produces zero outputs, the detection core shall identify the last stage
with verified nonzero input and the first stage that failed to produce its expected
output.
### Requirement 6: Measured budget and change scoping
The detection core shall derive its time budget from measurement, not from a fixed constant.
1. When a change is analyzed, the detection core shall measure and record reply time and
change complexity, including changed regions, transition count, and changed size.
2. When a workload-class budget is published, the detection core shall disclose the sample
count, percentile, reference workload, reference hardware, and derived budget.
3. When a change exceeds its measured workload-class budget, the detection core shall fail
the budget gate naming the change, elapsed time, and budget derivation exceeded.
4. While a push is analyzed, the detection core shall limit analysis to regions affected
by the change and shall record the scope it used.
### Requirement 7: Verdict contract
The detection core shall distinguish supported defect claims, bounded satisfaction, and
bounded silence.
1. When the detection core reports a defect, the claim shall carry its evidence basis, an
exact location tied to the change, and the complete evidence record supporting the
claim.
2. When the detection core reports satisfaction, the claim shall carry the completed
security questions and the bounded scope over which they were decided.
3. When the detection core cannot decide a question, it shall report that question as
indeterminate with its coverage reasons and shall never present it as clean.
4. If a claim depends on an incomplete or unresolved semantic relation, the detection core
shall expose the affected question as bounded silence unless another complete path
independently supports the claim.
### Requirement 8: Staged readiness and promotion
The detection core shall require a vulnerability-family arm to pass each semantic readiness
stage before it can advance to broader evaluation.
1. When an arm begins development, the detection core shall require separate readiness
results for instrument validity, semantic recognition, transition construction,
security-object and constraint evaluation, vulnerable/fixed pair behavior, development
population behavior, fresh admission, and consumer integration.
2. When a readiness stage is evaluated, the detection core shall require a positive case,
a negative case, and an ambiguity or missing-evidence case appropriate to that stage.
3. If a readiness stage fails, the detection core shall stop promotion at that stage and
shall report the failing semantic layer and named correction lever.
4. If any prerequisite readiness result is missing, stale, or failed, the detection core
shall refuse to run a later population or integration gate as evidence of readiness.
5. When a semantic change invalidates an earlier readiness result, the detection core shall
mark every dependent later result stale.
6. When a family passes a stage, the detection core shall preserve the fixtures and results
as regression evidence without treating them as fresh admission evidence.
### Requirement 9: Model-augmentation contract
The detection core shall support bounded family-specific model reasoning over deterministic
evidence without allowing model output to rewrite that evidence.
1. Where a family uses a model program, the detection core shall present the canonical
evidence record, its completeness state, the family contract, and the residual question
to be answered.
2. When a model program returns a family decision, the detection core shall preserve the
model response, program and configuration identities, evidence-record digest, and
declared decision mapping.
3. If deterministic evidence is incomplete, the detection core shall prevent a model
response from converting the affected question into a satisfied outcome or a
proof-backed violation.
4. When model output supports an advisory claim, the detection core shall label the claim
as model-corroborated and shall keep it distinct from deterministic and execution-backed
claims.
5. The detection core shall not allow a model to propose and independently adjudicate the
same claim within one evidence path.
6. The detection core shall reserve blocking verdicts for execution-backed proof with a
differential witness.
### Requirement 10: Provenance and digests
The detection core shall stamp every evidence record and verdict with the identities of its
inputs.
1. When an analysis is recorded, the detection core shall include the analyzer revision,
family-contract digest, fact-set digest, acquisition-record digest, configuration
digest, population digest, and per-change input digest.
2. When an evidence record uses reviewed semantic knowledge, the detection core shall
include the semantic pack and fact identities needed to reproduce each derived relation
or effect.
3. When a model program contributes a decision, the detection core shall include the model,
program, prompt, and configuration identities needed to reproduce the call.
4. If any recorded digest or semantic fact identity cannot be re-derived from the named
inputs, the detection core shall refuse the result rather than accept it.
### Requirement 11: Reviewed semantic fact packs as the extension point
The detection core shall admit family and ecosystem knowledge only through reviewed,
versioned semantic fact packs over the family-neutral contract.
1. When a semantic fact pack is admitted, the detection core shall validate its schema
version, claimed family and ecosystem scope, stable fact identities, declared semantic
roles, and review evidence.
2. When a semantic fact matches analyzed product code, the detection core shall permit the
consuming family to record the corresponding transition or effect only with the matching
evidence and fact provenance attached.
3. The detection core shall not allow a name-only match without the fact's required
structural evidence to create a complete semantic relation or determinate outcome.
4. If a semantic fact has an undeclared role, unknown field, invalid scope, missing review
evidence, or inconsistent digest, the detection core shall refuse the pack.
5. When admitted knowledge is insufficient to resolve a semantic relation, the detection
core shall preserve the partial evidence record and report the relation as incomplete or
unresolved.
6. The detection core shall report, per arm, the vulnerability families and ecosystems the
arm claims, so admission and rollout numbers are never read across unsupported scopes.
### Requirement 12: Admission gate contract
The detection core shall gate every generalization claim on completed readiness stages and
a reviewed untouched population whose denominator matches the arm's declared scope.
1. When an arm is proposed for consumer use, the detection core shall require a reviewed
manifest naming the frozen analyzer, family contract, model program where applicable,
facts, acquisition, configuration, claimed families, claimed ecosystems, denominator
policy, readiness results, and untouched admission population before any admission
label is opened.
2. The detection core shall consume admission manifests without producing, mutating, or
re-labeling them and shall refuse manifests that are unreviewed, label-exposing,
acquisition-changed, or spent.
3. When an admission manifest is validated before label opening, the detection core shall
verify that every pre-registered success threshold is arithmetically reachable under the
declared family, ecosystem, and denominator policy.
4. If a threshold cannot be reached even when every in-scope case succeeds, the detection
core shall refuse the admission configuration rather than run or report a misleading
no-go result.
5. When a population contains cases outside an arm's declared scope, the detection core
shall score quality gates on the pre-registered in-scope denominator and shall report
whole-population coverage separately.
6. When admission evidence is scored, the detection core shall report paired
vulnerable/fixed behavior, ordinary false alarms, semantic-stage coverage, model-call
outcomes where applicable, and every budget, instrument, and quality gate as an
evidence-backed go or no-go result.
### Requirement 13: Separation of development and evaluation
The detection core shall prevent evaluation labels from influencing an arm before its
generalization claim is scored.
1. The detection core shall prohibit prompts, examples, thresholds, fitted models, semantic
facts, or decision mappings whose preparation data includes admission labels.
2. When a fitted component is proposed, the detection core shall require reversed-pair
probes, repository-held-out development evaluation, and admission on an untouched
population before any generalization claim is made.
3. When semantic knowledge is acquired, the detection core shall require source,
documentation, or independently reviewed behavioral evidence that is isolated from
vulnerability, fix-location, and ordinary-change labels.
4. If evaluation labels influence semantic fact content, matching behavior, prompt
content, examples, thresholds, or promotion, the detection core shall mark the affected
population spent and shall refuse it for admission.
### Requirement 14: Consumer and re-parenting contract
The detection core shall be the single contract that existing and future detection specs
reuse.
1. The detection core shall define the interface through which the pre-push transaction
consumes evidence records and verdicts within its deadline.
2. The detection core shall define the interface through which proof-based blocking
evidence attaches to an existing claim.
3. When an existing arm's behavior is described, the detection core shall require that
description to be expressed over the family-neutral contract rather than as a separate
runner contract.
4. When a consumer receives bounded silence caused by missing semantic knowledge, the
detection core shall require the consumer to preserve the named coverage reasons.
5. When a family specification is created, the detection core shall require it to identify
its family contract, readiness fixtures, development population, and admission boundary.
### Requirement 15: Fixed measurement schema
The detection core shall require one fixed measurement schema for every detection number.
1. When any detection result is reported, the detection core shall require population and
freeze digests, claimed family and ecosystem scope, denominator policy, family decision
vocabulary, neutral outcomes, evidence bases, every rate with its sample count and
uncertainty interval, coverage, instrument failures, model-call failures, and runtime.
2. When semantic relations participate in an arm, the detection core shall require the
report to include sites considered, sites recognized, transitions completed, security
objects constructed, constraints evaluated, determinate outcomes, and unresolved counts
by named reason.
3. When a measurement changes population, denominator policy, claimed scope, family
contract, fact schema, model program, or outcome mapping, the detection core shall treat
the result as a new baseline and shall not present it as a trend against the previous
baseline.
4. When a zero or clean-looking result is reported, the detection core shall require proof
that every tracked input was read and that every prerequisite semantic stage executed.
5. When a later-stage metric is reported, the detection core shall include the results and
digests of every prerequisite readiness stage.
## Evaluation-Admission V3 Amendment
Requirements 16–20 extend Requirements 12 and 15 for a new evaluation-admission contract. Requirements 1–15 and completed version-two evidence retain their existing meaning, identities, and validation behavior; the amendment does not reinterpret or upgrade them.
### Requirement 16: Versioned evaluation-admission compatibility
The detection core shall provide a new evaluation-admission contract for exact-scope, multi-arm qualification without changing the meaning or validation of historical admission artifacts.
#### Acceptance Criteria
1. When a new evaluation admission is requested, the detection core shall validate it under a distinct contract version and shall keep every earlier admission contract independently replayable.
2. The detection core shall preserve the accepted bytes, digests, decisions, and replay behavior of existing version-two admission artifacts.
3. If pooled version-two evidence is presented as exact-scope version-three evidence, the detection core shall refuse the conversion and shall require a new baseline evaluated under the new contract.
4. If an admission artifact names an unknown, mixed, or incompatible contract version, the detection core shall refuse it rather than infer a compatible shape.
5. When a population, evaluation plan, arm identity, claimed scope, denominator policy, threshold set, judgment vocabulary, analyzer identity, fact identity, configuration identity, registered budget, execution identity, outcome mapping, or combination rule changes, the detection core shall require a new baseline identity.
6. The detection core shall prevent an insufficient result, impaired result, aggregate result, incomplete arm result, raw report, or other partial artifact from authorizing consumer use.
### Requirement 17: Reviewed multi-arm plan and atomic spend authority
The detection core shall bind every arm evaluated against one untouched population to a reviewed label-blind plan and one atomic admission spend.
#### Acceptance Criteria
1. When an evaluation plan is registered before label access, the detection core shall require a reviewed label-blind plan naming the population, input-set identity, every component arm, every combined arm, and every expected observation slot.
2. When an arm enters an evaluation plan, the detection core shall require its immutable baseline identity, exact claimed family-and-ecosystem scopes, denominator policy, thresholds, sufficiency rules, outcome mapping, and combination lineage where applicable.
3. When the label-blind plan is reviewed, the detection core shall evaluate threshold reachability and evidence sufficiency independently for every planned arm and exact claimed scope.
4. If any planned arm or claimed scope cannot meet its registered minimum-size, threshold-reachability, or interval-precision rule, the detection core shall report that arm and scope as insufficient and shall refuse the shared population spend.
5. When every planned observation is complete and before label access, the detection core shall require an admission authority reviewed independently from its producer that binds the feasible plan to the exact completed observation identities and the current population custody state.
6. If an expected observation is absent, unreadable, changed, associated with another arm, input, or plan, or inconsistent with the feasible plan, the detection core shall refuse admission authorization.
7. When shared labels are released, the detection core shall require one admission preparation, one exact monotonic population transition, and one resulting label-release receipt binding every planned arm.
8. If the custody head is stale, the transition is not the exact available-to-spent successor, the receipt is absent or mismatched, or availability is later restored, the detection core shall refuse label release and scoring.
9. The detection core shall not authorize a partial spend for a subset of planned arms or scopes.
10. While the population remains unspent, the detection core shall permit admission reuse only when raw observations and population-specific diagnostics remain unavailable to detector development and only closed process status, counts, and digests are exposed.
11. If raw observations or population-specific diagnostics are inspected, or an arm, plan, configuration, registered budget, or execution identity changes after an attempt, the detection core shall require a development or spent disposition and shall refuse that population for admission.
12. When a pre-spend attempt is retried, the detection core shall accept the retry only under the exact unchanged population, plan, arms, inputs, configuration, registered budgets, and execution identities.
### Requirement 18: Exact-scope denominators and whole-population coverage
The detection core shall derive qualification independently for each arm's exact vulnerability-family and ecosystem scope while preserving separate whole-population coverage.
#### Acceptance Criteria
1. When quality is evaluated, the detection core shall derive one complete standard metric set for every exact arm, vulnerability-family, and ecosystem scope declared in the reviewed plan.
2. When recall or pair correctness is derived for a scope, the detection core shall use only vulnerable and fixed cases independently verified for that exact family and ecosystem.
3. When ordinary false alarms are derived for an arm, the detection core shall count each eligible ordinary push once for every family claimed by that arm in the push's reviewed ecosystem and shall not project it into another ecosystem.
4. When precision is derived for a scope, the detection core shall use only findings and cases assigned to that exact arm, family, and ecosystem under the reviewed denominator policy.
5. When one underlying push contributes to multiple verified scoped denominators, the detection core shall count it once in whole-population coverage.
6. When a scoped metric is reported, the detection core shall include its numerator, denominator, value, sample count, and Wilson 95% interval under the fixed metric algebra.
7. Where an overall aggregate is reported, the detection core shall identify its constituent arms and scopes, sample counts, and weighting and shall not attach a gate or authorization decision to the aggregate.
8. If a post-spend scoped denominator is zero or differs from the reviewed label-blind census and feasible plan, the detection core shall refuse the evaluation as invalid rather than report insufficient, go, or no-go.
### Requirement 19: Version-three case release, arm results, and proof binding
The detection core shall validate post-spend evidence separately for every planned arm while preserving shared population custody and combination lineage.
#### Acceptance Criteria
1. When post-spend cases are released, the detection core shall require one version-three case-release graph reviewed independently from its producer and bound to the reviewed evaluation plan, shared spend, and exact completed observations.
2. When a case is released for an arm, the detection core shall bind its arm identity, exact scope, role, pair identity, result, evidence, change inputs, reviewed input manifest, and release review.
3. If a version-two case release is supplied to a version-three evaluation, the detection core shall refuse it rather than reinterpret it under the new contract.
4. When a combined arm is evaluated, the detection core shall preserve every component arm's observations, metrics, gates, and decision and shall report the registered combination lineage.
5. If a combined result omits, replaces, or contradicts a required component result, the detection core shall refuse the combined evaluation.
6. While ground-truth roles remain sealed, the detection core shall treat every label-blind proof candidate as non-authoritative for demonstrated status.
7. When a proof-backed status is derived after case release, the detection core shall require an exact validated proof attachment matching the released family, scope, pair roles, change inputs, finding location, differential record, control binding, producer, verification receipt, and trust policy.
8. When a combined arm retains proof evidence, the detection core shall require the identical validated proof candidate from the matching component observation and shall prevent the combined arm from creating or transferring proof evidence.
9. If proof is absent or inconclusive, the detection core shall retain the finding as not demonstrated.
10. If a proof attachment is role-swapped, cross-family, cross-scope, location-mismatched, or bound to unrelated inputs or controls, the detection core shall refuse the attachment as invalid and shall not count it as demonstrated.
11. The detection core shall not allow one planned arm's valid result to hide another planned arm's missing, invalid, or failed result.
### Requirement 20: Complete evaluation reports and replay authority
The detection core shall make the complete multi-arm evaluation replay the only admission artifact capable of authorizing downstream consumption.
#### Acceptance Criteria
1. When a planned arm completes evaluation, the detection core shall report one go or no-go decision for every exact claimed scope from that arm's validated instrument, budget, readiness, and quality gates.
2. The detection core shall reserve go and no-go for completed quality evaluations and shall keep pre-spend insufficiency, unread input, skipped stages, incomplete arms, and other instrument failures distinct from quality decisions.
3. When an evaluation report is produced, the detection core shall preserve per-arm scoped metrics separately from whole-population coverage, completed and deferred work, coverage funnels, instrument and model failures, runtime, and every degradation reason.
4. When every planned arm has a valid report, the detection core shall produce one complete evaluation replay binding the reviewed plans, feasibility results, admission authority, shared custody transition and receipt, the version-three case-release graph, independently valid per-arm scoped reports, and combination lineage.
5. If any planned arm report, mandatory gate, custody binding, case release, or replay component is missing or invalid, the detection core shall refuse evaluation completion.
6. When an evaluation replay is validated, the detection core shall rederive identities, counts, metrics, gates, and decisions from the canonical source artifacts rather than trust caller-supplied summaries.
7. When a consumer requests admission evidence, the detection core shall provide authorization only from a complete validated evaluation replay and shall not authorize from a per-arm report or aggregate alone.
8. When a zero or clean-looking arm result is included, the detection core shall require nonzero input-read evidence and execution evidence for every required stage before accepting the replay.
## Out of Scope
- The pre-push hook transaction, rollout policy, and capability registry.
- Vulnerability-family algorithms, prompts, examples, and optimization programs.
- Framework- or repository-specific semantic fact content.
- Analysis or indexing of vendored dependency source.
- Proof execution and sandbox orchestration.
- Enabling a vulnerability family for advisory or blocking use.
- Analysis-engine, frontend, graph, and family-internal implementation details.
- Population sourcing, human curation, permanent reservation, licensing, and lifecycle operations.
- Durable object-store or compare-and-swap adapter implementation, bucket administration, and retention configuration.
- Detector execution and isolation, observation storage and access enforcement, and retry orchestration.
- Workflow heartbeat and operator-status presentation, notification transport, and product rollout.
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"feature_name": "detection-core",
"created_at": "2026-10-30T13:55:30Z",
"updated_at": "2026-11-06T12:21:47Z",
"language": "en",
"phase": "tasks-generated",
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": true
}
},
"ready_for_implementation": true,
"approval_note": "2026-11-06: Requirements and the merged V3 design were auto-approved by the maintainer's explicit -y request. Additive V3 task groups 12-18 passed the task-plan review and an independent graph sanity review; existing groups 1-11 remain historical V2 context, and blocked task 11.4 is not a V3 prerequisite. Tasks are approved for implementation."
}
+716
View File
@@ -0,0 +1,716 @@
# Implementation Plan
## Merge record
| Previous v1 item | Disposition in this plan |
|---|---|
| Completed groups 1–7 and 8.1 | Retained as regression substrate; their former requirement mappings do not count toward v2 coverage |
| Previous task 8.2 | Permanently parked after admission NO-GO #3; no task below revives that arm |
| Previous tasks 8.3–8.4 | Superseded by v2 consumer and proof conformance work; no rollout or live capability enablement is included |
| Existing task 11.4 | Retained as blocked historical V2 validation; it is not a prerequisite for additive V3 tasks 12–18, which have explicit dependencies and their own compatibility gate |
## Task constraints
- New and amended code keeps McCabe complexity at most 10, functions at most 50 logical lines, and new modules at most 400 logical lines.
- Core owns contracts and pure validation only; analyzers, model execution, proof execution, persistence, acquisition, and rollout remain with adjacent owners.
- Every zero-result test first proves nonzero readable input; an unread input is an instrument failure.
- Historical v1 artifacts remain byte-identical. V2 creates a new baseline and is never trended against v1.
- The parked authorization arm remains parked. Admission orchestration uses synthetic/reference inputs only and does not consume a live population.
- Existing target facts may be schema-migrated without expanding vocabulary or behavior.
- Tasks 12–18 are the current additive V3 implementation scope; completed groups 1–11 remain historical V2 implementation context.
- V2 aggregate roots and public routes remain frozen; V3 uses a distinct schema and adds no converter or inferred projection.
- Benchmark-owned storage, compare-and-swap, execution, access, retry, and workflow orchestration remain outside core and are exercised only through synthetic or in-memory boundaries.
- [ ] 1. Establish migration-safe foundations
- [x] 1.1 Freeze historical v1 artifacts and the reviewed migration inventory
- Pin representative v1 trace and report bytes and hashes as immutable regression fixtures.
- Provide an independent schema-routed read-only decoder that validates committed v1 artifacts without importing the compatibility namespace, constructing v2 determinate outcomes, or exposing a writer or converter.
- Freeze the reviewed initial set of v1 readers and writers with stable entry identities, paths, symbols or capabilities, owner boundaries, and open migration status.
- Preserve every inventory identity permanently and allow only monotone transitions from open to `v2_migrated` or `retired`; fail unreadable or zero-byte sources, additions, deletions, and rekeying.
- Preserve v1 as migration input only and never treat it as v2 conformance or a comparable trend baseline.
- Done when fixture tests detect any byte or hash change, the decoder validates both historical schemas read-only, every inventory entry resolves to a nonzero source and valid target, and schema, status, or addition mutations fail.
- _Requirements: 14.3, 15.3_
- [x] 1.2 Build the strict canonical codec and resolver-backed artifact boundary
- Produce deterministic UTF-8 JSON with full SHA-256 identities, stable mapping order, and retained sequence order.
- Refuse non-finite numbers, unsupported values, unknown schemas, duplicate logical identities, and malformed digests.
- Validate resolved bytes, length, storage receipt, retention identity, trusted store, and configured size limits.
- Provide deterministic in-memory resolver and receipt fixtures for later contract tests.
- Done when identical values produce byte-identical output and each content or receipt mismatch fails before parsing.
- _Requirements: 1.4, 10.4_
- [ ] 2. Implement canonical semantic, provenance, and evidence contracts
- [x] 2.1 (P) Represent family-neutral semantic relations and observations
- Cover registration, invocation, call, return, value transformation, operation effect, and constraint effect.
- Preserve role, location, conditions, provenance, and independent complete, incomplete, or unresolved status.
- Refuse conflicting complete candidates by producing explicit ambiguity rather than selecting one.
- Done when contract tests exercise every relation kind and all three relation statuses with canonical identities.
- _Requirements: 1.2, 4.1, 4.2, 4.3, 5.2_
- _Boundary: Semantic Evidence_
- _Depends: 1.2_
- [x] 2.2 (P) Validate complete input and decision provenance
- Stamp analyzer, family contract, fact set, acquisition, configuration, population, and change-input identities.
- Require the closed named-input role set and resolve every referenced input through the trusted artifact boundary.
- Keep model and proof identities outside the immutable deterministic packet.
- Done when every stamp field has a tamper test and unreadable or substituted named input is refused.
- _Requirements: 10.1, 10.4_
- _Boundary: Provenance and Lineage_
- _Depends: 1.2_
- [x] 2.3 Enforce append-only population custody
- Construct and validate contiguous reviewed-custodian receipt chains against the complete current log.
- Require compare-and-swap requests to name the expected log head and derive the next sequence and predecessor.
- Make spent status monotonic and reject old prefixes, omitted receipts, broken chains, and later availability claims.
- Provide deterministic in-memory custody-port and custodian fixtures for lineage and admission tests.
- Done when concurrent requests have one valid successor and no fixture can restore a spent population.
- _Requirements: 12.2, 13.1, 13.4_
- [x] 2.4 Bind lineage purposes to exact influenced artifacts
- Require each lineage use to name its purpose, influenced artifact, source population, label access, and current-chain receipt.
- Enforce required and optional use counts for prompts, examples, thresholds, fitted models, facts, matching behavior, mappings, and promotion.
- Distinguish fact ancestry that forbids all label access from development lineage that may use a different spent population.
- Refuse protected-population influence, redirected artifact identities, wrong purposes, and incomplete receipt history.
- Done when matching subjects cannot mask a wrong influenced artifact and contaminated populations are deterministically refused.
- _Requirements: 13.1, 13.3, 13.4_
- [x] 2.5 Bind immutable evidence packets to neutral decisions
- Preserve family identity, security object, analyzed scope, ordered semantics, observations, family fields, completeness, and provenance.
- Keep family decision codes separate from violation, satisfied, and indeterminate outcomes.
- Require bounded scope for satisfaction, named reasons for indeterminate outcomes, and a valid deterministic, model-corroborated, or execution-backed basis.
- Prevent incomplete or ambiguous required relations from producing violation or satisfaction.
- Done when positive, satisfied, and incomplete records round-trip canonically and every invalid packet/decision combination is refused.
- _Requirements: 1.1, 1.2, 1.3, 2.1, 2.2, 2.3, 2.4, 2.5, 10.1_
- [ ] 3. Add vulnerability-family contracts and unlike reference families
- [x] 3.1 Validate a closed vulnerability-family declaration
- Require family/schema versions, ecosystems, security-object fields, roles, decisions, outcome rules, completeness conditions, residual questions, and indeterminate reasons.
- Enforce typed family fields and exact ecosystem/schema scope.
- Declare whether each role accepts product evidence or requires reviewed excluded-dependency facts.
- Refuse undeclared fields, roles, decisions, reasons, or determinate outcomes without their minimum evidence.
- Done when the family registration matrix accepts only declared combinations and reports unsupported scope without cross-family fallback.
- _Requirements: 1.3, 2.1, 2.4, 3.1, 3.2, 3.3, 3.4, 4.4, 9.1_
- [x] 3.2 Build exact family catalog resolution and capability declarations
- Re-derive every contract and catalog digest and require canonical ordering and unique family/version/schema identities.
- Resolve a contract only when family, digest, and ecosystem identify exactly one entry.
- Validate that each family capability names its contract, readiness fixtures, development population, and admission boundary.
- Done when duplicate/substituted catalog entries and each missing or corrupted capability field are refused.
- _Requirements: 3.1, 3.3, 3.4, 14.5_
- [x] 3.3 Create obligation and taint-flow reference conformance fixtures
- Model an operation-plus-constraint family and an ordered source-to-sink family without target vocabulary.
- Provide positive, negative, and ambiguity or missing-evidence cases for both semantic shapes.
- Require each family to produce violation, bounded satisfaction, and indeterminate output through the same validators.
- Done when both fixture suites pass without adding a family-specific field to any shared core contract.
- _Requirements: 3.5, 8.2_
- [ ] 4. Expose operational coverage, budget, and readiness gates
- [x] 4.1 (P) Account for instrument reads and every semantic-stage outcome
- Count reads, questions, sites considered and recognized, transitions, security objects, constraints, determinate outcomes, and findings.
- Put every considered relation in exactly one complete, incomplete, or unresolved bucket.
- Fail unopened or zero-byte tracked input and diagnose the last verified nonzero stage and first missing expected output.
- Done when stage-by-stage fixtures distinguish a valid zero from unread input and identify every injected first failure.
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 15.2, 15.4_
- _Boundary: Coverage Funnel_
- _Depends: 2.1, 2.5_
- [x] 4.2 (P) Derive and enforce measured workload budgets and change scope
- Record changed regions, semantic-transition count, changed bytes, and total reply time.
- Derive workload-class budgets with sample count, percentile, reference workload, reference hardware, and canonical identity.
- Resolve the changed-byte measurement and refuse missing, zero, non-finite, or incompatible complexity data.
- Reject evidence and claim anchors outside the supplied affected scope and name the exceeded derivation on budget failure.
- Done when class derivation is deterministic and scope escape or elapsed-budget excess yields a named gate failure.
- _Requirements: 6.1, 6.2, 6.3, 6.4_
- _Boundary: Budget and Scope_
- _Depends: 1.2, 2.5_
- [x] 4.3 Record ordered readiness evidence with explicit evidence-use roles
- Cover instrument, recognition, transitions, constraints, pairs, development, admission, and consumer integration in fixed order.
- Bind each result to arm/family/ecosystem, analysis identity, population use, fixtures, predecessors, measurement, and correction lever.
- Keep regression, development, repository-holdout, fresh-admission, and integration evidence uses non-interchangeable.
- Done when each stage accepts the correct positive/negative/ambiguity triplet and rejects the wrong evidence-use role.
- _Requirements: 8.1, 8.2, 8.6_
- [x] 4.4 Stop promotion on failed, absent, or stale prerequisites
- Authorize a stage only when every ordered predecessor is current and passed.
- Derive transitive staleness from core schema, semantic vocabulary, family, facts, acquisition, configuration, model, and outcome mapping.
- Return the first failing layer and named correction lever without mutating historical results.
- Done when one changed identity makes all dependent later results stale and later population/integration gates refuse to run.
- _Requirements: 8.3, 8.4, 8.5, 15.5_
- [ ] 5. Replace semantic extension points with reviewed fact packs
- [x] 5.1 Validate versioned packs and independent review evidence
- Bind schema, family, ecosystem, language, stable facts, declared roles, normalized content, and review evidence.
- Accept source, official-documentation, or independently reviewed behavioral evidence under a trusted review policy.
- Resolve conflict disclosures and require producer/reviewer independence where behavioral evidence is used.
- Reject label-derived semantic-fact ancestry, unknown scope/roles, missing review, and inconsistent digests.
- Done when the schema/scope/review matrix accepts only fully reviewed label-isolated packs.
- _Requirements: 11.1, 11.4, 13.3_
- [x] 5.2 Validate complete structural observations and matcher lineage
- Match the declared requirement set, including qualified name, shape, receiver, target, argument, and configuration evidence as applicable.
- Resolve every observation artifact and require its identifier set to equal the fact’s complete structural requirements.
- Bind matcher revision, behavior digest, and current lineage to the exact evaluated product location.
- Prevent name-only, partial, fabricated, ambiguous, or protected-population-contaminated matches from becoming complete.
- Done when only a fully observed structural match with valid matcher lineage is eligible for effect application.
- _Requirements: 10.2, 11.2, 11.3, 13.4_
- [x] 5.3 Apply declared fact effects with reproducible provenance
- Materialize only relation kinds, roles, source/target roles, conditions, and effects declared by the matched fact and family.
- Attach exact pack, fact, product location, structural observation, matcher, and match-reference identities to every derived relation.
- Preserve unmet conditions as incomplete evidence and conflicting eligible matches as unresolved ambiguity.
- Mark label-influenced matching behavior or promotion as spent before the relation can participate in evaluation.
- Done when valid matches yield canonical effects while partial and ambiguous cases remain non-determinate with named reasons.
- _Requirements: 4.1, 4.2, 4.3, 10.2, 11.2, 11.3, 11.5, 13.4_
- [x] 5.4 Revalidate fact-derived relations and enforce the dependency boundary
- Re-open referenced packs, facts, match evidence, review policy, lineage, custody, and resolved bytes for every fact-derived relation.
- Reject undeclared roles, invalid scope, inconsistent content, and fact provenance that cannot be reproduced.
- Require reviewed semantic facts for determinate behavior attributed to excluded dependencies.
- Preserve missing dependency knowledge as named incomplete evidence rather than product-derived safety.
- Done when product-only dependency behavior is refused, a missing fact stays indeterminate, and the reviewed structurally matched equivalent is accepted.
- _Requirements: 2.4, 4.4, 10.4, 11.4, 11.5_
- [ ] 6. Add bounded model handoff and semantic shortcut probes
- [x] 6.1 (P) Preserve reproducible model requests, results, and responses
- Present the canonical evidence, completeness snapshot, family declaration, and declared residual question without importing a model runtime.
- Freeze provider, model, program, prompt, examples, fitted component, configuration, adapter, decision mapping, and input-bundle identities before execution.
- Preserve normalized responses plus one bounded redacted inline value or trusted immutable artifact, including failures.
- Done when matching success/failure fixtures round-trip and substituted request, packet, response, program, or mapping identities are refused.
- _Requirements: 9.1, 9.2, 10.3_
- _Boundary: Model Handoff_
- _Depends: 2.4, 3.1_
- [x] 6.2 Apply model results without rewriting deterministic evidence
- Validate current prompt/example/fitted-model/mapping lineage against the evaluated population and declared artifacts.
- Reject proposer/adjudicator role collision and results for another packet, family, residual, or mapping.
- Allow model-corroborated output only when deterministic residual prerequisites are complete.
- Prevent model output from creating semantic relations, satisfaction over missing evidence, or execution-backed basis.
- Done when accepted derivation leaves the evidence packet byte-identical and every incomplete or contaminated path remains indeterminate.
- _Requirements: 2.4, 2.5, 9.3, 9.4, 9.5, 13.1_
- [x] 6.3 (P) Evaluate independently reviewed equal-semantics reversed-surface probes
- Derive semantic projections from records and family contracts rather than trusting caller summaries.
- Resolve distinct surface artifacts and independently reviewed semantic-equivalence evidence.
- Reject unequal semantics, digest-only review, non-finite tolerances, and fabricated passed summaries.
- Detect a surface-shape component while accepting a relation-based component within its pre-registered tolerance.
- Done when probe results are reproducible solely from canonical probe inputs and review policy.
- _Requirements: 13.2_
- _Boundary: Probes_
- _Depends: 3.3, 5.1_
- [ ] 7. Compose validated evidence into bounded decisions and consumer output
- [x] 7.1 Revalidate deterministic and fact-derived records through one shared context
- Bind the exact catalog, packs, model declarations, lineage set, review policies, provenance trust, and custody policy.
- Resolve the family contract and all seven named provenance inputs for each raw record.
- Re-run family and fact validation and reject embedded pack, lineage, or named-input substitutes.
- Done when raw, forged, and context-mismatched records cannot reach admission or consumer decision code.
- _Requirements: 4.4, 10.1, 10.2, 10.4, 12.6, 14.1_
- [x] 7.2 Replay model derivation at the shared validation seam
- Require deterministic records to omit model derivation and model-corroborated records to include both request and result.
- Re-run model application from the pre-model record and compare the derived record canonically.
- Refuse constructible “validated” markers or model programs outside the context.
- Done when admission and consumer tests exercise the same validator and neither can bypass it with a raw record.
- _Requirements: 9.2, 9.3, 9.4, 9.5, 10.3, 10.4, 12.6, 14.1_
- [x] 7.3 (P) Derive defect claims, bounded satisfaction, and bounded silence
- Require exact change-tied locations and complete evidence for defect claims.
- Carry completed question identities and exact scope for satisfaction.
- Preserve named coverage reasons and record identities for every undecided question.
- Keep incomplete paths silent unless a separate complete path independently supports a claim.
- Done when mixed complete/incomplete fixtures produce supported claims plus bounded silence without any unbounded clean result.
- _Requirements: 2.2, 2.3, 7.1, 7.2, 7.3, 7.4, 14.4_
- _Boundary: Verdict and Proof_
- _Depends: 2.5, 3.1_
- [x] 7.4 Validate differential proof attachment without enabling rollout
- Re-derive reviewed producer, sandbox, oracle, affected/safe side, control, witness, and verification identities.
- Resolve all proof artifacts under the supplied producer, reviewer, store, and byte-limit trust policy.
- Upgrade basis only for demonstrated affected-only behavior tied to the same family, packet, location, and change/control.
- Reject opaque, self-asserted, inconclusive, same-side, or mismatched proof records.
- Done when only the valid differential fixture yields execution-backed basis and no attachment changes rollout policy.
- _Requirements: 2.5, 9.6, 14.2_
- [x] 7.5 Integrate validated records, proof, scope, and budget at the core consumer edge
- Accept raw validation envelopes and call the shared validator before creating any output.
- Preserve every bounded-silence reason and reject records or anchors outside the supplied change scope.
- Apply optional proof attachments through the proof validator and include all processing in total elapsed time.
- Return defects, satisfactions, silence, input identities, complexity, and measured budget identity.
- Done when a synthetic push returns a bounded verdict and scope, trust, or deadline violations fail explicitly.
- _Requirements: 6.4, 14.1, 14.4_
- _Depends: 4.2, 7.2, 7.4_
- [ ] 8. Implement fixed measurement and leakage-safe admission
- [x] 8.1 (P) Define comparable baseline identities and fixed metric algebra
- Bind arm, population/freeze, claimed scopes, exact contract/fact/model sets, acquisition, analyzer, configuration, denominator, threshold set, and outcome mapping.
- Define closed numerator/denominator rules for pair correctness, recall, fixed and ordinary false alarms, precision, semantic coverage, and model success.
- Require Wilson 95 intervals with numerator and denominator; serialize zero denominators as null and prevent them from passing.
- Treat any identity mutation as a new baseline and refuse cross-arm or cross-scope aggregation.
- Done when a baseline mutation matrix and two-arm isolation fixture distinguish every non-comparable result.
- _Requirements: 11.6, 15.1, 15.3_
- _Boundary: Measurement_
- _Depends: 4.1, 4.4_
- [x] 8.2 Build complete measurement records and typed repository-holdout reports
- Record family decision vocabulary, neutral outcomes, evidence bases, fixed metrics, semantic funnel, model outcomes/failures, instrument failures, runtime, and readiness ancestry.
- Require every later-stage record to carry current prerequisite readiness stage results and digests.
- Define a typed repository-holdout report with an explicit evidence-use role, training and holdout manifest references, repository identities, fitting lineage, and distinct measurement/report identities.
- Refuse zero or clean-looking results unless tracked input reads and prerequisite semantic stages passed.
- Done when missing metrics, counts, failure fields, readiness ancestry, or holdout-report identity fields invalidate the corresponding record.
- _Requirements: 13.2, 15.1, 15.2, 15.4, 15.5_
- [x] 8.3 Validate independently reviewed census and manifest payloads
- Canonically bind every substantive census and manifest field into separate review-subject digests.
- Require producer/reviewer separation, admitted reviewer identity, allowed identity pairing, and resolver-validated conflict disclosure.
- Reject self-review, copied attestations, unknown reviewers, malformed review digests, and unreadable disclosures.
- Preserve aggregate census cardinalities without exposing case identities or per-case labels.
- Done when only independently attested label-blind payloads can proceed to manifest validation.
- _Requirements: 12.1, 12.2_
- [x] 8.4 Validate exact admission context, readiness, scope, sets, and threshold lineage
- Bind the exact evidence-validation context, reviewed census, denominator, thresholds, readiness results, untouched population, and current custody head.
- Re-derive claimed scopes and exact contract, fact, model, threshold, and outcome-mapping sets from canonical objects.
- Require current instrument-through-development readiness with matching analysis identity and evidence-use roles.
- Validate prompt, example, fitted-model, fact, matcher, mapping, promotion, and threshold lineage before labels open.
- Reject label exposure, acquisition drift, context substitution, stale readiness, scope mismatch, or prior spend.
- Done when each refusal branch fails before feasibility and no digest-only set or lineage claim is accepted.
- _Requirements: 12.1, 12.2, 13.1_
- [x] 8.5 Refuse arithmetically impossible threshold configurations
- Enumerate reachable counts from label-blind pair, case, semantic-site, and model-call capacities.
- Evaluate count, rate, per-100, and Wilson-bound thresholds with the fixed comparator direction for each metric.
- Require every grouped threshold to have at least one jointly attainable count assignment.
- Done when the historical impossible-denominator case is a configuration refusal rather than a measured no-go.
- _Requirements: 12.3, 12.4_
- [x] 8.6 Validate the pre-label population-spend protocol inside core
- Construct a label-release request bound to the exact validated authorization and current custody-log head.
- Validate the returned receipt as the exact current-chain direct successor that changes the named population from available to spent.
- Reject missing, stale, mismatched, non-successor, or availability-restoring receipts before scoring authorization.
- Keep durable append, compare-and-swap execution, label release, persistence, crash handling, and concurrency outside core.
- Done when only the exact spent successor authorizes scoring and every other receipt shape is refused without an external side effect.
- _Requirements: 12.2, 13.4_
- [x] 8.7 Validate the authenticated post-spend case release
- Define canonical released-case bindings over case identity, scope, role, scoped pair identity, exact result digest, evidence-input digests, change-input digests, and input-manifest digest.
- Bind the release to the population, manifest, authorization, preparation, exact current-chain spend receipt, custodian identity, and an independent review attestation.
- Validate the release only after the exact spend successor and require one canonical binding for every supplied case and no others.
- Reject missing, extra, duplicate, cross-scope, role-swapped, pair-swapped, result-swapped, or rehashed operational-payload substitutions before metric production.
- Keep case identities and labels out of the pre-label census and make the external custodian, not the runner, authoritative for their post-spend association.
- Done when the reproduced vulnerable/fixed payload swap and every release-field mutation fail before any admission metric is returned.
- _Requirements: 12.2, 12.5, 12.6, 13.4, 15.1_
- _Boundary: Admission Case Release_
- [x] 8.8 Derive scoped admission metrics from canonical case evidence
- Re-run shared evidence validation, instrument, budget, verdict, and proof validation for every case.
- Consume only cases that exactly match the authenticated post-spend release; use its roles and scoped pairs while reconciling aggregate counts to the reviewed census.
- Score quality only over pre-registered family/ecosystem scopes while retaining whole-population coverage.
- Compute paired behavior, ordinary false alarms, semantic coverage, model outcomes, and all standard metrics without trusting runner aggregates.
- Done when forged decisions, substituted contracts, invalid fact review, contaminated lineage, or case-count drift fail before metric production.
- _Requirements: 12.5, 12.6, 15.1, 15.2_
- _Depends: 8.7_
- [x] 8.9 Assemble evidence-backed gates and the final admission report
- Emit instrument, budget, readiness, and quality gates with reasons and supporting identities.
- Bind the report to the validation context, authorization, pre-label spend receipt, authenticated case release, census, and complete case-result set.
- Require every mandatory metric and gate before allowing GO; make model success mandatory when a model is baseline-configured or validated claimed-scope calls occur. Refuse undeclared claimed-scope calls before report production; make configured zero-call, missing-threshold, and failed declared-call cases NO-GO while ignoring out-of-scope-only calls.
- Return an immutable admission replay bundle carrying the exact manifest, denominator, readiness, preparation, spend receipt, case release, cases, and derived report; give the bundle a canonical transport digest without treating it as a validation token.
- Revalidate the replay against independently supplied current context, custody, and content resolution by re-running the complete derivation and requiring exact report and replay equality.
- Keep model-corroborated, deterministic, and execution-backed counts separate.
- Done when any missing/failed mandatory gate forces NO-GO, coordinated report/readiness/threshold/case substitutions are refused, and a complete passing synthetic fixture yields a reproducible replayed GO report.
- _Requirements: 12.6, 15.1, 15.2, 15.4, 15.5_
- _Depends: 8.8_
- [x] 8.10 Authorize fitted components only from replayed probes and disjoint evaluation
- Accept only the typed admission replay bundle from task 8.9, validate it first against current authoritative context/custody/content, and refuse raw admission reports or coordinated replay substitutions.
- Resolve the component’s unique model declaration, family contract, review policy, and custody policy from the admission-bound context.
- Re-evaluate every canonical probe input and require the derived result set to match the component evidence.
- Consume the typed repository-holdout report from task 8.2 and validate its evidence-use role, training/holdout references, report identity, and fitting lineage.
- Prove the report’s training and repository-held-out repository sets are disjoint.
- Require an admission GO that was untouched before its bound label-release spend.
- Done when fabricated summaries, caller-selected policy substitutes, overlapping repositories, contaminated lineage, or mismatched admission are refused.
- _Requirements: 13.1, 13.2, 13.4_
- _Depends: 8.2, 8.9_
- [ ] 9. Integrate adjacent producers and benchmark-owned orchestration
- [x] 9.1 (P) Translate the parked authorization trace conservatively into v2 evidence
- Move conversion out of the family trace domain and keep authorization vocabulary outside shared core modules.
- Map missing per-hop knowledge to incomplete relations and indeterminate outcomes without behavior lifting.
- Preserve existing parked-arm regression behavior and avoid any admission rerun or reactivation.
- Done when adapter conformance matches the frozen v1 behavior while the authorization domain no longer imports v2 core types directly.
- _Requirements: 3.5, 14.3_
- _Boundary: Authorization Adapter_
- _Depends: 3.3, 7.2_
- [x] 9.2 (P) Translate hunt-owned model execution through the bounded handoff
- Convert structured executor inputs and outputs to the canonical request/result contracts.
- Keep prompt creation, provider calls, caching, parsing, and redaction in the hunt subsystem.
- Reject mismatched declared results through core validation without importing DSPy into core.
- Done when a fake executor success/failure pair crosses the adapter and core’s import census contains no model-runtime dependency.
- _Requirements: 9.1, 9.2, 9.3, 9.4, 9.5, 14.3_
- _Boundary: Model Adapter_
- _Depends: 6.2, 7.2_
- [x] 9.3 (P) Translate demonstrated search results into trusted proof attachments
- Map existing typed affected/safe verification records without moving sandbox or oracle execution into core.
- Preserve producer, control, witness, storage, and verification identities required by proof validation.
- Refuse inconclusive and mismatched fixtures at the adapter/core seam.
- Done when a canonical demonstrated fixture attaches successfully and every negative proof shape remains at its prior evidence basis.
- _Requirements: 9.6, 14.2, 14.3_
- _Boundary: Proof Adapter_
- _Depends: 7.4_
- [x] 9.4 Accept downstream reviewed fact-adapter conformance evidence
- Consume the immutable conformance fixture and manifest produced by the downstream family adapter without constructing its contract, packs, matches, authority, or target data in core.
- Re-run only the family-neutral fact-pack, fact-match, relation-application, full-evidence, canonical-reference, and import-boundary checks named by that handoff.
- Verify the manifest accounts for complete, name-only, partial, substituted, self-authored, protected-population-contaminated, and legacy-projection-only cases.
- Done when the downstream handoff is accepted by generic validators and core remains free of target imports; adapter implementation and target-specific fixtures remain downstream-owned.
- _Requirements: 10.2, 10.4, 11.1, 11.2, 11.3, 11.4, 11.5, 13.4, 14.3_
- _Boundary: Fact Pack Adapter Conformance_
- _Depends: 5.4_
- [x] 9.5 (P) Produce readiness and development records through benchmark-owned adapters
- Construct canonical readiness results while leaving sequencing and persistence with benchmark workflows.
- Emit development observations with v2 identities, evidence-use roles, instrument receipts, and prerequisite ancestry.
- Stop a runner before population work when readiness is failed, absent, or stale.
- Done when development fixtures pass through v2 and an injected semantic identity change prevents the later run.
- _Requirements: 8.1, 8.3, 8.4, 8.5, 8.6, 14.3, 15.5_
- _Boundary: Benchmark Readiness Adapter_
- _Depends: 4.4, 8.2_
- [x] 9.6 Integrate benchmark-owned admission scoring and guarded custody
- Delegate neutral scoring to core while retaining population persistence and run orchestration in the benchmark.
- Make the custodian the sole append-only writer and combine durable compare-and-swap spend plus label release as one guarded operation.
- After the winning spend, produce and independently attest the canonical case release over the exact immutable result, evidence, change, and input identities supplied to scoring.
- Supply expanded manifest, lineage, resolver, and validation-context identities without exposing labels early.
- Exercise durable append, label release, case-release production, crash-before-append, crash-after-append, and concurrent-winner behavior at this external boundary.
- Done when synthetic admission completes end to end, injected crash/concurrency cases prove labels reach at most the successful compare-and-swap winner, and swapped operational payloads are refused by core.
- _Requirements: 12.1, 12.2, 12.3, 12.4, 12.5, 12.6, 13.4_
- _Depends: 8.9, 9.5_
- [ ] 10. Complete the single-writer v2 migration
- [x] 10.1 Isolate every live v1 contract and writer behind the closed compatibility namespace
- Move all authorization-shaped v1 values into one compatibility contract owner and all live v1 serialization into its sole writer facade.
- Limit the compatibility namespace to the canonical primitive needed for byte stability and prevent every v2 module from importing it.
- Preserve the frozen v1 wire representation while eliminating independent contract or serializer implementations elsewhere.
- Done when old writes remain byte-identical, the compatibility namespace is the only live v1 contract and write owner, and ownership/import tests refuse any duplicate implementation.
- _Requirements: 14.3, 15.3_
- _Depends: 1.1_
- [x] 10.2 Convert the temporary public and module compatibility surfaces into explicit shims
- Keep the old named top-level API and designated compatibility modules as explicit imports from the isolated namespace during migration.
- Remove legacy definitions and re-exports from mixed v2 modules so each compatibility name has exactly one owner.
- Keep the canonical v2 paths private until the final atomic cutover.
- Done when the old named API remains compatible before cutover, direct imports of moved legacy symbols from mixed modules fail, and no shim owns an independent implementation.
- _Requirements: 14.3, 15.3_
- [x] 10.3 Build the syntax-only migration-boundary instrument
- Prove every scoped source file is readable and nonzero before interpreting a zero result.
- Report explicit compatibility-namespace imports, designated shim imports, named top-level v1 symbols, writer-facade references, and module-valued or dynamic core access without evaluating alias or data flow.
- Validate immutable inventory identities and their monotone open, `v2_migrated`, or `retired` status transitions.
- During migration, distinguish the designated compatibility implementation from the production-consumer scope; reserve repository-wide zero for post-deletion validation.
- Done when an adversarial syntax matrix reports the exact forbidden sites and `try`/`finally`, comprehension, assignment, and generic-call alias shapes cannot turn an explicit forbidden reference into a false zero.
- _Requirements: 14.3, 15.3_
- [x] 10.4 Migrate or retire remaining registered production source-tree consumers
- Move each registered non-benchmark production reader and writer to the canonical v2 contract or retire it within its owning boundary.
- Update the matching stable inventory entry to `v2_migrated` or `retired` in the same change without removing or rekeying it.
- Keep authorization analysis, model execution, proof execution, and other adjacent behavior in their existing owner boundaries while changing only their contract seam.
- Done when every source-tree entry owned by core or an adjacent adapter is terminal and the verified nonzero production-consumer scope outside the designated compatibility implementation has zero forbidden references.
- _Requirements: 14.1, 14.3, 15.3_
- _Boundary: Core and Adjacent Adapter Migration_
- _Depends: 10.3_
- [x] 10.5 Migrate benchmark readiness and admission consumers
- Move every registered benchmark reader and writer to v2 while retaining readiness sequencing, persistence, population custody, and admission orchestration in the benchmark boundary.
- Update each matching stable inventory entry to `v2_migrated` or `retired` without changing its identity.
- Preserve guarded spend, authenticated case release, fixed measurement, and bounded model behavior through the migration.
- Done when benchmark production scope has zero forbidden consumer references outside the designated compatibility implementation, every benchmark entry is terminal, and synthetic v2 readiness and admission complete end to end.
- _Requirements: 8.1, 12.6, 14.3, 15.3_
- _Boundary: Benchmark Readiness and Admission Adapters_
- _Depends: 9.5, 9.6, 10.3_
- [x] 10.6 Migrate or retire registered test and operational-tool consumers
- Move authorization assumptions into legacy or adapter tests, keep family-neutral contract tests on v2, and retain frozen v1 bytes only for decoder regression.
- Move or retire registered operational-tool consumers without adding a second compatibility or write path.
- Mark the corresponding stable inventory entries `v2_migrated` or `retired` and require every original entry to reach a terminal status.
- Done when the complete original inventory is terminal and the boundary instrument proves a nonzero repository scope with no forbidden consumer reference outside the designated compatibility implementation.
- _Requirements: 14.3, 15.3_
- _Boundary: Migration Tests and Operational Tooling_
- _Depends: 10.4, 10.5_
- [x] 10.7 Delete v1 write capability and publish the single v2 surface atomically
- Require every inventory entry to be terminal and the migration-time production-consumer boundary to be zero before deletion begins.
- Delete the compatibility namespace, v1 writer facade, and temporary shims while publishing the canonical v2 modules and top-level exports in the same integration change.
- Prove old top-level and submodule imports and writes fail and that no v1 serializer or converter remains.
- Retain the independent read-only decoder for frozen historical bytes and refuse comparison between v1 and v2 baseline identities.
- After deletion, require repository-wide zero with no compatibility exclusion.
- Done when isolated-tree and repository tests show old imports and writes fail, v2 imports and writes succeed, historical bytes still validate, and the final nonzero boundary scan remains zero.
- _Requirements: 14.3, 15.3_
- _Boundary: Single-Writer Cutover Integration_
- _Depends: 10.6_
- [ ] 11. Validate readiness, failure visibility, and implementation boundaries
- [x] 11.1 Run both reference families through the complete synthetic readiness path
- Start from a verified nonzero input and run recognition, transitions, security object/constraints, pair, development, reviewed synthetic admission, guarded spend, and consumer verdict.
- Produce violation, bounded satisfaction, and bounded silence for both unlike families.
- Require current prerequisite readiness identities at every later stage.
- Done when both authored repositories complete the path without target-specific core fields or a real admission population.
- _Requirements: 3.5, 8.1, 8.2, 8.4, 8.6, 12.1, 14.1, 15.5_
- [x] 11.2 Inject one failure at every semantic and governance stage
- Exercise unread/zero input, missing recognition, incomplete transition, missing constraint, pair mismatch, failed development, contaminated admission, and refused integration.
- Verify each run stops at the first failing stage with a named correction lever and no later metric or clean result.
- Mutate a semantic identity and verify transitive staleness through all dependent readiness and measurement records.
- Done when every injected failure is diagnosed at its source and no late admission-only surprise remains.
- _Requirements: 5.3, 5.4, 5.5, 8.3, 8.4, 8.5, 15.4, 15.5_
- [x] 11.3 Verify bounded validation cost and total consumer deadline accounting
- Measure canonicalization and validation across increasing relation, observation, receipt, and case counts.
- Exercise large transition sets for linear behavior and bounded memory without weakening semantic checks.
- Include optional model and proof processing in the same measured consumer budget.
- Publish sample count, percentile, workload, hardware, and derived budget through the fixed measurement contract.
- Done when performance fixtures satisfy the derived workload envelope and any excess fails the named budget gate.
- _Requirements: 1.4, 6.1, 6.2, 6.3, 6.4_
- [ ] 11.4 Run the full migration, security, and regression gate
- Verify core imports only standard-library and lower core layers and contains no target, DSPy, benchmark, proof-execution, or analyzer dependencies.
- Run all unit, adapter, migration, reference-family, end-to-end, admission, and historical compatibility suites.
- Enforce function/module complexity caps and scan for remaining v1 imports or writers.
- Prove each test instrument read nonzero fixture bytes before interpreting zero findings or failures.
- Done when the complete suite passes, boundary/import censuses are clean, historical bytes are unchanged, and all declared adapter and consumer seams pass conformance.
- _Requirements: 1.4, 10.4, 14.3, 15.3, 15.4_
- _Depends: 10.7_
- _Blocked: Benchmark population governance conflict: the permanent unseen-pool reservation guard and the later spent/retired training-only pilot policy disagree. The benchmark owner must approve the authoritative lifecycle and replacement/retirement plan before the full suite can pass._
## Evaluation-admission V3 amendment
Tasks 12–18 implement Requirements 16–20 as an additive predecessor for the approved
valid-detection-benchmark design. Existing groups 1–11 remain the historical V2
implementation record. The unresolved downstream-governance item 11.4 is not reopened and
is not a prerequisite for this V3 stream; V3 tasks depend explicitly on completed V2
contract work and use synthetic/in-memory workflow boundaries only.
- [ ] 12. Preserve V2 admission and establish V3 measurement foundations
- [ ] 12.1 (P) Freeze representative V2 admission behavior
- Pin accepted canonical bytes, digests, decisions, validation failures, report output, and authoritative replay output for representative V2 admissions.
- Prove every frozen fixture is readable and nonzero before treating a stable result as compatibility evidence.
- Keep pooled V2 evidence replayable only under V2 and add no converter or inferred V3 projection.
- Done when a byte, digest, decision, failure-order, or replay mutation fails the compatibility suite while the current V2 fixtures pass unchanged.
- _Requirements: 16.1, 16.2, 16.3, 16.4_
- _Boundary: V2 Admission Compatibility_
- _Depends: 8.9, 8.10_
- [ ] 12.2 Extract version-neutral metric arithmetic without changing V2
- Isolate only the closed standard-metric algebra and Wilson calculation; keep every serialized V2 identity and validator in its existing owner.
- Route V2 through the shared arithmetic and compare all frozen V2 outputs byte-for-byte.
- Expose the same pure arithmetic to V3 without accepting a V2 policy, result, or report as a V3 value.
- Done when both versions use one arithmetic implementation and the complete V2 compatibility fixture remains unchanged.
- _Requirements: 16.2, 18.6_
- _Boundary: Version-Neutral Metric Algebra_
- _Depends: 12.1_
- [ ] 12.3 Define exact arm-scope measurement and sufficiency contracts
- Represent one canonical arm-and-scope key for every policy, threshold, sufficiency rule, metric, gate, and decision.
- Bind exact denominator algebra, fixed Wilson intervals, sample counts, minimum sizes, interval precision, and rational aggregate weights.
- Include every arm identity input that must start a new baseline when changed, including judgment vocabulary, analyzer, facts, configuration, registered budget, execution, outcome mapping, and combination rule.
- Refuse duplicate, unsorted, unknown, cross-key, non-finite, or incomplete values through exact closed validation.
- Done when canonical contract tests accept one complete component and combined-arm configuration and reject every single-field identity or arm-scope mismatch.
- _Requirements: 16.1, 16.4, 16.5, 18.1, 18.6, 18.7_
- _Boundary: Evaluation Measurement_
- _Depends: 12.2_
- [ ] 12.4 Establish distinct V3 version-boundary primitives
- Reserve the exact evaluation-admission V3 schema identity and version-routing primitives without changing any V2 public name or route.
- Make V3 contracts exact-type only and prohibit V2-to-V3 conversion, subclass acceptance, and mixed-version child relationships as each later root is introduced.
- Define the shared non-authorizing marker for feasibility, insufficiency, raw reports, aggregates, incomplete arm results, and other partial values; leave complete root decoding to facade integration.
- Done when boundary tests preserve exact V2 routing, recognize only the reserved V3 identity on the V3 path, and prove that a representative partial V3 value has no authorization capability.
- _Requirements: 16.1, 16.3, 16.4, 16.6_
- _Boundary: Evaluation Version Compatibility_
- _Depends: 12.1, 12.3_
- [ ] 13. Register reviewed multi-arm plans and exact preflight
- [ ] 13.1 Define component, combined-arm, and declarative combination configurations
- Represent component and combined arm configurations with exact scopes, policies, thresholds, sufficiency rules, outcome mapping, and a closed declarative combination rule.
- Require component identities and a finite normalized decision table for every combined arm, with identical-component-candidate-only proof retention.
- Refuse missing components, duplicate arms, cross-key policies, and arbitrary executable combination behavior.
- Done when canonical component and combined configurations validate and every component, rule-row, policy, threshold, sufficiency, or outcome-mapping mutation changes the configuration identity.
- _Requirements: 16.5, 17.1, 17.2, 19.4_
- _Boundary: Evaluation Arm Configuration_
- _Depends: 12.3_
- [ ] 13.2 Build the non-circular plan declaration, baselines, and expected slots
- Derive the complete label-blind plan declaration before constructing per-arm baselines so the plan and baselines cannot form a digest cycle.
- Bind each baseline to the population, freeze, input set, declaration, and exact arm configuration so any listed change creates a new baseline.
- Define expected observation slots for every planned arm and input without including ground-truth roles or labels.
- Done when plan reordering is canonical, every substantive mutation changes the declaration and affected baselines, and a combined arm cannot name an absent component.
- _Requirements: 16.5, 17.1, 17.2_
- _Boundary: Evaluation Plan_
- _Depends: 13.1_
- [ ] 13.3 Validate the reviewed label-blind census and plan manifest
- Represent exact vulnerable/fixed scope capacities, ecosystem-specific ordinary-push capacities, semantic/model capacities, and deduplicated whole-population size without case labels.
- Require an independently reviewed plan payload that binds the population, input set, arm configurations, baselines, readiness, custody head, census, and expected slots.
- Resolve review evidence under the configured trust and byte limits and refuse self-review, copied attestations, exposed labels, missing scopes, or inconsistent population identities.
- Done when one reviewed multi-arm manifest validates with labels sealed and every review, census, scope, ordinary-ecosystem, or slot substitution is refused before feasibility.
- _Requirements: 17.1, 17.2, 17.3, 18.1, 18.3_
- _Boundary: Evaluation Plan Review_
- [ ] 13.4 Derive feasibility independently for every planned arm and exact scope
- Produce exactly one feasibility row for the canonical product of planned arms and their claimed scopes.
- Recompute minimum sample size, threshold reachability, and Wilson interval precision from the reviewed label-blind census for each row.
- Derive shared spend eligibility only when every row is feasible; keep insufficiency and its reasons distinct from GO and NO-GO.
- Refuse missing, extra, duplicated, or caller-summarized rows and prevent preparation when one row is insufficient.
- Done when a single insufficient scope blocks the shared spend without opening labels, while the complete feasible plan yields one reproducible preflight digest.
- _Requirements: 17.3, 17.4, 20.2_
- _Boundary: Evaluation Feasibility_
- [ ] 14. Seal planned observations and authorize one atomic spend
- [ ] 14.1 Validate exact nonzero observations against every expected slot
- Represent label-blind observation headers that bind slot, arm, input, execution, registered budget, configuration, input-read evidence, stage-execution evidence, cases, and proof candidates.
- Resolve every sealed observation through trusted content references and prove its bytes are nonzero before accepting its header.
- Require one-to-one equality between expected slots and sealed observations; refuse missing, extra, changed, unreadable, cross-arm, cross-input, or duplicate values.
- Done when the complete slot set validates and each single-slot removal, swap, truncation, zero-byte value, or identity mutation is refused before authority.
- _Requirements: 17.5, 17.6, 20.8_
- _Boundary: Evaluation Observation_
- _Depends: 13.3_
- [ ] 14.2 Validate reviewed attempt history, sealed access, and exact retries
- Bind each attempt event to the complete retry identity, closed access policy, raw-observation and diagnostic access counts, public status projection, disposition, review, and predecessor.
- Resolve the reviewed predecessor chain from its declared head and require every unspent attempt to have the identical population, plan, arms, inputs, configurations, budgets, and execution identities.
- Permit public pre-spend projection only for closed status codes, counts, and digests; any raw/diagnostic access or identity change produces a terminal development-opened or spent disposition.
- Enforce terminal monotonicity so a later sealed event cannot restore admission eligibility.
- Done when an unchanged all-sealed retry chain validates and broken, forked, changed, exposed, self-reviewed, or terminal-then-sealed histories cannot authorize.
- _Requirements: 17.10, 17.11, 17.12_
- _Boundary: Evaluation Attempt History_
- _Depends: 13.3_
- [ ] 14.3 Validate the independently reviewed admission authority
- Bind the feasible plan to the exact sealed observation set, attempt-history head, current readiness, validation context, and current complete custody head.
- Require the admission-authority payload to be reviewed independently from its producer and resolve its review evidence before authorization.
- Revalidate feasibility, all observation bytes, attempt history, readiness, and custody immediately before producing authority.
- Refuse stale or restored availability, changed observations, missing review, and any difference from the reviewed plan rather than returning a partial authority.
- Done when one canonical authority is derived only from the complete reviewed graph and every stale, substituted, or incomplete source fails before spend.
- _Requirements: 17.5, 17.6, 17.8, 17.10, 17.11, 17.12_
- _Boundary: Evaluation Authority_
- _Depends: 13.4, 14.1, 14.2_
- [ ] 14.4 Bind every planned arm to one preparation and custody receipt
- Construct one head-bound label-release request whose influenced artifact transitively commits to every planned baseline and exact authority.
- Validate only the exact monotonic available-to-spent successor and derive an explicit all-arm binding between preparation, receipt, and planned baselines.
- Reject subset preparations, missing or mismatched receipts, stale concurrent attempts, broken custody chains, and any later restoration of availability.
- Keep durable compare-and-swap and label release outside core while exercising the boundary with an in-memory custody port.
- Done when one receipt authorizes all and only the planned arms, concurrent/stale losers fail, and no partial spend is constructible.
- _Requirements: 17.7, 17.8, 17.9_
- _Boundary: Evaluation Shared Spend_
- _Depends: 14.3_
- [ ] 15. Authenticate V3 case release, combinations, and proof continuity
- [ ] 15.1 Build the arm-aware V3 case and independently reviewed release graph
- Bind each post-spend case to its arm-scope key, baseline, observation, underlying push, role, pair, result, evidence inputs, change inputs, input manifest, and proof-candidate identity.
- Bind one independently reviewed release to the plan, admission authority, preparation, exact shared spend, and complete canonical case set.
- Require exact planned-arm and released-case cardinality and exact V3 root types; refuse V2 releases and subclasses.
- Require nonzero read and required-stage execution evidence in every released case before it can enter scoring.
- Done when the complete V3 release validates and V2 substitution, missing arms, duplicate cases, wrong spend, or copied review fail before metric production.
- _Requirements: 19.1, 19.2, 19.3, 19.11, 20.8_
- _Boundary: Evaluation Release_
- _Depends: 14.4_
- [ ] 15.2 Revalidate exact role, pair, scope, result, and input associations
- Recompute every case and binding digest from canonical evidence rather than accepting caller-supplied summaries.
- Require vulnerable/fixed roles and scoped pair identities to form exact pairs; prohibit pair identities on ordinary cases.
- Preserve the separately reviewed underlying-push identity for scoped projection and later whole-population deduplication.
- Refuse arm, scope, role, pair, observation, result, evidence, location, input, or manifest swaps before any count is derived.
- Done when a canonical multi-scope release passes and a mutation matrix proves every association mismatch is invalid rather than a failed quality gate.
- _Requirements: 18.2, 18.5, 19.2, 19.10_
- _Boundary: Released Case Validation_
- [ ] 15.3 Replay combined-arm rules and preserve component observation lineage
- Recompute combined observation outcomes from the registered finite decision table and exact component observation digests.
- Preserve each component arm's observation identity and outcome independently of the combined result; defer completed metric, gate, decision, and report preservation to replay assembly.
- Refuse missing, extra, replaced, reordered, or contradictory component material and any combined arm whose lineage differs from the reviewed plan.
- Done when the registered combined arm replays deterministically from exact component observations and no component observation can hide another component's absence, contradiction, or lineage mismatch.
- _Requirements: 19.4, 19.5, 19.11_
- _Boundary: Combination Validation_
- _Depends: 15.1, 15.2_
- [ ] 15.4 Enforce proof-candidate identity and prevent proof transfer
- Keep every label-blind proof candidate non-authoritative until the V3 release establishes its exact arm, family, scope, roles, inputs, and location.
- Apply proof-backed status only through the existing trusted differential-proof validator and the identical released candidate.
- Permit a combined arm to retain only a candidate already present in its named component observation; prevent creation, substitution, and cross-arm transfer.
- Preserve absent or inconclusive proof as not demonstrated and refuse role-swapped, cross-family, cross-scope, location, input, control, producer, receipt, or trust-policy mismatches.
- Done when valid released proof attaches once, absent proof remains non-demonstrated, and the complete transfer/mismatch matrix is refused.
- _Requirements: 19.6, 19.7, 19.8, 19.9, 19.10_
- _Boundary: Proof Continuity_
- _Depends: 15.3_
- [ ] 16. Derive exact scoped metrics and completed reports
- [ ] 16.1 Partition quality evidence by exact arm, family, and ecosystem
- Select vulnerable and fixed cases only from the independently released exact scope and derive pair correctness without cross-scope reuse.
- Derive precision only from findings and cases assigned to the same arm-scope key under its reviewed denominator policy.
- Preserve family decision vocabulary, neutral outcomes, evidence bases, model outcomes, and semantic coverage independently for each key.
- Done when a multi-arm, multi-scope case set yields isolated partitions and any cross-arm, cross-family, or cross-ecosystem contribution is refused.
- _Requirements: 18.1, 18.2, 18.4_
- _Boundary: Evaluation Scoped Metrics_
- _Depends: 15.2, 15.4_
- [ ] 16.2 Project ordinary pushes by ecosystem and deduplicate whole coverage
- Count each eligible ordinary push once for every family claimed by the arm in the push's reviewed ecosystem and never in another ecosystem.
- Deduplicate one underlying push once in whole-population coverage even when it contributes to several verified scope denominators.
- Reconcile projected ordinary counts and global unique-push counts back to the reviewed census and released identities.
- Done when same-ecosystem multi-family projection produces the expected scoped counts, cross-ecosystem projection is absent, and whole coverage counts each push once.
- _Requirements: 18.3, 18.5_
- _Boundary: Evaluation Ordinary Projection_
- [ ] 16.3 Produce the complete scoped metric set and reject post-spend invalidity
- Derive every standard metric for every arm-scope key using the fixed algebra and Wilson 95% calculation.
- Publish numerator, denominator, sample count, value, and interval bounds for each scoped metric.
- Treat a post-spend zero denominator, census difference, missing standard metric, or inconsistent projection as invalid evaluation evidence rather than insufficiency, GO, or NO-GO.
- Done when complete nonzero scoped metrics match independent count fixtures and every zero/mismatch mutation fails before report construction.
- _Requirements: 18.1, 18.6, 18.8_
- _Boundary: Evaluation Metric Derivation_
- [ ] 16.4 Derive completed per-scope decisions and per-arm operational evidence
- Derive instrument, budget, readiness, and quality gates for every exact arm-scope result from canonical evidence.
- Emit GO or NO-GO only when the scope evaluation is complete; keep insufficiency, unread input, skipped stage, incomplete arm, and instrument impairment outside quality decisions.
- Preserve per-arm completed and deferred case identities, stage funnel, instrument/model failures, runtime, and closed degradation reasons separately from shared whole coverage.
- Require nonzero input reads and required-stage execution before accepting a clean-looking result.
- Done when every completed scope receives one reproducible decision and every incomplete or impaired fixture is refused without a quality decision.
- _Requirements: 20.1, 20.2, 20.3, 20.8_
- _Boundary: Evaluation Report_
- [ ] 16.5 Derive optional aggregates that can never authorize
- Bind aggregate constituents, sample counts, canonical rational weights, and derived metric values to exact arm-scope metric identities.
- Require weights to be non-negative, canonical, one-to-one with constituents, and sum exactly to one through integer arithmetic.
- Exclude gates, GO/NO-GO, proof promotion, and every consumer-authorization capability from the aggregate contract.
- Done when a valid aggregate reproduces from its constituents and raw, incomplete, gated, decision-bearing, or differently weighted aggregates are refused as authorization evidence.
- _Requirements: 16.6, 18.7, 20.7_
- _Boundary: Evaluation Aggregate_
- [ ] 17. Close complete replay and replay-only consumer authorization
- [ ] 17.1 Assemble one complete replay over the shared evaluation graph
- Bind the reviewed plan, exact feasibility, reviewed admission authority, preparation, receipt, all-arm spend binding, attempt-history head, V3 release, cases, per-arm reports, combination lineages, shared whole coverage, and optional aggregate.
- Require exact equality between planned arms/scopes and case/report identities, including every component required by a combined arm.
- Prevent replay construction when any arm, scope, mandatory gate, custody binding, release, case, component, or lineage is missing or invalid.
- Done when the complete graph produces one deterministic replay digest and every omitted or substituted component prevents construction.
- _Requirements: 19.4, 19.5, 19.11, 20.4, 20.5_
- _Boundary: Evaluation Replay_
- _Depends: 15.3, 16.4, 16.5_
- [ ] 17.2 Revalidate replay from canonical source artifacts and current trust state
- Resolve the complete attempt chain and observation/review content again, revalidate current custody, and rederive every identity, count, metric, gate, decision, report, lineage, and replay digest.
- Require exact equality with the carried values and refuse coordinated rehashing, stale external state, unknown/mixed versions, and partial roots.
- Make the validated complete replay the only value that may cross a V3 authorization boundary.
- Done when independent replay validation reproduces the original reports exactly and every source, summary, or trust-state mutation is refused.
- _Requirements: 16.6, 20.5, 20.6, 20.7, 20.8_
- _Boundary: Evaluation Replay Validation_
- [ ] 17.3 Publish the integrated V3 preflight, authorization, scoring, and replay facade
- Wire the exact plan preflight, reviewed authority, shared-spend validation, score/replay, strict decoding, and replay validation through one public V3 contract surface.
- Preserve every V2 facade and import unchanged and expose no benchmark storage, execution, lifecycle, or presentation implementation from core.
- Reject wrong-phase values and make each returned artifact usable only by the next declared phase.
- Done when a client can execute the complete synthetic contract through the public V3 schema and V2/unknown/mixed artifacts fail at the first boundary.
- _Requirements: 16.1, 16.4, 20.4, 20.6_
- _Boundary: Evaluation Admission Facade Integration_
- _Depends: 17.1, 17.2_
- [ ] 17.4 (P) Authorize fitted components only from one exact replayed arm-scope result
- Validate the complete V3 replay before selecting exactly one planned arm-scope result and require that completed result to be GO.
- Preserve existing reversed-pair probe replay, fitting lineage, exact model declaration, and disjoint resolved repository-holdout checks.
- Refuse V2 replay, raw arm report, aggregate, incomplete replay, missing/ambiguous key, NO-GO scope, or substituted fitted evidence.
- Done when a matching fitted component authorizes only from its exact GO scope and every partial, aggregate, cross-arm, or probe/holdout mutation fails.
- _Requirements: 13.2, 16.6, 20.7_
- _Boundary: Fitted Consumer V3_
- _Depends: 17.2_
- [ ] 18. Validate the V3 predecessor end to end
- [ ] 18.1 Run a synthetic multi-arm, multi-family evaluation through one shared spend
- Use at least two component arms and one combined arm across at least two families and two ecosystems with multi-scope fixes and representative ordinary pushes.
- Prove nonzero reads, reviewed plan, exact preflight, sealed observations, reviewed attempt history, one preparation/receipt, V3 release, scoped reports, complete replay, and exact fitted selection.
- Preserve component observations/results and verify ordinary projection plus shared whole-population deduplication in the completed replay.
- Done when the public V3 facade produces one validated replay with every planned arm/scope and no V2 or benchmark-owned implementation is used as a substitute.
- _Requirements: 16.1, 16.5, 17.1, 17.3, 17.5, 17.7, 18.1, 18.3, 18.5, 19.1, 19.4, 20.1, 20.4, 20.7, 20.8_
- _Boundary: V3 End-to-End Conformance_
- _Depends: 17.3, 17.4_
- [ ] 18.2 (P) Run the adversarial pre-spend and compatibility matrix
- Exercise V2 byte/digest/replay stability, V2-as-V3, unknown/mixed/subclass schemas, baseline mutations, impossible scopes, missing slots, zero/unreadable observations, self-review, forked attempt history, identity changes, raw access, stale custody, and subset spend.
- Prove every measurement instrument reads nonzero fixture bytes and fails loudly on unread input.
- Verify insufficiency leaves custody available and no refused case reaches detector execution or label release in the synthetic boundary.
- Done when every pre-spend mutation fails at its owning layer, V2 remains unchanged, and no failure produces a clean-looking result or quality decision.
- _Requirements: 16.1, 16.2, 16.3, 16.4, 16.5, 16.6, 17.2, 17.4, 17.6, 17.8, 17.9, 17.10, 17.11, 17.12, 20.2, 20.8_
- _Boundary: V3 Pre-Spend and Compatibility Validation_
- _Depends: 17.3_
- [ ] 18.3 (P) Run the adversarial post-spend, metric, combination, and replay matrix
- Exercise V2 release substitution, arm/scope/role/pair/result/input swaps, missing components, contradictory combinations, proof transfer, absent/inconclusive proof, cross-ecosystem ordinary projection, duplicate whole coverage, zero/mismatched denominators, missing gates, and incomplete reports/replay.
- Verify invalid post-spend evidence never becomes insufficiency, GO, NO-GO, aggregate authority, or consumer eligibility.
- Revalidate every complete replay from source artifacts and current custody rather than trusting carried summaries.
- Done when the valid control replay reproduces exactly and every post-spend mutation is refused before authorization without hiding another arm's status.
- _Requirements: 18.2, 18.4, 18.6, 18.7, 18.8, 19.2, 19.3, 19.5, 19.6, 19.7, 19.8, 19.9, 19.10, 19.11, 20.3, 20.5, 20.6, 20.7_
- _Boundary: V3 Post-Spend and Replay Validation_
- _Depends: 17.2, 17.4_
- [ ] 18.4 Verify dependency boundaries, complexity caps, and bounded validation growth
- Verify dependency direction, no V2/V3 aggregate imports across the version boundary, standard-library-only core runtime, and absence of benchmark, analyzer, model-runtime, or proof-execution ownership in V3 modules.
- Enforce function and module complexity caps and measure validation growth over increasing arms, scopes, observations, attempt events, cases, and reports.
- Fail each measurement fixture unless its scoped inputs are readable and nonzero before interpreting timing, count, or clean-result output.
- Done when import/boundary checks are clean, amended code stays within complexity caps, and validation cost remains bounded by supplied artifact counts without weakening checks.
- _Requirements: 16.2, 20.6, 20.8_
- _Boundary: V3 Boundary and Performance Validation_
- _Depends: 18.1, 18.2, 18.3_
- [ ] 18.5 Run the final compatibility, regression, and schema-export gate
- Run the V2 golden suite, every V3 unit/integration/adversarial suite, and the existing core regression suite without consuming a real admission population.
- Prove all fixtures are readable and nonzero and the exact `openultrasast.detection-core.evaluation-admission.v3` schema is publicly exported.
- Done when all gates pass, V2 outputs remain frozen, validation stays bounded by supplied artifacts, and the validated predecessor is ready for downstream benchmark task generation.
- _Requirements: 16.1, 16.2, 16.4, 20.6, 20.8_
- _Boundary: V3 Final Validation Integration_
- _Depends: 18.4_
## Implementation Notes
- The frozen migration inventory is a reviewed worklist with stable identities, not a semantic-use census. It may only close entries as `v2_migrated` or `retired`; it never grows, deletes entries, or proves absence by interpreting Python flow.
- Cutover is proved by explicit syntax checks plus deletion of the compatibility and v1 write capability. No task extends the discarded alias/data-flow interpreter.
- Task 10.5 debug established that executable owner probes cannot compensate for blanket source-closure acceptance; resumed hunt attempts also require collision-resistant attempt identity and create-only persistence before handoff validation is authoritative.
- Task 10.6 debug established that exact-byte successor authority must require canonical repository-relative paths and strict resolved-root containment, including rejection of outside-root symlink targets, before module digests can prove closure.
- Human review reopened Task 10.6 for one focused remediation cycle covering canonical path syntax, resolved-root containment/symlink refusal, and the task-local formatting gate.
- Human review reopened Task 10.5 for one focused remediation cycle covering exact source-closure evidence, collision-resistant create-only attempt persistence, and complexity-cap conformance.
- Task 11.4 debug established that the 73 reserved-repository references are a benchmark-governance specification conflict, not a detection-core or instrument defect; historical artifacts and the reservation guard must remain unchanged pending an owning-spec decision.
@@ -0,0 +1,490 @@
# Engine change-scoped analysis (G2 workstream)
Status: **proposed / in progress** (opened 2026-10-11). Owner: engine. Tracks work to address the
engine-detection gap found during the G0 baseline run. This is G2 ("improve what moves the unseen-repo
numbers"), gated on the pool metric; it is not part of the G0 record-07 baseline (that runs on the quick-rule tier).
## Problem (measured, not assumed)
The engine produces **no finding** on real unseen vulnerability changes. Live 300 s long-deadline replays show
that the cause is usually not a detection-capability gap:
- Clean Python injection change (`aws/bedrock-agentcore-sdk-python`, `install_packages`, introducing, 3-file
diff): `wall=303 s`, hit the deadline. The ranker found candidates (`static_rank=1.0`) but **`selected: []`** and
every candidate `deferred` with `reason: deadline_exhausted`, `analysis_not_started=deadline_exhausted`,
`change_context_incomplete`. No analyses ran. The ranked candidates were **repo-wide and unrelated to
the push** (`auth.py` access_control/config_secrets), not the changed `install_packages`. The engine is trying to
re-audit the whole repo and runs out of budget before analyzing the change.
- Noisy last_touch config_secrets change (`jenkinsci/metrics-plugin`, 14-file diff): finished in 206 s but
`comparison_unknown` + `capability_unavailable` + `eligibility_provenance_mismatch` +
`contributor-scan ... projection_unavailable`. This has several causes. Correspondence was one (fixed separately,
commit c39fbb8); the capability registry is Task 8.3.
## Hypothesis
A pre-push hook should check **the code this push changed**. Starting the engine's analysis from the
**changed declarations and their dataflow neighborhood** should let it run within budget instead of being
deferred. The `change_sites` are already in the replay spec. The missed detection comes from deferral,
rather than a limit in Joern's capability.
## Anchors (confirm in increment 1)
- `src/openultrasast/rank.py` is the static-evidence ranker (`ranking_mode="evidence"`, `existing-static-evidence-v1`).
- `src/openultrasast/mapping.py` builds the `selected`/`deferred` scope + the budget allocation that defers on
`deadline_exhausted`.
- `src/openultrasast/push/policy.py` and `push/runner.py` consume the scope; change_sites arrive via the push input.
- `benchmarks/unseen/replay_entry.py` carries `changed_paths` / `declaration_paths` in the spec.
## Increments (progress tracking)
- [x] **0. Cost split confirmed (2026-10-11, timed live replay).** `build_seconds=66.69` (CPG build is cheap),
`query_seconds=0.0` (no query ever executed), total 303 s (hit the deadline), **4,095** candidate questions all
`analysis_not_started=deadline_exhausted`, `selected: []`, `regions_scanned=0`. So of the 303 s, ~67 s built the
CPG and ~236 s went to enumerating/ranking/preparing 4,095 repo-wide candidate questions for a **3-file** change.
The budget was spent before a single query ran. The over-broad candidate set is the bottleneck, rather than the CPG build.
This confirms change-scoping as the fix. Preparing each candidate includes line-correspondence work,
so fewer candidates also reduce that cost.
- [x] **1. Located (2026-10-11).** The over-enumeration is upstream of the scan, not inside it:
- `push/runner.py:345-348`, `head_regions = discovery(tip.root, …)` discovers regions across the **whole head
tree** (the ~4,095), independent of the change.
- `push/runner.py:397-407`, those regions are passed wholesale to `scan_repository(..., change_context=context)`;
`change_context` is threaded in but **never scopes the region set**.
- `model/scan.py:167` and `:364`, `[_identity(unit, r, f) for r in regions for f in r.families]` enumerates all
regions × families; with `order_by_evidence=True` (`runner.py:288/403`) the scan evidence-tiers **every** pair
before analysis. That pre-work is the ~236 s measured in increment 0, so the deadline hits at `query_seconds=0`.
- Primitive already present: `model/regions.py:279` computes the changed-path set from the context;
`compare_targeted_base` (`runner.py:411`) is a precedent for change-targeted work.
- For increment 2, restrict the *analyzed* region set to the change's declarations + dataflow
neighbourhood (filter `head_regions` by `context.changed_paths` before the scan, or scope the enumeration inside
`scan_repository`). Keep the **CPG whole-repo** so taint queries can still trace sources from unchanged code into
the changed regions. Only the question set is change-scoped.
Correction (2026-10-11), after reverting the first attempt: the filter got two things wrong.
- **The engine already reaches into unchanged code by design, and it is tested.** A changed `source` that reaches
an unchanged `sink` must still select the unchanged sink's question
(`tests/test_model_change_context.py::test_changed_dependency_reaches_unchanged_ranked_question`;
`test_change_does_not_replace_ranker_selection` pins that change context must not alter the ranker's selection).
`_changed_files` feeds `_evidence_pass` only as `contextFilter`/`contextPaths`. This controls which locations to itemise vs
summarise, rather than shrinking the tier population. A "changed files only" scope deletes the reach-into-unchanged
behaviour. Any scope must keep changed regions **plus the regions they reach** (the affected/`relationships`
neighbourhood), not changed files alone.
- **The 236 s is now split (2026-10-11, saved artifacts).** `total=291s build=52s query=0s arbitrate=238.75s`,
and `query_seconds_by_kind["evidence"]=238.54s`, the evidence-tier pass is **99.9 %** of the arbitrate phase;
`affected_context` + `_scope_work` are **0.21 s**. The pass tiered 1,015 pairs (`tier_counts 0:1009, 1:6`) at
**~235 ms/pair** (contended; ~120 ms uncontended) and that alone blew the budget, `selected=0`,
`deferred=1365` all `deadline_exhausted`. Secondary anomaly: the design assumes ~2.5 ms/pair
("17,700 in 44 s"); here it is 50–100× that, i.e. the evidence query is unusually slow on a cold large
CPG. Fixing that is separate from cutting the pair count.
- Increment 2 targets the evidence pass (`scan.py:338-359`, `_evidence_pass`). The options must
reduce its cost while preserving flows into unchanged code:
- (i) Scope + bound: order the evidence candidates change-first and bound the pass to a fraction of the
budget so analysis queries still run; reached unchanged regions are tiered only while budget lasts.
- (ii) Neighbourhood pass: tier the changed regions, read the reached regions from the context rows the
pass already returns, then tier those in a second small pass. This preserves exact reach-into-unchanged behaviour but needs more code.
- (iii) Per-pair speedup: reduce the ~235 ms/pair (warm server / batching) rather than the pair count.
This helps every scan but needs a deeper engine change and does not by itself bound work on a huge repo.
**CORRECTION (2026-10-11, after implementing (i) + four live re-verifies, the cost model was wrong).**
Scope + bound was built and tested (change-first ordering, adaptive budget-fitted portioning, `_context_pass`
scoped to changed files, honest `evidence_pass_bounded` degradation; branch `engine-change-scoped-evidence-pass`).
It cut the tiered pair count, but `query_seconds` stayed **0** every time. The live numbers show why:
| pairs tiered | run_batch calls | evidence s | build s |
|---|---|---|---|
| 1015 | 1 | 238 | 52 |
| 512 | 1 | 219 | 68 |
| 64 | 2 | 220 | 68 |
| 4 (probe) | 1 | 160 | 127 |
Evidence time is **flat regardless of pair count** → the cost is **per-*call* fixed** (~150–220 s: loading the CPG
into the query engine + the census the evidenceOnly batch computes), ~85 ms/pair on top. Pair-count scoping
therefore cannot free budget, and portioning re-pays the fixed cost per call (so (i) can be counterproductive).
Build time also varies from 52→127 s. Two engines compete for the 7 GB host, which the record-07 baseline shares.
The fixes now resemble (iii), rather than (i): a **warm/persistent query engine or a cached census** to remove the
~150 s per-call fixed cost, and/or a **bigger engine-arm budget** than 300 s. A **clean (uncontended)
measurement** is also needed because contention may inflate build + fixed cost 2–3×. Scope+bound is not merged.
**UNCONTENDED measurement (2026-10-11, baseline paused via SIGSTOP + container kill, then resumed).** Shipped
engine, one evidence call over 1,015 pairs: `build=48 s`, `evidence=243.79 s`, `query=0`. The evidence pass is
**the same ~244 s uncontended as ~238 s contended** → the cost is **intrinsic, not contention**; contention only
inflated `build` (48 → 127 s) and added run-to-run noise. The model is `evidence ≈ 150 s fixed (CPG load into the
query engine + the census the evidenceOnly batch computes) + ~93 ms/pair` (244 = 150 + 1015×0.093). Consequences:
- **~200 s of fixed overhead** (`build` + evidence-load) precedes any analysis, so a 300 s budget leaves only
~100 s for everything else. This is why `query=0` even on a clean host.
- **Change-scoping still helps, but only by cutting the per-pair tail** (~1,015×93 ms ≈ 95 s): one evidence call
over ~30 changed pairs is ~150 s + 3 s instead of 244 s. This would free ~90 s, which might leave enough time
for analysis, but only just.
- **It must be ONE call, not portioned.** The ~150 s is a first-call load, so the adaptive probe attributed it to
its 4 probe pairs (per-pair≈37 s), concluded the budget fit ~0 more, and tiered only 4, the implemented
scope+bound has both problems under this cost model. The correct shape: change-first candidates, count-capped, ONE
evidence call, `deadline=None`.
- Pre-push viability: the ~200 s fixed floor means the engine arm cannot be a fast (≤30 s) pre-push check on a
repo this size without a warm/persistent session or a cached census. A larger budget (e.g. 600 s) makes the
benchmark arm run but is not a realistic pre-push deadline. The lever that matters for the product is killing
the ~150 s fixed cost, not scoping pairs.
- [ ] **2. Seed analysis from the change.** Prioritize (or restrict first-pass to) the changed declarations plus
their call/data neighborhood over repo-wide static rank, within budget.
- [ ] **3. Soundness.** Keep cross-file dataflow into the changed function (don't scope to literal changed lines);
add tests; keep existing push tests green.
- [ ] **4. Live re-test.** Re-run the bedrock change: does analysis now run and produce a finding on
`install_packages` (vs `deferred`)? Re-run metrics-plugin.
- [ ] **5. Measure the delta.** Engine-arm sample on the pool (catch/false-alarm + engine-coverage rate), needs the
kube lane (blocked on the AX completion path) for scale.
## Dependencies / notes
- Coverage `complete_within_scope` also needs the capability registry (Task 8.3) regardless of this work; this work
moves findings from `comparison_unknown`/deferred to a decided novelty → advisory engine finding.
- Implement directly and verify on the live instrument (a fake-client/unit pass is not evidence the live engine
changed); the kube-lane and store endpoints are infrastructure owned by the operator.
## Efficiency barrier analysis (post-record-07 priority)
The records confirm that the record-07 baseline measured regex only. The default and
long_deadline replay arms both hardcode `model="disabled"`, `max_model_calls=0`, `cost_usd=0`. No LLM is
called, and the engine never completed (`engine_covered=0`, the ~200 s wall). The measured `catch≈2.5%` covers
only the quick pattern-rule tier, a structured pattern search. This measures the floor, rather than the product.
It confirms the maintainer's point that pattern rules alone are insufficient; otherwise `grep` would do.
The product's two detection tiers did not run:
| Tier | Mechanism | Measured cost (uncontended, mid-size repo) | Ran? | Catch |
|---|---|---|---|---|
| 1 quick rules | regex/pattern over changed lines | ~seconds | yes | 2.5% (floor) |
| 2 engine (Joern) | CPG build + taint/dataflow | build 48 s + evidence (150 s fixed + 93 ms/pair) + analysis (unmeasured) | no (walled) | unknown |
| 3 LLM adjudication | model judgment / search-with-proof (hunter, fusion, decision engine) | model latency + $/call (unmeasured in the push path) | no (disabled) | unknown |
To be releasable, the net needs tier 2 and/or tier 3 to run and catch within a realistic pre-push window.
The target is **≤60–120 s** (developer patience), hard ceiling 300 s. The analysis records each tier's budget against
that window and identifies the measurements needed:
- Engine barrier (tier 2). Fixed floor `build(48) + evidence_fixed(~150) ≈ 198 s` precedes any dataflow answer.
Measure (clean host): **E1** split the ~150 s into JVM-start / graph-load / census; **E2** does a warm session
with the graph **preloaded at build** eliminate `evidence_fixed` on the first query (the architectural
test)?; **E3** analysis (`reachableByFlows`) per-pair cost + warm reuse; **E4** build cost vs repo size.
Levers in priority order: warm session + graph preload → cached census → change-scope the per-pair tail.
- LLM barrier (tier 3). Unmeasured in the push path. Measure: **L1** model round-trip latency per call
(DeepSeek) and calls needed per push for the decision/proof stage; **L2** the recall lift of tier 3 over tiers
1+2 on the pool, does the LLM catch what regex and dataflow miss, and at what $ and latency per push?
- Produce a **barrier table** for budget ∈ {60, 120, 300, 600} s × each tier combination. Record whether it fits
and its measured recall/false-alarm on the pool. Use this to identify the minimal viable config for a releasable
net. 600 s unblocks the benchmark arm but is not a pre-push deadline; the product target is tiers 2+3 under ~120 s.
Sequence: finish G0 (record 07) → **E1/E2 first** (they decide whether the warm session alone crosses the engine
barrier) → E3/E4 → L1/L2. Each on a clean host (the session times out contended). The change-scoped branch
(`engine-change-scoped-evidence-pass`, unmerged) only trims the tier-2 per-pair tail and is a late add-on, not the
lever.
## E1/E2 RESULTS (measured 2026-10-12, CLEAN host, bedrock-agentcore, 3-file change, 300 s budget, head side)
Four clean-host replays **correct the earlier "~200 s graph-load wall"** diagnosis. It had confused graph-load
cost with host contention from the running baseline replay:
| Run | build | evidence (taint batch) | dominance | query_s | selected | deferred |
|---|---|---|---|---|---|---|
| contended (prior) | 48–127 | 243.8 | — | **0** | **0** | 1365 |
| disposable (shipped, clean) | 27.9 | 225.1 | 34.8 | 34.8 | **356** | 1009 |
| warm session `SESSION=1` (clean) | 32.9 | 228.6 | 25.9 | 25.9 | **356** | 1009 |
| session + graph preload (clean) | 30.4 | 223.2 | 32.5 | 32.5 | **356** | 1009 |
The measurements show:
1. Host contention prevented the engine arm from running. The prior `query=0 / selected=0 / all
1365 deferred` was measured while the slice-1 baseline replay contended for the 7 GB host. On a clean host the
shipped (disposable) engine already runs dataflow: `query_seconds` 0 → ~35 s, `selected` 0 → **356 regions**,
dominance flow queries complete. This is the first time the engine arm produced analysis on this change.
2. **The evidence/taint batch (~225 s) is invariant to session and to graph preload** (225.1 / 228.6 / 223.2 across
the three clean runs). Warm session and preload-at-build do **not** reduce it. So the dominant cost is intrinsic
to the taint batch, not graph I/O.
3. Graph-load is cheap and does not account for the delay. The `dominance` query is a full cold query *including*
`importCpg`, and it costs only ~35 s. So `importCpg` is tens of seconds, not ~150 s. **This corrects the earlier
note** that attributed the ~150 s fixed to "first query JVM-start + graph-load + census." The ~160 s fixed lives
inside `taint.sc`'s own per-batch flow work (seed/sink memo fill over the distinct `(family, file)` combinations
it walks). It is paid lazily as the first pairs are processed, independent of the raw pair count.
4. **The warm session is not the lever.** It starts fine on a clean host (the contended `session_startup_timeout`
was contention-only), but gives ~no benefit here: subsequent queries (dominance) are already cheap disposable,
and the evidence pass is a single batch call, so there is nothing for the session to amortise.
5. The barrier, restated: `build(~30) + evidence(~225) ≈ 255 s` of a 300 s budget, so 1009/1365 regions defer
as `deadline_exhausted`; only 356 are analysed. The fix is to **reduce the taint batch's work** by scoping
the pair/family set to the change so the batch fits the budget. Session reuse, preload, or a bigger JVM will not do this. The parked branch
(`engine-change-scoped-evidence-pass`) is therefore **relevant after all, on a clean host**, provided it scopes
the family/file set `taint.sc` walks (not merely the pair list); its adaptive portioning still needs replacing
with a fixed cap (per-call-fixed cost, not per-pair).
6. Side note: the `base`-side CPG build failed (`cpg_build_failed`, 0.9 s) in the preload run, a separate
intermittent base-revision build issue; the `head` side built and analysed normally. Track separately.
E1/E2 did not answer the G2 question: does the engine catch vulnerabilities regex misses when it selects 356 regions
on a clean host? That needs an **engine-on unseen pilot** (a handful of pool repos replayed with the
engine arm enabled on a clean host) scored against record 07's regex-only floor (catch 3/92). That is the
next experiment, ahead of E3/E4/L1/L2.
## ENGINE-ON PILOT RESULTS (measured 2026-10-12, CLEAN host, 5 public pool repos, vuln change each, 300 s)
Five public repos were picked for flow-shaped families and small size, giving the engine its best chance to finish and catch:
| repo | fam | size | REGEX on vuln | ENGINE finding | cov | vuln region ranked | best taint | selected/deferred | wall |
|---|---|---|---|---|---|---|---|---|---|
| masci/banks | path | 2f | ✗ | **none** | incomplete | yes | sink found, no source (score 0) | 281 / **0** | 236 s |
| vitejs/launch-editor | injection | 1f | ✗ | **none** | incomplete | yes | tier1 score2.0 sink (wrong fam) | 13 / 38 | 261 s |
| getzep/graphiti | injection | 5f | ✗ | **none** | incomplete | yes | injection score 0 | 882 / **0** | 304 s |
| aws/bedrock-agentcore | injection | 3f | ✗ | **none** | incomplete | yes | — | 356 / 1009 | 305 s |
| miguelgrinberg/python-socketio | deser | 10f | **✓ (n=2)** | **none** | incomplete | yes | **tier1 score2.0 sink found** | 136 / **0** | 257 s |
Result: regex 1/5, engine 0/5. On a clean host, with budget to spare on most, the shipped engine **added no
catches over the regex floor.**
The diagnosis was corrected after inspecting `head.findings` rather than ranker evidence:
1. The budget hypothesis is refuted. banks, graphiti and socketio **finished their region queue** (deferred=0)
and still emitted no finding. Change-scoping to fit budget would not have changed the outcome.
2. The engine produced no vuln-family findings (path/injection/deser) on any of the 5. Actual `head.findings`:
banks 1 (config_secrets), socketio 2 (config_secrets), launch-editor / graphiti / bedrock **0 findings each**.
The dataflow/taint tier traced **no source→sink flow** for the vuln families (`taint_evidence_not_applicable`;
e.g. banks found the `path.open` sink but **no source reaches it**). Correction: the tier=1 / score=2.0
"sink" seen in the ranker's `normalized_evidence` is the ranker's static sink candidate, not a confirmed taint
finding. An earlier draft of this section misread it as detection partially working. It was not.
3. `ambiguous_line_correspondence` does not explain these missed vulnerabilities. It fires on all 5, but there
are **no vuln-family findings to anchor**, so a region-level witness fallback (the proposed "Option A") would
change nothing here. Witness/anchoring only touched the config_secrets findings that did appear.
4. **The LLM adjudication tier was disabled** (`model_calls=0`, `cost_usd=0`, `model_assistance={}` on all 5). The
rungs reached: `suspicion=0, model_corroborated=0, model_entailed=0–2, execution_confirmed=0`. The tier designed
to confirm a vuln the static dataflow cannot trace **never ran**.
5. **Ranking also under-promotes the vuln region** (tier 0 for most families; banks path family stuck low). So even
where a sink exists, the region is not analysed deeply enough to attempt a flow.
6. The pool's SZZ last-writer change definition yields large/moved diffs; a cleaner probe
uses tight vuln-introducing diffs. Secondary to (2)/(4).
Reprioritised engine levers (corrected, anchoring is not the lever):
- #1: the LLM adjudication tier (disabled in the push path) has the highest priority. regex+dataflow = 1/5 with
the model OFF; the static tiers produce no vuln-family findings on these shapes. The hypothesis
([[search-with-proof]]) is that the LLM tier catches what regex+dataflow miss. The pilot supports
testing it: **L1** model latency + calls/push, **L2** recall lift on the same 5 (and beyond). These calls cost money, so stage
them (5–10 sizing → ~10–20 pilot → full only on lift).
- #2 taint source/sink coverage + region ranking so the dataflow recognises these vuln sources and promotes the
vuln region (banks path: sink found, source unrecognised; graphiti injection: score 0).
- #3 change-anchoring robustness matters only after the static/LLM tiers produce vuln-family
findings to anchor. Deprioritised until (1)/(2) yield findings.
Detection does not meet the pre-push net's release requirements. Clean host, generous budget, LLM off: regex 1/5,
engine 0/5. The static tiers (regex + Joern dataflow) do not detect these unseen vulns; the untested lever
is the **LLM adjudication tier**. Next, run an **LLM-tier pilot (L1/L2)** on these 5 repos to test whether the model
produces vuln-family findings regex+dataflow miss, and measure its $/latency.
## ROOT CAUSE of engine 0/5 + the "#2 guided by #1" workstream (2026-10-12)
Code and evidence show why the taint tier traced no flow. The pilot repos are **libraries**, whose
untrusted input arrives as **public-API function parameters**, not HTTP requests. The Python taint **source**
vocabulary (`ruleset/semantic/python.toml`) is entirely web/CLI: Flask/Django `request.*`, `sys.argv`, `sys.stdin`,
`input(`. There is no source for a library's own API parameter. The engine *does* have a parameter-as-source mode
(`_parameters_are_input`, `scan.py`), but it fires **only for recognised entry points** (`_is_entry_point` and
`entry_kind != "function"`). The pilot vuln regions carry `entry:false`, so their parameters are **deliberately
not** sources. That restriction is intentional: treating every function parameter as a source caused a flood of
false SQL-injections on WordPress/PMPro (documented in the `_parameters_are_input` docstring). The static
engine cannot safely widen its sources to library APIs without reintroducing those false alarms.
For `banks` path: sink `path.open` found, `source_local/near=false`; `graphiti`/`bedrock` injection: no source.
The maintainer's "#2 guided by #1" directive addresses this gap. Static taint cannot *safely*
decide which library parameters are trust boundaries; the **LLM can** reason about which public-API surface is
attacker-reachable (intent, not pattern). So:
- #1 (LLM) guides: on a changed library region the engine ranks but cannot source, the LLM adjudicates whether a
parameter is an untrusted trust boundary and names the source→sink it believes exists (search-with-proof).
- #2 (taint) traces + proves: the engine then runs the flow from the LLM-named source to the known sink,
confirming or refuting it with a traced path. The trace provides proof beyond the LLM's judgment. This would avoid the static FP
increase while giving every catch a dataflow witness.
- Where the LLM repeatedly names the same source shape (e.g. a registry-key → path family), promote it
to a static fact in `python.toml` guarded by the entry/shape conditions that keep WordPress quiet.
Proposed first increment (pending budget confirmation, LLM calls cost money): wire the disabled LLM tier on the
5 pilot regions in *LLM-guided-source* mode and measure: (a) does it name the right trust-boundary source per vuln,
(b) does the taint engine then trace source→sink and emit a vuln-family finding, (c) catch lift over regex (1/5) and
false-alarm cost on the ordinary changes. Stage it (5–10 sizing → ~10–20 → full on lift). This makes "#2 guided by
#1" the concrete G2 detection workstream; budget/change-scoping and change-anchoring stay deprioritised until it
yields findings.
## BANKS SIZING + "2 guided by 1" validated (2026-10-12, budget-approved, DeepSeek)
Sized the LLM tier on banks (the clearest pilot miss) with the project's own DeepSeek client (`deepseek-flash`).
DeepSeek is reachable, and model id `deepseek-flash` is valid. **~$0.00016–0.00025 per
call, ~1.1 s latency.** This costs less than a cent per push even at 8 candidates × a few regions. The GLM/abliteration fallback
(`ABLITERATION_*`) currently errors `HTTP 422` on the `thinking` request param. A client fix (don't send
`thinking` to that endpoint) is needed before it can be used; DeepSeek works today so it is not blocking.
"2 guided by 1" works in this case: the LLM gives the correct verdict when given the real sink + source
context:
- For `_load` (the pool's SZZ-labelled site, 135–141), the model returned `vulnerable=false`. This is correct: `_load` reconstructs
paths from its own index, not the sink.
- Asked about the real sink `from_prompt_path`/`set()` line 44 (`path / f"{prompt.name}…"; write_text`) with the
context that `prompt.name` can be request-derived: **`vulnerable=true`**, *"prompt.name is joined into a
filesystem path and written without sanitization… path traversal or arbitrary file write."* This is a correct catch.
Two blockers to realising it in the live pipeline (both now concrete):
1. **Context, not single-shot.** The model only catches with the real sink + caller/source context. A single-shot
question on the ranker's region (which pointed at `_load`, and at the benign local-glob `_scan`) does not. This
requires the **agentic search-with-proof hunter** (explores callers/entry points to establish the trust
boundary), not the single CANDIDATE_QUESTION path. That needs more calls → their cost is the next measurement.
2. **Corpus-label soundness.** The pool's banks label is mislocated: `head_lines=[[30],[104],[138–141]]`,
`function=_load`. The real sink is line **44**, outside head_lines±10 and a different function. So (a) even a
correct finding at line 44 would not score as a catch under `location_match`, and (b) the sink pre-exists in base
(the change did not touch it), so a change-scoped pre-push hook **correctly stays quiet**. The pool even
annotates this bias ("delta hook can correctly stay quiet"). **Part of the pilot's engine 0/5 is this SZZ
mislocation, not a detection failure.**
The budget is approved. Keep the revised next steps staged:
- Validate the 5 pilot labels first (cheap, LLM-assisted corpus audit): for each, is the real sink inside the
changed lines, and is it change-introduced vs pre-existing-in-base? Only sound, change-located vulns are fair
targets. This separates true detection gaps from SZZ artifacts before spending on detection runs.
- Then run the agentic hunter (search-with-proof) on the sound targets and size its per-vuln call count/cost;
measure catch + false-alarm (on ordinary changes) vs regex 1/5. The single-shot path is insufficient; the hunter
is the real "#1".
- Feed confirmed source shapes (e.g. library param → path join) back as guarded static facts (#2).
## Prompt evolution is a CORE lever for the LLM arm (maintainer directive 2026-10-12)
**"What is key is that we evolve the prompts as well"**, and **"GEPA or comparable; this is an evolving field."**
The bedrock hunter ran (64 model calls, $0.0076), but its **current prompts** led it to flag adjacent concerns
(network-PUBLIC default, download_file). It never named the `install_packages` command injection. This was a
*prompting/targeting* miss, rather than a cost or capability limit. So the LLM arm needs a **prompt-optimisation loop** that reflects on misses/low-confidence
outputs, updates the prompt, and retries. Build it on **DSPy** (its GEPA / MIPRO optimisers). Keep it **pluggable**
behind an interface because the field changes quickly (GEPA, MIPRO, TextGrad, successors). This aligns with
[[learned-decisions-not-hand-tuning]] (learned decisions + local memory, not hand rules). It applies to both LLM
uses here: the search-with-proof detector and the corpus-label auditor.
The optimisation must learn which hints/annotations the model needs to reach the real sink. Rewording the
prompt alone is insufficient. bedrock needed the hint "`install_packages` runs `pip install` with caller-supplied package
names → command injection"; banks needed "`prompt.name` is request-derived and joined into a filesystem path →
traversal". DSPy optimises which evidence/hints to feed the signature so the model targets the true sink instead of
adjacent noise.
Use the project's existing framework knowledge for those hints. The semantic facts taxonomy
(`ruleset/semantic/*.toml`, `ruleset/frameworks.toml`) encodes sources/sinks/sanitizers per framework without
user annotations ([[php-detection-is-engine-only]] zero-annotation value). Where framework knowledge covers the
API, feed it as a hint. Where it does not (library APIs like banks/bedrock), the LLM proposes the trust boundary,
which becomes a fact after repeated confirmation (the #2 feedback loop). No new annotation burden on the
user. This is now part of the "#2 guided by #1" workstream and must not be deferred. (DeepSeek
`deepseek-flash` is the working default; GLM 5.3 "cyber" model is future-spec.)
## POOL-P1 LABEL AUDIT (LLM, 2026-10-12): 47% of labels are sound
The banks label-location check was extended to all **49 public** pool vulns via DeepSeek (advisory + head code +
pool label → sound/unsound). Full artifact: `benchmarks/measurements/2026-10-12-pool-p1-label-audit/`.
- **label_sound = 23/49 (47%)**; 53% unsound (25 pre-exist in base, 16 sink outside changed lines). Cost $0.038.
- By family (sound/total): injection 10/13, access_control 5/8, untrusted_destination 4/7, deserialization 1/2,
config_secrets 1/4, **path 1/7, output_encoding 1/8**. Injection labels mostly sound; path/output_encoding mostly
mislocate or pre-exist.
- Record 07's floor (catch 3/92) is scored against a corpus ~half unfair to a delta hook. On the ~53%
pre-existing/mislocated vulns a change-scoped hook correctly stays quiet. The honest catchable denominator is the
sound subset; record 07 needs this caveat + a sound-subset recomputation.
- Only bedrock + graphiti (injection) of the 5 pilot cases have sound labels. The engine missed both, so these
are detection gaps. The 23 sound targets are the fair local eval set for "#2 guided by #1".
- Next: run the agentic search-with-proof hunter on the sound targets (start bedrock + graphiti), size its
per-vuln call count/cost, measure catch + false-alarm. Then recompute record 07 on the sound subset.
## MODEL HUNTER on the 2 sound targets (2026-10-12): first LLM catch the static tiers missed
Ran `ousast scan --mode deep` (DeepSeek `deepseek-flash`, model layer + tool_hunter, 60-call/8-hotspot bound) on
bedrock + graphiti, the two sound pilot targets the static engine missed. Artifact:
`benchmarks/measurements/2026-10-12-hunter-sound-targets/`.
- **hunter 1/2 vs static engine 0/2.** graphiti: **HIT**, `falkordb_driver.py:207`, tag `family:cypher-injection`,
rationale traces `build_fulltext_query` concatenating user input into a Cypher string at line 278 (inside the
labelled function, location-matches). bedrock: **miss**, 12 suspicions on adjacent concerns, never named the
`install_packages`→`pip` command injection.
- Cost is low: ~$0.007/repo, 64 model calls for a whole-repo deep scan.
- Everything stopped at `suspicion`. The proof arm did not elevate to corroborated/entailed/execution_confirmed.
Release-grade BLOCK needs *proven* findings; the next step is to measure the proof arm's lift.
- bedrock's miss is the concrete case for the DSPy prompt/hints loop above.
Next: (a) measure the proof arm (can it elevate graphiti's suspicion to proven?); (b) stand up the DSPy hint loop
and re-run bedrock; (c) widen the hunter to the 23 sound targets for a catch rate; (d) then a change-scoped + proven
run for the release-relevant shape.
## STRATEGIC PIVOT: core problem = unprovable reachability; FIX THE EVALUATION TARGET FIRST (2026-10-12)
The maintainer and team identified the problem: the tool can find dangerous operations but cannot prove
an attacker can reach them. The trust boundary is outside the changed library code, in a caller/application
that a change-scoped scan does not see. The project will not emit unproven alarms. Recall → ~0 from unprovable
reachability, not blindness. Cost/latency/engine-wall/corpus-labels did not explain this problem.
The chosen bet is to fix the evaluation target first. The three options were fix-target / advisory-with-conditions / build-prover.
A pre-push hook runs inside a developer's application (entry points present, request→sink in-repo), but the pool is
library-heavy, so we measured the hardest/wrong case.
Shape classification (LLM, 49 public vulns, $0.033, `benchmarks/measurements/2026-10-12-pool-p1-shape-classification/`):
reachability_in_repo True only 12/49 (24%); 37/49 are pure libraries. A valid catch needs a sound label and
in-repo reachability → 5/49 (~10%): graphiti, desktopcommandermcp, malla, document-merge-service, mcp-remote.
Repository shape predicted the hunter result (graphiti app-shaped→caught; bedrock library→missed). So record-07's 3/92 is
scored against a ~90%-illegitimate denominator. The near-zero catch largely comes from a mismatch between the targets and the tool.
Next: deep scan (regex/engine/hunter) on the 5 fair targets → per-tier catch rate on the corrected set; then
stand up an app-shaped eval set from known-vulnerable apps (VAmPI/NodeGoat/DjanGoat/vibe-py) and recompute the
floor. Revisit the deferred bets (advisory-with-conditions, reachability prover) once the fair-target results are available.
## FAIR-TARGET RESULT: detection 3/5 on app-shaped+sound (vs ~0 library-heavy), target-fix validated (2026-10-12)
Deep scan on the 5 app-shaped+sound targets (`benchmarks/measurements/2026-10-12-hunter-fair-targets/`):
- 3/5 detected: graphiti (hunter, Cypher injection), mcp-remote (hunter, url-scheme injection), dcmcp (ENGINE
overlay, "outbound.request reached from parameter" on readFileFromUrl; the param-as-source mode fired because an
entry point is present). 2/5 missed: malla (DOM/template XSS) and dms (Jinja SSTI), both in families
documented as limited in families.toml (template-mediated escaping).
- The library-heavy pilot found ~0 → this confirms that the target mismatch affected detection. On an app-shaped repo the engine
itself detects (parameter is a recognized source once the entry point is in-repo).
- Everything stops at `suspicion`/`static_corroboration`. None are proven. Cost ~$0.007/repo.
Bet #1 (fix the eval target) is necessary and improves detection, but is insufficient on its own.
Detection appears on fair targets; release-grade BLOCK still needs (a) proof-arm elevation (suspicion→proven) and
(b) family coverage (template XSS/SSTI) + the DSPy hint loop for misses. Next: recompute a floor on an app-shaped
sound subset / real app corpus (VAmPI, NodeGoat, DjanGoat, vibe-py), and measure the proof arm's suspicion→proven lift.
## PROOF-ARM MEASUREMENT on the 3 fair-target hits (2026-10-12): push-time lift 0/3, structural
Measured whether the proof arm can elevate the 3 fair-target suspicions (graphiti, mcp-remote, dcmcp) to
`exploit_demonstrated`.
- The search arm's `FAMILY_ORACLES` (search/demo.py) differs from families.toml's `verifier`.
All 3 hits have a runtime oracle: injection→(sql,command), untrusted_destination→(ssrf). (deserialization/
access_control/config_secrets have none, empty tuple → `no_oracle`.) So dcmcp is provable in principle
(earlier "unprovable" was a families.toml misread).
- Runtime differential proof through `search/verify.verify()` requires (1) an authored declarative demo
(start the app on a port, run exploit steps, fill oracle placeholders: sql→database_path, command→marker_dir,
ssrf→callback_url), (2) both head and base runnable with offline deps (`pip --no-index`, `npm ci --offline`),
(3) a sandbox differential "owned effect in 3/3 affected runs and 0/3 safe runs". Local host has a sandbox
(`isolation_mode: userns`); the plane supplies a task-boundary dispatcher.
- The scan/push path reaches only `suspicion`/`static_corroboration` (verification.py levels). There is no code
path from a scan suspicion to `exploit_demonstrated`, and no caller wires the search Coordinator from the push
flow. The vulns are real, but proof-arm lift at push time = 0/3 because proving is a
runtime differential operation with an offline build for each target that cannot run inside a pre-push hook. (Prior
per-pair demo attempts were mostly inconclusive on this exact wiring, app startup, offline deps, oracle plumbing.)
This resolves the three-bet choice. Proof/BLOCK runs asynchronously (CI / plane / triage
with authored demos + a runnable sandbox). At push time, the strongest available evidence is corroborated
suspicion:
- Bet #2 (advisory-with-conditions) is the honest PRE-PUSH product: surface corroborated suspicions ("change
introduces a flow that looks like X; reachable if untrusted Y reaches it"), let the dev / coding-agent (who has
the app context) judge. This matches the "oversee coding agents" purpose and the evidence available at the suspicion rung.
- Bet #3 (the proof arm) is the ASYNC BLOCK lane: run `search/verify` on the plane/CI for the suspicions worth
proving, with authored (eventually DSPy-authored) demos. Run a bounded, separate test on one tractable target
(mcp-remote) to confirm that the arm demonstrates a real unseen vuln.
## BANKS FIXED-SEARCH MVP (2026-10-12): "no wrong targets, only wrong searching" demonstrated
Record: `benchmarks/measurements/2026-10-12-banks-fixed-search-mvp/`. Target: banks (the "library" that gave
no engine path findings in the pilot). The target and CPG are unchanged; only the search changes.
- Injected the two annotations a per-target LLM steer would supply: **parameter-as-source**
(`parameter_sources=True`, so `prompt.name` counts as untrusted) + the **missing file-write sink**
(`write_text`/`read_text` added to path sinks).
- The shipped search baseline resolved 0 `prompt → write_text` flows. Its 5 flow rows are all census/
evidence (`sink=None`), matching the pilot's zero path findings.
- Fixed search: the engine resolves `prompt → path-join → prompt_file.write_text(prompt.raw)` in
`from_prompt_path` (`source=prompt`), plus a second `write_text` sink in `_scan`. These appear only with
the annotations.
- The banks result supports the hypothesis: the target was valid, but the search lacked annotations. The
fix needed exactly **two** inputs: the param-source and the file-write sink. These are what the
per-target LLM steer (DSPy-optimised, framework-seeded) has to learn to supply. This is the first measured
instance of a complete "#2 guided by #1" loop on an unseen target.
- Caveats: (1) one row shows `source=path` not `prompt`, the param-source fires broadly, the steering loop needs to control that breadth to improve
precision; (2) this is a trace, not the runtime differential proof (proof stays the async BLOCK lane).
The first MVP used the single-query `cpg.run("taint", …)` path and returned
0/0 because of an instrument failure, rather than producing a valid result (`cpg query taint failed` on every query). Root cause filed in
`bugs/taint-sc-single-query-signature-skew.md`: `request_params()` emits 5 keys (`guards`, `fixedOrigin`,
`originAnchors`, `quotedSanitizers`, `fieldParameterSources`) that `taint.sc`'s `exec` signature does not
declare, so both single-query sub-routes (CLI `--param` and session named-arg) reject them. The batch path
(`run_batch` → `requestsFile` JSON) tolerates them, which is why the pilot scans ran and this MVP used it.
@@ -0,0 +1,127 @@
# BUG: `taint.sc` single-query path broken by exec-signature skew
Filed: 2026-10-12 · Repo HEAD at filing: `7ab5c2a` · Severity: **high (silent zero)** · Status: **FIXED (2026-10-12)**
## Resolution (2026-10-12)
Fixed via **option 1** (filter params to the declared signature) in `src/openultrasast/cpg/backend.py`:
- New `_declared_exec_params(source)` parses the `@main def exec(...)` parameter names from a shipped script
(brace-aware, tolerant of an unfindable signature → empty set → "do not filter").
- New memoised `JoernBackend._declared_params(query)` reads+caches a script's declared names.
- `JoernBackend.query()` now drops any param the script does not declare before building the `--param` list
(disposable route) and the in-session named-arg call (session route). This is faithful, not lossy: the
single-request branch of `taint.sc` (line 1542) calls `rowsFor(...)` with top-level params only and lets
`fixedOrigin`/`originAnchors`/`quotedSanitizers`/`guards`/`fieldParameterSources` default; it never could
consume them. The batch path keeps carrying them as JSON the script parses itself.
- No-op for `config`/`dominance`: their own `request_params` emit only params their `.sc` declares (verified),
so nothing is dropped. Only `taint` sheds the 5 extras.
**Verified end-to-end** (patched backend mounted into the replay image, single-query path on banks): the
queries now RUN (no `cpg query taint failed`); baseline = 0 findings, fixed search (annotation) = 2 scored
`path`/`model_entailed` findings (`from_prompt_path` → `prompt_file.write_text(prompt.raw)` L45; `_scan` →
`self._index_path.write_text(...)` L155). Regression test:
`tests/test_cpg_backend.py::test_a_single_query_passes_only_params_the_script_declares` (fake runner, no Joern;
asserts the 5 undeclared params never reach the command line and the query parses).
## Summary
Any call on the **single-query** taint path (`cpg.run("taint", request_params(...))`) fails on **both**
of its sub-routes, returns `None`, and is read upstream as "no flows / clean". `request_params()` emits five
keys that `taint.sc`'s `@main def exec(...)` does not declare, and the single-query dispatch passes every
param straight to the script. The **batch** path (`run_batch` → `requestsFile` JSON) is unaffected, which is
why the engine-on scans ran while the single-query MVP did not.
This is exactly the failure shape AGENTS.md warns about: a tool that could not read/execute its input reports
a plausible zero that is indistinguishable from a clean result.
## Mechanism
`request_params()` (`src/openultrasast/model/taint.py`) always emits, among others:
- `fieldParameterSources` (line 54)
- `fixedOrigin` (line 65)
- `originAnchors` (line 66)
- `quotedSanitizers` (line 67)
- `guards` (line 68)
`taint.sc`'s signature (`src/openultrasast/cpg/queries/taint.sc:41` `@main def exec(...)`) declares **none**
of those five. Its 19 params are: `cpgFile, sources, sinks, sanitizers, function, file, callDepth,
boundedSinks, parameterSources, fieldSources, evidenceOnly, hookCallbacks, dispatchApply, dispatchValue,
contextFilter, contextPaths, requests, requestsFile, trace`.
`backend.query()` (`src/openultrasast/cpg/backend.py:1266`) dispatches a single query two ways, **both** of
which choke on the undeclared keys:
- Disposable / CLI route (lines 1276–1278): appends `--param {key}={value}` for every param. joern errors
`Unknown arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources`
(and, as a knock-on, `Missing argument: --cpgFile`). Observed verbatim via `banks_debug.py` on 2026-10-12.
- Session route (lines 1321–1326): defines `def ousast_taint(...)` by renaming `@main def exec(` and then
calls `ousast_taint(key = value, ...)` with named args. The same five keys are not parameters of that
method → the Scala named-arg call fails to compile. (By construction; the rename preserves the exact
`exec` parameter list.)
Why the batch path survives: `run_batch` writes the per-request params **inside** the `requestsFile` JSON
object, and `taint.sc` parses that JSON itself (`requestsJson`, taint.sc:67). The extra fields ride as JSON
keys, never as `exec` arguments, so they are tolerated.
## Blast radius
Broken (single-query taint):
- `taint._usable_flows` (`taint.py:124`) → `verdicts()` (`taint.py:131`) → `verdict()` (`taint.py:99`).
- `pipeline.scan_region` dispatches taint families to `taint_verdict` (`pipeline.py:121`) → so **per-region
single-query taint via `scan_region` is dead** whenever the disposable or session route is taken.
Unaffected:
- The push/scan **batch** path: `scan.py` uses `run_batch("taint", …)` (scan.py:1259, 1264, 1387). This is
why the engine-on pilot produced results.
- `config` and `dominance` single-queries (`config_value.py:46`, `dominance.py:54`): their `.sc` signatures
are presumed to match what their specs emit (confirm separately; not exercised here).
Open question: **CLOSED by call-graph trace (2026-10-12).** No shipped product path and no in-repo
benchmark/test reached the real single-query `JoernBackend.query("taint")`; the bug was latent but unreached
by the product. Traced:
- Shipped push/scan/replay (`cli.py:1334` → `scan_repository` → `_scan_repository_impl`) is DOUBLE
shielded: Phase 2 fetches taint via the **batch route** (`run_batch`, always wired by `build()`), and
Phase 3's `scan_region` (scan.py:559) runs against a **`_prefetched` CpgResult** whose `run` ignores the
query and returns the already-fetched batch rows (scan.py:1472). So `scan_region → taint_verdict →
_usable_flows → cpg.run("taint", …)` is a no-op replay that never reaches `JoernBackend.query()`.
- The non-batching fallback (`scan.py:519 cpg.run(kind, params)`) only runs for a backend without
`run_batch`; `JoernBackend` always provides it (backend.py:625/658), so it is dead for Joern.
- In-repo real-backend benchmarks both use the batch path: `benchmarks/ranking/position.py` via
`scan_repository`; `benchmarks/learn/engine_trace_worker.py` via `cpg.run_batch_once("taint", …)`.
- `tests/test_model_taint.py` / `tests/test_model_pipeline.py` drive `verdict`/`scan_region` against FAKE
canned-row `CpgResult`s, not a real `JoernBackend`, so they never exercised `query()` with these params.
- The only callers that reached the real single-query path were **out-of-product ad-hoc harnesses** (pair /
measurement scripts, the banks MVP). Regression onset: **562b670 (2026-09-25)**, when `request_params`
began emitting `fixedOrigin`/`originAnchors` (taint.sc never declared them on `exec`; they are batch-JSON
only by design). Residual unknown: any EXTERNAL/historical pair harness that drove single-query taint
against a real backend after that date would have silently zeroed; none is present in-repo to confirm.
The fix restores the single-query path for pair/direct use without altering push behaviour.
## Evidence
- `benchmarks/measurements/2026-10-12-banks-fixed-search-mvp/`: the batch path works (11 flow rows,
`write_text` sinks); the v1 single-query attempt returned 0/0 on `cpg query taint failed`.
- Raw joern error captured by `banks_debug.py` (scratchpad): `Missing argument: --cpgFile; Unknown
arguments: --fixedOrigin --guards --quotedSanitizers --originAnchors --fieldParameterSources`.
## Suggested fix (not yet applied)
Pick one, lowest-risk first:
1. **Make `backend.query()` filter params to the script's declared signature** before building `--param` /
named-arg lists (parse the `@main def exec(` arg names once, intersect). Fixes every current and future
signature skew for all single-query scripts, symmetrically with how the batch path already tolerates
extras. Preferred.
2. Add the five params to `taint.sc`'s `exec` signature as ignored passthroughs (mirrors batch semantics but
duplicates the JSON parsing already in the script; narrower).
3. Route `_usable_flows` through `run_batch` with a one-element request (makes single-query taint share the
tolerant path; changes the call shape upstream).
Add a regression test under the joern-gated suite: `cpg.run("taint", request_params(path_spec, …))` must
return a parsed payload (not `None`) on a fixture CPG, asserting the instrument actually executed.
+8 -8
View File
@@ -1,4 +1,4 @@
# Brief: finding-feedback-loop — what happens after a finding is judged wrong
# Brief: finding-feedback-loop, what happens after a finding is judged wrong
**Status:** brief only, unapproved. Successor scope lifted out of `contributor-scan`, which is mid-implementation
and does not cover this.
@@ -7,13 +7,13 @@ and does not cover this.
Two task groups were drafted into `contributor-scan/tasks.md` on 2026-09-09 without passing through
requirements or design. That was a workflow error: they are not refinements of scan integration, they are a
different feature — a loop that learns from findings a human rejected — and writing them as tasks skipped the
different feature (a loop that learns from findings a human rejected) and writing them as tasks skipped the
two phases that decide whether the feature is shaped right. They are lifted here to be specified properly.
## The gap this exists to close
Nine engine defects were found in one session against two repositories. **Three of them were the same bug in
three different matchers**: a spec token matched as a substring rather than as a word — `resolve` inside
three different matchers**: a spec token matched as a substring rather than as a word: `resolve` inside
`resolveUrl` in the sanitizer matcher, `user` inside `users` in the discharger matcher, `gets` inside `fgets`
in the sink matcher. Each was found by a measurement, fixed locally, and left no trace. The third survived
until a 89k-line C library made it visible as 26 identical false positives.
@@ -60,17 +60,17 @@ sound. The same machinery writing sink tables is the thing that produced the lea
## What the shape looks like
* **Per-repository, declarative, committed by the developer.** A dismissal file in the *target* repository —
site, rung, reason, author — read by the tool and never written by it, the same rule the declared policy
* **Per-repository, declarative, committed by the developer.** A dismissal file in the *target* repository (
site, rung, reason, author), read by the tool and never written by it, the same rule the declared policy
file already follows. Reviewed like code, diffable, and a dismissal without a reason is how a suppression
list becomes a graveyard nobody can audit.
* **Dismissals are labels, not suppressions.** With the known vulnerabilities a `benchmarks/repos/` recipe
already declares, a dismissal file gives a per-repository evaluation set with *both* classes in it — which
already declares, a dismissal file gives a per-repository evaluation set with *both* classes in it, which
is what an optimiser needs and what a suppression list normally throws away.
* **A ledger for the maintainer.** Counter-examples grouped by what they share (same token, same matcher
clause, same witness shape), so a recurring cause is visible as a cause rather than as five separate bugs.
* **Split-respecting adoption.** Train split only, holdout never read, the three gates byte-identical, and
precision *and* recall reported on both sides — an adoption that quietly trades recall is worse than none.
precision *and* recall reported on both sides; an adoption that quietly trades recall is worse than none.
## The acceptance test for the model's role
@@ -80,7 +80,7 @@ recover a cause we already know should not be trusted to propose one we do not.
## Known limits to carry forward
* The pair corpus could not see any of the nine defects; whole-repository measurement is what found them, and
this loop inherits that. Its evaluation set has to include repositories, not only pairs.
this loop inherits that. Its evaluation set has to include repositories as well as pairs.
* Only two repositories exist today (one Python, one C), and the C one has never produced a true positive.
An optimiser tuned on that evidence base would be tuned on very little.
* `contributor-scan` Req 8 already warns about the +16% overfitting gap between the slice a change was
+10 -10
View File
@@ -10,7 +10,7 @@ anything.
The design turns on one asymmetry: **two of those three bugs needed no judgement at all.** Once the first was
understood, the other two were the same fix carried across. So the automation that pays is *propagation and
memory*, and the model is confined to the one step that genuinely needs inference — naming what a group has in
memory*, and the model is confined to the one step that genuinely needs inference: naming what a group has in
common.
### Goals
@@ -22,7 +22,7 @@ common.
### Non-Goals
- **Automated fact-table edits.** Not deferred — prohibited. See Boundary Commitments.
- **Automated fact-table edits.** Prohibited rather than deferred. See Boundary Commitments.
- Suppressing findings silently. A dismissal makes a finding *labelled*, never invisible.
- A general-purpose optimiser. The only surfaces evolved are two prompt constants.
- Replacing the pair corpus. It is the holdout that keeps per-repository tuning honest.
@@ -38,10 +38,10 @@ common.
### Out of Boundary
- `semantic/facts`, `semantic/obligations`, `ruleset/`, the declared policy file — **no automated writer, by
- `semantic/facts`, `semantic/obligations`, `ruleset/`, the declared policy file: **no automated writer, by
any path, at any time.** This is the boundary the whole design exists to respect.
- The three gates. They are the invariant an adoption is measured against, not a thing this spec changes.
- `model/taint.py`, `dominance.py`, `config_value.py` — arbiters are fixed here; a proposal that would change
- `model/taint.py`, `dominance.py`, `config_value.py`: arbiters are fixed here; a proposal that would change
one is a proposal for a human, not an edit.
### Allowed Dependencies
@@ -64,7 +64,7 @@ Three pieces already exist and are reused rather than rebuilt:
- **`benchmarks/repos/*.toml`** declares known vulnerabilities per checkout, with `in_scope` stating whether a
family can decide one at all. That is the positive half of an evaluation set, already committed.
- **`pairs.py`** already carries a train/holdout split and the machinery that respects it. The leak of
2026-09-05 was in the *lever*, not the splitter — `semantic/loo.evaluate_loo` was honest throughout.
2026-09-05 was in the *lever*, not the splitter; `semantic/loo.evaluate_loo` was honest throughout.
- **`redaction.py`** is applied to every prompt and persisted trajectory, and applies here unchanged.
### Boundary Map
@@ -87,21 +87,21 @@ benchmarks/repos/*.toml ── read ────▶ evaluation set
**A dismissal is a label, not a suppression.** A suppressed finding disappears and its reason rots; a
labelled one is reported as dismissed with its reason, and a dismissal that stops matching is reported too.
This is what turns a suppression list into the negative half of an evaluation set — the thing that makes the
This is what turns a suppression list into the negative half of an evaluation set, the thing that makes the
rest of the feature possible.
**The tool never writes the dismissal file.** Same rule as the declared policy file, for the same reason: a
file the tool edits is a file no reviewer trusts. It is committed by a developer and reviewed like code.
**Grouping is deterministic; only the hypothesis is not.** Entries are grouped by shared spec token, matcher
clause, family and witness shape — all mechanical. The model is asked one question about a group it did not
clause, family and witness shape; all mechanical. The model is asked one question about a group it did not
choose, and its answer goes to a queue. This keeps the inference confined to the step that needs it.
**Evolution targets the question, never the facts.** A prompt is how we ask; a fact table is what we claim.
`CANDIDATE_QUESTION` and `residual_question` are already the LLM-facing surface and are the only mutable
targets. Enforced by a test that fails if any writer touches the fact modules, not by convention.
**The holdout is never read.** Not "should not" — the evaluation-set API has no path to it during
**The holdout is never read.** The evaluation-set API has no path to it during
optimisation, and a test asserts the absence. The 2026-09-05 leak was possible because reading it was merely
discouraged.
@@ -123,9 +123,9 @@ benchmarks/ledger/
### Changed
- `cli.py` — a `feedback` subcommand: `ledger`, `groups`, `propose`, `evolve`. Maintainer surface; `scan`
- `cli.py`: a `feedback` subcommand: `ledger`, `groups`, `propose`, `evolve`. Maintainer surface; `scan`
gains only the dismissal read.
- `reports.py` — a dismissed finding is rendered as dismissed with its reason.
- `reports.py`: a dismissed finding is rendered as dismissed with its reason.
### Unchanged and load-bearing
@@ -5,22 +5,22 @@
## Introduction
Nine engine defects were found in one session against two repositories. **Three were the same bug in three
matchers** — a spec token matched as a substring rather than a word: `resolve` inside `resolveUrl` in the
matchers**: a spec token matched as a substring rather than a word: `resolve` inside `resolveUrl` in the
sanitizer matcher, `user` inside `users` in the discharger matcher, `gets` inside `fgets` in the sink matcher.
Each was found by a measurement, fixed locally, and left no trace; the third survived until an 89k-line C
library made it visible as 26 identical false positives.
Nothing in the system remembers that a finding was wrong. There is no artifact saying *this site, at this
rung, is not a defect, and here is why* — so a class recurs, and each recurrence costs another measurement.
rung, is not a defect, and here is why*, so a class recurs, and each recurrence costs another measurement.
Two audiences want that artifact for different reasons. A maintainer wants the *propagation* automated: two of
the three matcher bugs needed no judgement at all, only the fix carried across. A developer wants to dismiss a
finding in their own repository, have it stay dismissed, and have the reason reviewable beside the code — a
finding in their own repository, have it stay dismissed, and have the reason reviewable beside the code; a
tool that cannot be told it is wrong is one they stop running.
This feature is bounded by a rule the project has held from the start and a leak that shows why. An optimiser
may evolve **the question** — the judge and residual prompts, which are how we ask. It may never author **the
facts** — tables, rulesets, the declared policy file, which are what we claim. On 2026-09-05 the improve lever
may evolve **the question**: the judge and residual prompts, which are how we ask. It may never author **the
facts**: tables, rulesets, the declared policy file, which are what we claim. On 2026-09-05 the improve lever
admitted candidate shapes because they "recover a currently missed holdout pair"; five of eleven were taught by
the holdout pairs they then recovered, and a holdout Youden of **−0.059 was reported as +0.059**.
@@ -38,11 +38,11 @@ recorded and my colleagues can see why.
#### Acceptance Criteria
1. The system shall read dismissals from a declarative file in the **target** repository, and shall never
write to it — the same rule the declared policy file already follows.
write to it, the same rule the declared policy file already follows.
2. A dismissal shall carry the site it concerns, the rung it was dismissed at, a reason, and an author.
3. The system shall reject a dismissal that carries no reason, because a suppression list without reasons is
a graveyard nobody can audit.
4. The system shall report a dismissed finding as dismissed, with its reason, rather than omitting it — so a
4. The system shall report a dismissed finding as dismissed, with its reason, rather than omitting it, so a
stale dismissal is visible rather than silent.
5. When a dismissal no longer matches any finding, the system shall say so, because a dismissal that has
stopped applying is either a fixed bug or a moved target and both are worth knowing.
@@ -54,20 +54,20 @@ cause is visible as a cause rather than as five separate bugs.
#### Acceptance Criteria
1. When a finding is contradicted — by a dismissal, by the judge with coverage, or by a declared contract —
1. When a finding is contradicted (by a dismissal, by the judge with coverage, or by a declared contract)
the system shall record the site, the rung, the witness, the spec tokens that produced it, and the commit.
2. Each ledger entry shall be runnable as a regression case: this site, at this commit, shall not be reported
at that rung.
3. The ledger's regression cases shall run offline in CI, so that re-introducing a fixed cause fails a test
rather than a repository scan four weeks later.
4. The system shall group ledger entries by what they share — spec token, matcher clause, family, witness
shape — and report the groups by size.
4. The system shall group ledger entries by what they share (spec token, matcher clause, family, witness
shape) and report the groups by size.
5. The ledger shall be committed evidence, not a local artifact.
### Requirement 3: A model may propose a cause, and only propose it
**Objective:** As a maintainer, I want the model to do the part that needs judgement — naming what a group of
counter-examples has in common — without it deciding anything.
**Objective:** As a maintainer, I want the model to do the part that needs judgement (naming what a group of
counter-examples has in common) without it deciding anything.
#### Acceptance Criteria
@@ -132,7 +132,7 @@ quietly loses recall is worse than none.
1. The evaluation set shall include whole repositories, not pairs alone: the pair corpus could not see any of
the nine defects that motivate this feature, and the gates stayed byte-identical through every fix.
2. The system shall state how many repositories its evidence base contains, because two — one of which has
never produced a true positive — is very little to tune against.
2. The system shall state how many repositories its evidence base contains, because two (one of which has
never produced a true positive) is very little to tune against.
3. Where an adoption is measured on one repository only, the system shall label it as such rather than
reporting it as a general improvement.
+53 -53
View File
@@ -1,10 +1,10 @@
# Brief: flow-aware-ranking — spend the expensive query where the cheap evidence is strongest
# Brief: flow-aware-ranking, spend the expensive query where the cheap evidence is strongest
> **Status 2026-10-02:** dormant. Brief only and unapproved; its 2026-09-12 amendment placed ranking inside the pre-push safety net, where the ranker now lives.
**Status:** brief only, unapproved.
## Current amendment — 2026-09-12
## Current amendment, 2026-09-12
This section supersedes forward-looking interpretations below; the iteration history is retained.
The product goal is a scalable, actionable pre-push security safety net as AI accelerates code changes.
@@ -23,7 +23,7 @@ need explicit summaries or unresolved coverage, never automatic vendor reintrodu
- Latest recorded phase-2 budgets: PMPro **82**, WP Statistics **91**, MW WP Form **54**, VAmPI **16**.
The `<50` gate remains unmet. The evidence passes recorded no unanswered pairs. These are stored
ranking measurements, not fresh detection/precision results or a pre-push latency demonstration.
- **Equal-score ties are not the whole remaining problem.** At fixed scores, perfect tie-breaking
- Equal-score ties are not the whole remaining problem. At fixed scores, perfect tie-breaking
still needs at least **68** regions for PMPro and **86** for WP Statistics (67/85 strictly
higher-scored same-tier regions precede the targets). An LLM restricted to equal-score ties
cannot pass `<50`. Within-tier reordering across scores or a new feature is a distinct experiment.
@@ -95,7 +95,7 @@ Three regions from one plugin, with what the ranker sees today and what is actua
| `pmpro_rest_api_get_membership_level` | **0.80** | none — no SQL sink at all |
A region with no sink outranks two holding raw concatenated SQL. The discriminating signal is in the sink
call's own source text — interpolated versus bound — and nothing in the ranker reads it.
call's own source text (interpolated versus bound), and nothing in the ranker reads it.
One detail decides whether a keyword rule suffices, and it does not: `esc_sql` IS in scope for the CVE's
function, applied to a different statement. "A sanitizer is present, rank it lower" demotes precisely the
@@ -122,8 +122,8 @@ finding**, it decides what is looked at, and `regions_truncated` already reports
Each phase ships only if it beats the previous one **on a repository the change was not developed against**.
A phase that cannot show that is not an improvement, it is a story.
- **Phase 0 — the oracle and the baseline.** The evidence extractor (no model) and a scoring harness over the
pinned CVEs, reporting each one's rank position. Gate: reproduces today's numbers — CVE-2023-23488 at
- Phase 0, the oracle and the baseline. The evidence extractor (no model) and a scoring harness over the
pinned CVEs, reporting each one's rank position. Gate: reproduces today's numbers, CVE-2023-23488 at
position 390 of 4,463, 83.8% of regions in one tier. Without a baseline nothing later is measurable.
**Done 2026-09-10.** `benchmarks/ranking/position.py`, committed as
@@ -138,36 +138,36 @@ A phase that cannot show that is not an improvement, it is a story.
**And this corrects the cross-assessment above.** "Ranking's job is concentration, not inclusion" was
measured on `pmpro` alone, where the CVE is inside the budget. On the other two plugins the CVE sits at
512 and 503 — **outside a 500-region budget** — so the current ranker never examines it at all, and the
512 and 503, **outside a 500-region budget**, so the current ranker never examines it at all, and the
slices found those CVEs only because a slice has too few regions to exclude anything. Both jobs are
real: inclusion on two repositories, concentration on all three. The single metric `budget_at_recall`
covers both, which is why it is the one that matters.
The instrument is `evidenceOnly` mode on the taint query, recorded on every scan as `ModelScanResult.tiers`
and `tier_counts`, and acted on by nothing — so every later phase is measured against a record made by
and `tier_counts`, and acted on by nothing, so every later phase is measured against a record made by
scans that did not know they were being measured.
**Measured cost of the instrument**, 637-file graph, the scan's own 500-region request set:
2,500 pairs in 98.9 s including JVM load — about 40 ms per pair
2,500 pairs in 98.9 s including JVM load, about 40 ms per pair
tier 0 (no sink of that family in reach): 2,161 of 2,500 = 86%
Two attempts before that ran past an hour each without producing a payload, and the cause was mine both
times: repository-wide walks — the sink scan, the reachable-method BFS, `familyInRepo` — repeated per
times: repository-wide walks (the sink scan, the reachable-method BFS, `familyInRepo`) repeated per
request. Memoising them across the batch is what made "milliseconds per pair" true rather than claimed.
**And the same memoisation reaches the dataflow mode.** The identical 250-request set that cost 2,026.3 s
(8,105 ms per request) with both joins on now costs **287.0 s — 1,148 ms per request — for the same 5,779
(8,105 ms per request) with both joins on now costs **287.0 s (1,148 ms per request) for the same 5,779
rows.** Most of what contributor-scan 5.13 measured as "the base taint query" was the per-request scans,
not the dataflow. That correction is recorded there.
- **Phase 1 — tier 0 only, no model.** Exact pruning. Gate: **53% fewer taint requests issued, measured** —
- Phase 1, tier 0 only, no model. Exact pruning. Gate: **53% fewer taint requests issued, measured**,
and nothing more. 1,177 requests at 6.63 s is still 2.2 hours, so this phase does NOT make the scan finish;
it halves the problem. Claiming completion here would send someone looking for a bug where there is none.
**Done 2026-09-10, gate exceeded, scan still does not finish — and the reason corrects phase 0's cost
**Done 2026-09-10, gate exceeded, scan still does not finish, and the reason corrects phase 0's cost
figure.** On the 637-file plugin: `tier_counts = {0: 2161, 1: 60, 2: 20, 3: 211, 4: 48}`, **2,161 of 2,500
requests pruned (86%)**, evidence pass 94.5 s. The remaining **339 requests still hit the 2,400 s ceiling**
— more than 7 s each.
requests pruned (86%)**, evidence pass 94.5 s. The remaining **339 requests still hit the 2,400 s ceiling**,
more than 7 s each.
**The scan finishes, 2026-09-11.** Not because of anything in this spec: contributor-scan 5.13 found
that every query batch had been recomputing Joern's dataflow overlay at load since PHP builds went
@@ -188,12 +188,12 @@ A phase that cannot show that is not an improvement, it is a story.
which 88% were tier 0 and returned instantly; the ~30 real requests in it cost about 9.5 s each. Pruning
removes exactly the cheap questions and keeps the expensive ones, so it cannot make those faster. The
honest per-request cost of a taint question that *can* have an answer is **7–10 s at `callDepth = 3`**,
and that is the number phase 2 and 5.13 must work against — not 1.1 s, and not 6.6 s either.
- **Phase 2 — heuristic tiers, still no model.** The vector and tiers defined below, ordered within a tier
by a published weighted sum. Gate: `budget_at_recall` — the smallest K holding every pinned CVE — **below
and that is the number phase 2 and 5.13 must work against, not 1.1 s, and not 6.6 s either.
- Phase 2, heuristic tiers, still no model. The vector and tiers defined below, ordered within a tier
by a published weighted sum. Gate: `budget_at_recall` (the smallest K holding every pinned CVE) **below
50 on every pinned repository**, and `pmpro`'s taint query completing at that K. This is the phase that
makes a scan finish, because the job is concentration: the CVE is already inside 500, the question is
whether it is inside 50. **If this passes, the model is a refinement, not the plan** — and knowing that is
whether it is inside 50. **If this passes, the model is a refinement, not the plan**, and knowing that is
worth more than assuming it.
**Implemented 2026-09-11, measurement in flight.** `Evidence.score` is the published weighted sum, and
every weight is in one table in `model/evidence.py`: per open sink +1.0 (capped at 5), per cleansed or
@@ -304,17 +304,17 @@ A phase that cannot show that is not an improvement, it is a story.
facts and a weight table can do; the ties are phase 3's. After that, within-tier order is phase 3's -- a model reading the code the ties cannot --
or one more exact path fact (`distance`), and the gate stays unmet until one of them moves it.
- **Phase 3 — the LLM ranker.** Same evidence, ordered by a model. Gate: beats phase 2 leave-one-repository-out.
- Phase 3, the LLM ranker. Same evidence, ordered by a model. Gate: beats phase 2 leave-one-repository-out.
If it does not, phase 2 ships and phase 3 does not.
- **Phase 4 — break the closed loop.** An exploration slice (~10% of budget sampled outside the top-K) and
- Phase 4, break the closed loop. An exploration slice (~10% of budget sampled outside the top-K) and
full labelling on repositories small enough to arbitrate every region. Gate: a measured estimate of the
selection bias in phase 3's numbers.
- **Phase 5 — prompt optimisation (GEPA / SIMBA / DSPy).** Only after 3 and 4. An optimiser run against a
- Phase 5, prompt optimisation (GEPA / SIMBA / DSPy). Only after 3 and 4. An optimiser run against a
censored metric optimises the censorship.
## What the prompt has to carry
The question put to the model is **not** "is this vulnerable" — that is adjudication, it is the arbiter's job,
The question put to the model is **not** "is this vulnerable". That is adjudication, the arbiter's job,
and it is the one thing a ranker must never be asked. It is:
> Given a fixed budget of expensive dataflow queries, which of these sites are worth spending it on?
@@ -326,14 +326,14 @@ reachable from a request handler.
## Constraints, from measurement and from prior scars
- **Cost.** One model call per region is 4,463 calls a scan. Batch over a prefilter, or it does not ship.
- **The loop is closed.** A region the ranker excludes is never arbitrated, never labelled, so the ranker
- Cost. One model call per region is 4,463 calls a scan. Batch over a prefilter, or it does not ship.
- The loop is closed. A region the ranker excludes is never arbitrated, never labelled, so the ranker
never learns it was wrong. Every run confirms the ordering it already had.
- **N projects.** Four pinned CVEs is not a training set. This project already has the closed-loop
- N projects. Four pinned CVEs is not a training set. This project already has the closed-loop
train-on-test scar; the reported figure must come from a repository the optimiser never saw.
- **Determinism.** Two runs of one repository must agree or a baseline diff is meaningless. The ranking is
- Determinism. Two runs of one repository must agree or a baseline diff is meaningless. The ranking is
recorded in the manifest.
- **Degradation.** With no model configured, phases 0–2 still rank, and the report says which ranker ordered
- Degradation. With no model configured, phases 0–2 still rank, and the report says which ranker ordered
the scan.
## Cross-assessment against the measurements
@@ -342,7 +342,7 @@ Read against what was actually measured today, the plan above has two errors and
here, corrected, rather than silently rewritten, because the corrections are themselves results.
**Phase 1's gate is wrong.** It says pruning makes `pmpro`'s taint query complete. It does not. Pruning
removes 53% of requests, and 1,177 × 6.63 s is still **2.2 hours** — nowhere near the 300 s default or the
removes 53% of requests, and 1,177 × 6.63 s is still **2.2 hours**, nowhere near the 300 s default or the
2,400 s that was tried. Pruning halves the problem; it does not solve it. The honest gate is "53% fewer
requests issued, measured", and completion belongs to phase 2. Stating otherwise would have sent someone
looking for a bug in phase 1 when phase 1 had done exactly what it can.
@@ -356,8 +356,8 @@ the numbers it had; the numbers were the instrument's.
**Ranking's job is concentration, not inclusion.** The brief implies the CVE is missed because the ranker
excluded it. It did not: CVE-2023-23488's region sits at position 390 of 4,463, inside the 500-region budget,
and was examined. It goes unreported because the budget is spent on 500 regions when 50 would do, and 500
is unaffordable at 6.63 s per family per region. So the ranker's success metric is not "is the CVE in the
budget" — it already is — but **"how small can the budget be while the CVE stays in it"**. That is a
is unaffordable at 6.63 s per family per region. So the ranker's success metric is
**"how small can the budget be while the CVE stays in it"**; the CVE is already in the budget. That is a
different objective, and it is the one that makes a scan finish.
**The gap: nothing above says what the evidence IS.** "Sink shape" and "cleansing on the call" are named,
@@ -366,24 +366,24 @@ check for language leakage. The rest of this brief defines it.
Two things the measurements confirm and the plan should lean on harder:
- **The joins are cheap and must survive any ranker.** 22% of query cost, two of three CVEs. A ranker that
- The joins are cheap and must survive any ranker. 22% of query cost, two of three CVEs. A ranker that
demotes field- or hook-carried regions because they show no direct source is a ranker that loses those
CVEs. The evidence vector below therefore carries the join's own stage-one result as a field.
- **84% of regions in one tier means any separation is progress**, and the separation does not need to be
- 84% of regions in one tier means any separation is progress, and the separation does not need to be
clever to be large. A tier 0 that is merely exact already reorders the budget more than the current ranker
ever has.
## The evidence vector, defined
The unit is a **(region, family) pair**. For each, a fixed vector of fields is computed. Every field is
defined in terms of **fact-table categories** — source, sink, sanitizer, dispatch, access — never in terms of
defined in terms of **fact-table categories** (source, sink, sanitizer, dispatch, access), never in terms of
a language's syntax, which is what makes the definition language-agnostic: any language with a fact table
gets the same vector, and a heuristic that mentions `$wpdb` or `wp_ajax_` has by construction left the
vector and must be rejected in review.
**Where it is computed matters as much as what it is.** The graph is already built for the arbiter, at 76 s
for 637 files. Extracting these fields is a CPG query **without dataflow** — call nodes, their arguments,
their enclosing methods, the call graph one hop out — which is milliseconds per call rather than 6.63 s per
for 637 files. Extracting these fields is a CPG query **without dataflow** (call nodes, their arguments,
their enclosing methods, the call graph one hop out), which is milliseconds per call rather than 6.63 s per
request. The extractor is therefore a sibling of `taint.sc` that never calls `reachableByFlows`:
> **Tiering is the CPG query without dataflow. Arbitration is the CPG query with it. Same graph, same
@@ -393,7 +393,7 @@ That is the whole architecture in one line, and it is why the evidence is langua
merely language-neutral-in-intent: the CPG normalises syntax across frontends, so "the argument of this call
is a literal" means the same thing in PHP, Python and C.
### Sink evidence — from `[[sink]]` facts of family F
### Sink evidence, from `[[sink]]` facts of family F
| field | definition | exactness |
|---|---|---|
@@ -401,7 +401,7 @@ is a literal" means the same thing in PHP, Python and C.
| `shape` | per sink call, one of: **`literal`** (the vulnerable-position argument is a constant), **`bound`** (the call is a declared-safe form — a `parameterized` sanitizer fact wraps it, or its arity/shape matches the bound form the taint query already recognises), **`computed`** (the argument is an expression: concatenation, interpolation, a variable, a call result) | exact for `literal` and `bound`; `computed` is the residue |
| `cleansed_on_call` | per sink call, whether a sanitizer of F is an **ancestor of the vulnerable-position argument in the AST** — not merely present in the same function | exact, and this is the field a text scan gets wrong: `esc_sql` in scope on a different statement is `false` here |
### Source evidence — from `[[source]]` facts and the entry-point contract
### Source evidence, from `[[source]]` facts and the entry-point contract
| field | definition | exactness |
|---|---|---|
@@ -411,7 +411,7 @@ is a literal" means the same thing in PHP, Python and C.
| `access` | the declared access level, **with provenance**: `declared` (a registration or route contract says so) or `inferred` (nothing found) — because "public" meaning "no decorator found" is the false positive the project has already paid for | exact |
| `carried` | the region reads a **field** whose assignment is fed by a source anywhere in the file, or a **dispatch** value whose registered producer is — i.e. stage one of the two-stage join, which is already computed and memoised, reused here as evidence | exact where computed; absent (not false) where the join was not run |
### Path evidence — cheap structural proxies for what dataflow would establish
### Path evidence, cheap structural proxies for what dataflow would establish
| field | definition |
|---|---|
@@ -438,14 +438,14 @@ is a literal" means the same thing in PHP, Python and C.
Two properties are load-bearing and must hold in every implementation:
1. **Only tier 0 excludes; every other tier orders.** So the only place exactness is *required* is tier 0,
1. Only tier 0 excludes; every other tier orders. So the only place exactness is *required* is tier 0,
and tier 0's predicate has a proof: a family with no sink in reach cannot produce a flow, in any language,
under any dataflow model. Above tier 0, being wrong costs position, and `regions_truncated` reports what
position cost.
2. **Tier 0's scope must match the arbiter's.** At `callDepth = 0` the scope is the region; at depth k it is
2. Tier 0's scope must match the arbiter's. At `callDepth = 0` the scope is the region; at depth k it is
the region plus its reachable methods. A tier 0 computed on the file when the arbiter will look k hops
out is not exact and must not be called so. The repository-wide form (`family_in_repo == false`) is exact
at every depth and is the cheap first cut — it alone removes all 487 `deserialization` requests on `pmpro`.
at every depth and is the cheap first cut. It alone removes all 487 `deserialization` requests on `pmpro`.
**Within a tier**, order by a score. In phase 2 that score is a fixed weighted sum over the vector, published
in the manifest so two runs agree. In phase 3 it is the model's ordering **of the same vector**, which is
@@ -456,23 +456,23 @@ what makes the two comparable: same input, different ordering function, one held
The vector above is exact wherever exactness was cheap. What remains genuinely uncertain is narrower than
"ranking":
- **Ambiguous cleansing.** `cleansed_on_call` is exact when the sanitizer wraps the argument directly. It is
`false` — correctly, but unhelpfully — when cleansing happens two statements earlier into a variable that is
- Ambiguous cleansing. `cleansed_on_call` is exact when the sanitizer wraps the argument directly. It is
`false` (correctly, but unhelpfully) when cleansing happens two statements earlier into a variable that is
then interpolated. Deciding whether *that* cleansing reaches *this* call is reading the code, and it is the
single largest source of tier-2/tier-3 misplacement.
- **Unfamiliar shapes.** A sink call whose form the `bound` rule does not recognise, in a framework with no
- Unfamiliar shapes. A sink call whose form the `bound` rule does not recognise, in a framework with no
facts yet. The model can say "this looks bound" as an *ordering* opinion; it cannot add a `bound` fact,
because a fact is policy.
- **Within-tier priority** when the fixed score ties, which on a real repository it will, constantly.
- Within-tier priority when the fixed score ties, which on a real repository it will, constantly.
And the boundaries:
- **The model never computes tier 0.** Exclusion is exact or it is a silent miss, and a model's "no sink
- The model never computes tier 0. Exclusion is exact or it is a silent miss, and a model's "no sink
here" is not a proof.
- **The model never sees the question "is this vulnerable".** It sees "which of these is worth an expensive
- The model never sees the question "is this vulnerable". It sees "which of these is worth an expensive
query", with the vector and the sink call's source text. Adjudication is the arbiter's, and keeping it
there is what keeps the gates byte-identical.
- **The model's input is the vector plus text, never the arbiter's verdict** on the same run — that is the
- The model's input is the vector plus text, never the arbiter's verdict on the same run. That is the
closed loop feeding on itself.
## Metrics, so every phase can fail
@@ -498,8 +498,8 @@ answer is visible:
1. ~~Where does the evidence extractor live?~~ **It is a CPG query without dataflow, a sibling of `taint.sc`
in `cpg/queries/`.** Not the mapper: the mapper reads text and would have to re-derive per language what
the graph already normalises. The vector is language-agnostic *because* the graph is.
2. ~~Per file or per reachable set?~~ **Both, in that order.** `family_in_repo` first — exact at any depth,
free, a fifth of the budget on `pmpro` — then the reachable set at the arbiter's own `callDepth`. Never the
2. ~~Per file or per reachable set?~~ **Both, in that order.** `family_in_repo` first (exact at any depth,
free, a fifth of the budget on `pmpro`), then the reachable set at the arbiter's own `callDepth`. Never the
file alone above depth zero, because that is not exact and must not be called tier 0.
3. How many pinned repositories before leave-one-out means anything? Four is not enough. This is the
corpus ask, and it is the same ask contributor-scan 5.8 made: plugins chosen by installed base, CVEs read
@@ -507,7 +507,7 @@ answer is visible:
4. Does the arbiter's verdict feed back as a label? **Only through the exploration slice.** A verdict on a
region the ranker chose is a label the policy selected, and training on it closes the loop. A verdict on
an exploration region is the only unbiased label the scan produces, and it is the one worth keeping.
5. **New:** the weighted sum in phase 2 has weights. Where do they come from before any model exists?
5. New: the weighted sum in phase 2 has weights. Where do they come from before any model exists?
Proposed: set by hand from the tier semantics, published in the manifest, and treated as a baseline for
phase 3 to beat — not tuned, because tuning four weights on four CVEs is the overfit the plan exists to
phase 3 to beat, not tuned, because tuning four weights on four CVEs is the overfit the plan exists to
avoid.
-30
View File
@@ -1,30 +0,0 @@
# Brief: remove HarnessX after the plane proves value
Status: brief only, gated. Not started until `ai-service-plane` Requirement 6 records a met gate.
## Inventory (read-only, 2026-09-29)
- Code: `src/openultrasast/harness_ext.py` (97 lines) and `hunter_harness.py` (197) exist only for HarnessX;
guarded branches in `cli.py` (372-512), `fusion.py` (LLM panels, ~100 lines), `verify_judge.py` (judge path,
~90), `stage_processors.py` (`host_under_harnessx`, ~40), `config.py` (`HarnessxConfig`). About 575 of
30,243 lines (1.9%). Every path falls back deterministically when the extra is absent and records a
degradation (`harnessx_extra_unavailable`; fallbacks `run_hunter_pool`, `structural_verifier`,
`deterministic_panels`). `evolve`, `regress`, `benchmark`, `gate` and quick mode do not depend on it.
- Comment-only mentions (re-verified 2026-09-30): `gate.py:174`, `improve/evolve.py:9`, `slot_contract.py:7-13`.
- Packaging: `pyproject.toml` extra `harnessx` pinned to a git commit; mypy override; `uv.lock` entry.
- Tests: 14 files touch it; dedicated `test_harness_ext.py`, `test_hunter_harness.py`, `test_cli_hx_dispatch.py`,
`test_verify_judge.py`; `tests/conftest.py` fixture `assert_cold_of_harnessx`.
- Docs: README sections "Configuring the HarnessX agentic plane" and "The HarnessX self-improving cycle";
`docs/examples.md` section 6; `docs/threat-model.md` egress and spend caps.
- Specs: `harnessx-self-improving-rulesets` (approved, 43/43 done, Requirement 1 is direct adoption);
`learning-harness` (30/33); `three-stage-scan` framed as its follow-up; `openrouter-sast-harness` (no
spec.json); mentions in four more; steering `overview.md:175`.
## Shape of the removal
1. Replace the three guarded paths with plane tasks (hunter pool -> `verify`; judge -> `judge`; panels ->
`judge`/`agree`) so their fallbacks become the only path, then delete the two modules, the config block,
the extra, the override and the fixture.
2. Retire the four specs' HarnessX requirements with a dated note, not a rewrite; update README and docs.
3. Gate: the plane increment met (recall and cost), and the replaced features measured no worse on the
development pins before deletion.
-507
View File
@@ -1,507 +0,0 @@
# Design Document
## Overview
HarnessX leaves the product in one reviewable sequence, and the plane's memory layer takes over what HarnessX was
meant to drive. Three facts from the code shape the design:
- **No HarnessX capability is measured, and one of them never existed.** The LLM paths are reached only through
config keys, not CLI flags (`scan` has `path`, `--mode`, `--config`, `--fail-on`; `cli.py:163-167`), and
`benchmarks/` holds no run of any of them (no `hx-hunter:` finding, no `llm-panels` decision). The
`MetaAgent.evolve` proposer is only a docstring (`improve/evolve.py:8-9`, `harness_ext.py:7-8`). No code calls it.
- **The plane already does the LLM work better, and outside `ousast scan`.** `verify` (two passes, known callers
from `repo-facts`) plus `agree` (2-of-3 with the tie-break) met the ai-service-plane gate
(`benchmarks/independent/plane-increment-2.json`: 16/20 declared sites agreed, $0.0205 per candidate). The
standard-mode LLM hunter that users actually reach, the tool hunter at the MAP stage (`cli.py:631-640`,
`tool_hunter.resolve_hunter_client`), is not HarnessX and stays.
- **Everything deterministic already runs without the extra.** Every guarded branch falls back to a deterministic
function (`run_hunter_pool`, `verify_findings`, `fuse_finding`). Removal therefore makes the fallback the only
path. Quick mode, `benchmark`, `pairs`, `improve` and the gates never enter a guarded branch.
So every HarnessX capability in `ousast scan` is **retired** with its deterministic path kept. The plane's `verify`
and `agree` tasks are the documented agentic path (`ousast plane run <Run>`). The proposer is **carried by the
memory layer**: a persistent store, keyed by repository and pin, that plane Runs write through a model-free
`remember` task. From that store a deterministic generator emits `RuleStatusEdit` proposals into the unchanged
`EvolveValidator` and gate. The improvement loop becomes a plane Run whose only Model-bound tasks are the verify
passes.
## Architecture
```
Before After
ousast scan --mode standard ousast scan --mode standard
hunter_pool: HxScanOrchestrator | run_hunter_pool hunter_pool: run_hunter_pool (only path)
verify: verify_judge (llm-judge) | verify_findings verify: verification.verify_findings
fusion: llm panels | deterministic panels fusion: deterministic panels (fuse_findings)
MAP: tool hunter ([models] hunter, DeepSeek) MAP: tool hunter (unchanged)
harness_ext.py, hunter_harness.py, verify_judge.py deleted
stage_processors.host_under_harnessx deleted; deterministic pipeline kept
[harnessx] config, extra, mypy override, lock one load warning; extra/override/lock gone
improve: benchmark per-rule proposer (+ MetaAgent improve: benchmark proposer + memory proposer (--memory)
"plugs in" -- never implemented)
plane Run (per case)
facts -> va, vb -> agree -> vc -> final (existing)
alerts (new, no model) ------------------+
remember (new, no model) <- facts, final, alerts, va/vb/vc
plane Run (loop tail, once)
measure -> propose -> improve(validate+gate) (no model)
~/ousast-results/plane/memory/ the store: facts by content, rows by repo + pin
```
New code: `src/openultrasast/plane/memory.py` (store, ingest, seed; host side),
`src/openultrasast/plane/tasks/{alerts,remember,measure,propose,improve}.py`,
`src/openultrasast/improve/memory.py` (deterministic proposal rules). None of it is in the reconciler. The
reconciler gains only `mark_done` (below). It is 465 lines today, and `tests/test_plane_reconciler.py:759-761`
keeps it under 500.
## Components and Interfaces
### 1. Fate of each HarnessX capability (Requirement 1)
| Capability | Where today | Fate | What the user loses |
| --- | --- | --- | --- |
| LLM hunter pool (`HxScanOrchestrator`) | `hunter_harness.py:49-185`; gated in `cli.py:400-456` (`hx_hunter` at :405, orchestrator at :438-448, degradation at :452-455) | **Retired.** `run_hunter_pool` becomes the only standard-mode hunter pool. LLM hunting of a repository stays available in two places: the MAP-stage tool hunter in `ousast scan --mode standard/deep` with `[models] hunter` (`cli.py:631-640`, not HarnessX), and plane `verify` via `ousast plane run plane/runs/validation-46.yaml` | `hx-hunter:<path>:<line>` findings from an anthropic/openai/litellm agent (`hunter_harness.py:155`). No run of this path is recorded under `benchmarks/`, so no measured TP is lost. |
| LLM judge | `verify_judge.py:22-63`; gated in `cli.py:406, 499-511` | **Retired.** `verification.verify_findings` is called directly and `verify_judge.py` is deleted (it is only the dispatcher and the judge). The plane's LLM verification is `verify` + `agree` (+ `vc`/`final` tie-break) | Per-finding `plausible/unsupported` verdicts that set `ACCEPTED/REJECTED` (`verify_judge.py:83-88`). Unmeasured. `[models] judge` (model-grounded-detection, `model/endpoint.py:195`) is a different capability and stays. |
| LLM fusion panels | `fusion.py:194-326` (`_fuse_with_llm_panels`, `_run_llm_panels`); gated in `cli.py:519-541` | **Retired.** The deterministic panels (`_panel_a`, `_panel_b`, `_reconcile`, `fusion.py:101-167`) become the only path. `fuse_findings_dispatch` is renamed `fuse_findings(findings, verifications, *, high_assurance, mitigated_ids)`. The plane counterpart of independent cross-examination is `agree` across independent passes with the 2-of-3 tie-break | Steel-man LLM panels (`decision_source: "llm-panels"`). Unmeasured. `fusion.json` for deterministic decisions is unchanged. |
| Deterministic stages hosted under HarnessX | `stage_processors.py:129-156` (`host_under_harnessx`); only caller is the skipped test `test_slot_contract.py:119-133` | **Retired, no replacement.** ax runs whole tasks, so a model-disabled in-loop host has nothing to host. `stage_processors`/`slot_contract` stay as the standalone deterministic pipeline that `test_gate.py:147-148` pins | Nothing. The function has no production caller. |
| LLM evolution proposer (`MetaAgent.evolve`) | docstring only: `improve/evolve.py:8-9`, `harness_ext.py:7-8` | **Carried by the memory layer** (Requirement 6): `improve/memory.py` rules over the store, fed into `run_round` as extra proposals | Nothing implemented is lost. |
No CLI flag is HarnessX-specific, so Requirement 1.3 applies to config keys. A key that asked for a retired
capability makes `load_config` raise `RetiredConfigError(ValueError)`, and the CLI prints the message and exits 2.
A silent downgrade is not allowed:
| Key | Message (abridged) |
| --- | --- |
| `[models] verifier` | "`[models] verifier` was retired with HarnessX on <date>: LLM verification runs on the plane (`verify` + `agree`, `ousast plane run`, see ops/ax/README.md). Remove the key." |
| `[fusion] panel_model`, `[fusion] decider_model` | "... LLM fusion panels were retired; fusion is deterministic; independent LLM agreement is the plane's `agree` task ..." |
`[harnessx]` is different (Requirement 3.3). It only tuned budgets and never requested a capability, so it loads
with **one** `logging.warning` on logger `openultrasast.config` that names the plane ("`[harnessx]` is ignored:
HarnessX was removed; agentic work runs on the ax plane with per-task budgets, see ops/ax/README.md"). The section
is otherwise ignored. `[models] hunter` keeps its meaning (tool hunter, obligations intent adjudication at
`cli.py:1237`), so it gets no message. With the extra installed it no longer starts a second, HarnessX hunter. The
release notes say so.
Requirement 1.2 (moved capabilities have a CLI path, a Model per task and a fixture test) is met by the existing
`verify`/`agree` tasks (`ousast plane run`; Model `deepseek-flash` bound per Task; `tests/test_plane_verify.py`,
`tests/test_plane_scoring_parity.py`, `tests/test_plane_tiebreak.py`), and by the new memory and loop tasks below, each with its own fixture test. No
capability is moved into a new Model-bound task.
### 2. Removal mechanics (Requirement 2)
**Source.**
- Delete `harness_ext.py` (97 lines), `hunter_harness.py` (197) and `verify_judge.py` (130).
- `cli.py`: drop the imports at :44, :46 and :94. Drop `harnessx_present`, `hx_hunter` and `hx_verify` (:400-406),
the orchestrator branch and its degradation (:438-456), and the verify degradation (:499-502). Call
`verify_findings(findings)` at :503-511. Drop `hx_fusion` and its degradation (:521-527). Call
`fuse_findings(findings, verifications, high_assurance=config.fusion.high_assurance)`. The `verifier_model`
local goes away.
- `fusion.py`: drop the import at :24, the `panel_model`/`decider_model`/`provider`/`use_harnessx` parameters, and
`_with_degradation`, `_fuse_with_llm_panels`, `_run_llm_panels` and `_text` (:229-338). Rewrite the module
docstring (:10-14) and update `__all__`.
- `stage_processors.py`: drop `host_under_harnessx` (:129-156) and its `__all__` entry, and rewrite the module
docstring (:10-11). `slot_contract.py:7-14` becomes "the zero-dependency substrate for the deterministic
pipeline".
- `config.py`: drop `HarnessxConfig` (:99-107), `ResolvedConfig.harnessx` (:201), `_load_harnessx` (:345-351) and
its call (:230). Drop `ModelConfig.verifier` (:37), `FusionConfig.panel_model/decider_model` (:123-124) and
their loaders (:266, :366-367). Add `RETIRED_KEYS` and the one-warning rule above. `harness.py:183` keeps the
`"verifier": None` literal in `model_roles`, so `harness.json` keeps its schema byte for byte (the key has always
been `null` in the baseline config).
- Comment-only mentions: `gate.py:174` ("a model-backed stage"), and `improve/evolve.py:8-9`, which now names the
memory proposer.
- `plane/Dockerfile.runner:23` loses "no HarnessX".
**Packaging.**
- Delete the `harnessx` extra and its comment (`pyproject.toml:14-22`) and the `harnessx.*` mypy override and its
comment (:67-71).
- `uv lock` regenerates `uv.lock`. The `harnessx` package (:1062), the extra (:2145-2146), the requirement
(:2169) and `provides-extras` (:2178) go.
- Extras that remain: `semantic`. There is no `all` extra. `pip install .` and `pip install '.[semantic]'` in a
fresh venv are the Requirement 2.2 check.
- No source or test imports a HarnessX transitive dependency. The only third-party imports are `yaml`,
`tree_sitter` and `pytest` (checked with an import scan), so dropping the lock entry cannot break an import.
**Tests** (14 files plus `conftest.py`):
| File | Action |
| --- | --- |
| `test_harness_ext.py`, `test_hunter_harness.py`, `test_verify_judge.py`, `test_cli_hx_dispatch.py` | **Delete.** Each exercises only HarnessX or its dispatcher. |
| `conftest.py` | Rename fixture `assert_cold_of_harnessx` to `assert_cold_import(code)`: a fresh interpreter runs `code`, with no module-name guard. `test_semantic_extra.py:28-35` is its remaining user (the tree-sitter check brings its own assertion). |
| `test_gate.py` | Delete `test_harnessx_stage_stays_within_tolerance_of_baseline` (:124-150) and the `has_harnessx` import (:23). Replace the two "never import" tests (:155-175) with `test_governance_planes_import_only_stdlib_and_declared_dependencies`, an allow-list (stdlib, `openultrasast`, `yaml`) over module-level imports of `GOVERNANCE_SCORING_BENCHMARK_PLANES`. It covers any extra without naming one. Update the docstring at :5-6. |
| `test_scan_pipeline_integrity.py` | Rename `test_standard_scan_extra_absent_matches_deterministic_baseline` to `test_standard_scan_with_a_hunter_model_keeps_deterministic_findings` (config `hunter` only). Rename `test_quick_scan_is_unaffected_by_the_extra` to `test_quick_scan_records_no_degradation`. Drop the `has_harnessx` monkeypatch and the `harnessx` parameter. Recompute the expected degradation set from the post-removal run, since `hunter_pool`/`verify` entries no longer exist. |
| `test_slot_contract.py` | Delete `test_host_under_harnessx_disables_the_model` and the imports at :16 and :22. Keep the rest. |
| `test_config.py` | Replace `test_harnessx_defaults_when_section_absent` and `test_load_config_reads_harnessx_section` with `test_retired_agentic_section_loads_with_one_warning` (caplog: exactly one record naming the plane) and `test_retired_llm_keys_fail_naming_the_replacement` (verifier, panel_model, decider_model). |
| `test_fusion.py` | Delete `test_dispatch_records_degradation_when_llm_requested_but_extra_absent` (:142-146) and the import at :16. Rename the dispatch calls to `fuse_findings`. |
| `test_tool_hunter.py` (:229, :266), `test_scan_deep.py` (:144) | Drop the `has_harnessx` monkeypatch line. |
| `test_semantic_extra.py` | Use `assert_cold_import`. |
| `test_module_audit.py` | The subsystem tuple at :31 becomes `("fusion", "mcp", "skills")`. A new assertion checks that the three deleted modules are absent from the audit. |
**Module audit.** `model/audit.py:76-77` (`harness_ext`, `hunter_harness`) are deleted, and `:78`
(`stage_processors`) is reworded to "deterministic slot-contracted scan stages; pinned by the zero-dependency
guard in test_gate". Regenerate `benchmarks/measurements/2026-09-08-module-audit.json` in place with `to_dict(audit(SRC))`.
`test_the_manifest_matches_the_tree` pins that path. The diff shows three rows removed and no orphan.
**Search (Requirement 2.4).** `grep -rni harnessx src tests pyproject.toml README.md` may match only lines tagged
`retired 2026-..`:
- the `RETIRED_KEYS` entry and warning text in `config.py`
- the two retirement tests in `test_config.py`
- the one README line in the plane section
Requirement 3.3 cannot be implemented or tested without naming the section, so these lines count as retirement
notes. The check is a test (`test_no_harnessx_outside_retirement_notes`) that fails on any untagged match. It
builds its pattern at runtime, so it does not match itself.
### 3. Equality proof (Requirement 3)
The spec's "`ousast regress`" is not a subcommand (`cli.py:133-251`). Regress exists only as the deep-mode
sandbox stage. On this project the regression command over the development pins is `ousast pairs`
(README.md:38): the vuln-vs-fixed replay over local fixtures and vendored pinned VFC pairs, offline by default.
The benchmark replay is README.md:35's manifest. The baseline also covers the two paths the removal touches
directly: one standard-mode scan (verify and fusion) and one `improve --dry-run` (evolve). Everything is
quick or standard mode with no Joern on the host, so each command takes minutes, not hours, on this 7 GB host.
**Environment (instrument first).**
- Run from a `git archive HEAD` export in the scratchpad (the source is frozen, per the long-runs rule).
- `uv sync --frozen --extra semantic`, which drops the installed `harnessx`. It is installed in `.venv` today.
- `OPENULTRASAST_SKIP_DOTENV=1` and `env -u DEEPSEEK_API_KEY -u OPENROUTER_API_KEY -u ANTHROPIC_API_KEY -u OPENAI_API_KEY`,
plus an empty `--config`, so no command can reach a model.
The harness asserts `find_spec("harnessx") is None`, `find_spec("tree_sitter") is not None`, and a
`uv pip freeze` hash. It also prints the byte size of each input manifest, and fails on any non-zero exit code.
**Commands** (one script, `benchmarks/harnessx_removal_equality.py record|compare <dir>`):
1. `pytest -q -p no:cacheprovider --junitxml=suite.xml`, which gives outcome per test id.
2. `ousast pairs --json > pairs.json`. Instrument check: pair count > 0 and at least one TP.
3. `ousast benchmark benchmarks/manifests/python-vulnerable.toml --mode quick`. Keep
`benchmark_result.json`, `calibration_records.json` and `external_baseline_deltas.json`. Instrument check:
`expected > 0`.
4. `ousast scan benchmarks/fixtures/python-vulnerable --mode standard --config empty.toml`. Keep
`findings.json`, `verification.json`, `fusion.json`, `report.sarif`, `harness.json` and `manifest.json`.
5. `ousast improve benchmarks/manifests/java-spring-boot-vulnerable.toml --dry-run --no-pair-gate`, stdout plus
the dry-run journal.
**Stored.** `benchmarks/measurements/<date>-harnessx-removal-baseline/`: the normalized outputs, and `env.json`
(commit, freeze hash, instrument checks). The script's exclusion list is written into that directory's
`README` line.
**Comparison.** JSON is parsed, normalized and re-serialized with sorted keys. Everything else is compared as
raw bytes. Excluded:
- keys `runtime_seconds`, `scan_id`, `timestamp`, `started`, `finished`, `created_at`, `generated_at`, and any
key ending `_seconds` or `_ms`
- run directory names (timestamped, `benchmark.py:186`)
- `trace/events.jsonl` as a whole (timings)
- the export's absolute root path, replaced by `<ROOT>`
For the suite, every test id that survives must keep its outcome. Deleted and renamed ids are compared through
the map in the Tests table above, and any other difference is a failure. Any difference in 2-5 blocks the merge.
### 4. Memory layer (Requirement 6.1, 6.4)
**Store** (`plane/memory.py`). The store lives at `results_root()/memory/` (`~/ousast-results/plane/memory/`,
honouring `OUSAST_RESULTS`, `reconciler.py:58-59`). It is JSON files, not SQLite: the rest of the plane is JSON,
diffable and greppable, and one run at a time holds the run lock, so SQLite's concurrency buys nothing.
```
memory/
index.jsonl one row per ingested (run, task): sha256 of its memory.jsonl, row count
facts/<sha256>.json facts.json by content (identical facts stored once)
repos/<host>__<owner>__<name>/<pin>.jsonl the rows of one repository + 40-hex pin
```
Ingest is idempotent: an `index.jsonl` hit skips the delivery, and rows carry a deterministic `id`. Ingest also
refuses to write when `shutil.disk_usage` reports less than 1 GiB free. The disk is at 98% today, so it names
the store path in the error.
**Storage backends (maintainer, 2026-09-30: asked to use the object-store features of the S3 server he runs,
including its JSON support).** The store is written against one interface, `MemoryStore` (`put_row`, `put_facts`, `get_facts`,
`rows(repo=..., pin=..., kind=..., where=...)`, `index`), with two backends of identical behaviour:
- `FileStore`: the layout above, for this laptop and the unit tests.
- `S3Store`: an S3-compatible bucket (boto3; RustFS is the tested server), and the store of record once the plane runs on a separate
Kubernetes cluster. The layout maps onto one bucket with the same key paths:
- **Object metadata and tags** on every row object: `repo`, `pin`, `kind`, `family`, `run`, `population`,
`split`. Rules list by prefix (`repos/<repo>/`) and filter by tag instead of scanning the bucket.
- **Server-side JSON queries**: `rows(where=...)` pushes the filter down with S3 Select
(`SelectObjectContent`, JSON Lines input, JSON output), so a proposal rule reads matching rows, not whole
files. Support is probed once per process against the server (a small Select on a known object); when the
server lacks it, the backend fetches and filters locally, logs the
fallback once, and returns the same rows. A query never silently returns nothing because a feature is
missing.
- **Versioning** on the bucket: each proposal's provenance records the object key and version id of every
memory row it cites, so its evidence cannot change underneath it (Req 6.4). Object lock (governance mode) is
optional for the `proposals/` prefix.
- **Lifecycle rules**: raw run outputs under `runs/` expire after a configurable number of days; `repos/`,
`facts/` and `proposals/` are kept. The near-full laptop disk stops being the store's limit.
- **Presigned URLs** (the step that removes the host receiver for the Kubernetes move, not built in this
spec): the reconciler can hand a task presigned PUT URLs for its outputs and GET URLs for its inputs in the
start request, so tasks write to the bucket directly and never hold credentials. The interface keeps
`presign_put`/`presign_get` so that follow-on changes the reconciler, not the store.
- **Bucket notifications** (follow-on): an event on new `repos/` rows can trigger the loop Run
(event-driven loop engineering).
- **Configuration**: `OUSAST_MEMORY` selects the backend (`file:///path` or `s3://<bucket>[/<prefix>]`); the S3
endpoint and credentials come from `.env` (`S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
optional `S3_REGION`), never from a manifest, never printed. Client: boto3 as an optional extra
(`openultrasast[s3]`); the core install stays PyYAML-only.
- **Tests**: the same contract tests run against `FileStore` and against `S3Store` pointed at an S3-compatible
endpoint (skipped unless `OUSAST_MEMORY_TEST_S3` is set), including the Select probe with its fallback
forced, versioned provenance, and metadata filters. The disk-space refusal applies to `FileStore` only.
**`remember` task** (`plane/tasks/remember.py`, no Model, budget `{usd: 0, calls: 0}`, one per case). It reads the
case's delivered artifacts as inputs: `facts.json`, the `units.jsonl` of `va`/`vb`/`vc`, `final/agreed.json` and
`final/disputed.json` with the per-candidate rows, and `alerts.jsonl`. It takes the repository URL and pin from
the bound Workspace (`AX_WORKSPACES_YAML`, `OUSAST_GIT_PINS`) and the population from the Run annotation
`openultrasast.io/population`. It writes `memory.jsonl` and passes `facts.json` through. Task code interprets
the artifacts, so the reconciler and runner stay content-blind (ai-service-plane Req 3.4). After `reconciler.run`
returns, `cli._plane` calls `memory.ingest(run_dir)`. `ousast plane remember <run>` does the same by hand.
**Row kinds** (every row has `id`, `kind`, `repo`, `pin`, `run`, `task`, `population`, `split`, `image`):
- `facts`: `sha256`, `candidates_digest` (`repo_facts._digest`, `repo_facts.py:188`), `files`.
- `verdict`: `candidate` (`path::function`), `family`, `site`, `passes {a, b, c}` (true/false/null), `final`
(`agreed | rejected | disputed`), `tiebreak` (bool), `site_match` (bool/null), `usd`, `turns`.
- `unit_cost`: `pass`, `path`, `usd`, `calls`, `usage`, `turns`, `model`.
- `alert`: `rule_id`, `rule_status` (`enabled | shadow`), `path`, `line`, `function`, `pin_role`
(`vulnerable | fixed`).
- `proposal_outcome`: `edit_key`, `evidence_digest`, `outcome` (`accepted | reverted | rejected`), `gate_run`.
**Fact reuse.** A facts entry is valid for the key (repo, pin, candidates digest, runner image digest). The image
is in the key because the host's source and the image's source can differ, and facts computed by other code are
not the same facts. `ousast plane workspaces --validation-set` writes the key as the Task annotation
`openultrasast.io/memory-key`. Annotations are already stripped before ax (`manifests.py:4-6`). Before
`reconciler.run`, `cli._plane` calls `memory.seed(run_manifest)`. For each Run entry whose Task carries a key with
stored facts and no state yet, seed writes `<run>/<task>/facts.json` and a `summary.json`
(`status: done, units_done == units_total == 1, reused: {"sha256", "from_run"}`). It then calls
`reconciler.mark_done(run, task, reused=...)`, a public helper of about 10 lines that takes the run lock, sets
the state and releases it. The reconciler already skips `done` tasks (`reconciler.py:342`). The receiver already
serves any regular file under the run directory as a declared input (`egress.py:242-246`). A changed pin, a
changed candidate set or a rebuilt image recomputes.
### 5. Proposals from memory (Requirement 6.2, 6.4)
**Interface.** The proposal surface HarnessX would have used is `run_round` (`improve/evolve.py:149-247`). It
builds `RuleStatusEdit`s (`improve/validator.py:36-47`), filters them through the journal's novelty gate
(`journal.reverted_edit_keys`, `journal.py:26-38`), validates each with `EvolveValidator.validate`
(`validator.py:75-81`, which accepts only `RuleStatusEdit` and `PolicyConstantEdit`), smoke-replays and gates.
`improve/memory.py` exposes:
```
propose_from_memory(rows, ruleset_by_id, current_ledger, journal_rounds, *, excluded) -> list[MemoryProposal]
MemoryProposal = (edit: RuleStatusEdit, provenance: dict)
```
`run_round` gains `proposals: Sequence[MemoryProposal] = ()`. It appends their edits after the benchmark's edits
and keeps the first edit per `key()`. The validator and the gate are untouched. `ousast improve` gains
`--memory [DIR]` (off by default, so the deterministic `improve` output is byte-identical) and
`--qualify-population NAME` (repeatable).
**Rules** (the thresholds are module constants and part of the provenance record). Join key: an `alert` and a
`verdict` match when repo, pin and `path::function` are equal. A `disputed` final counts as no evidence in
either direction.
- **M1, repeated false alerts** (`enabled -> shadow`). Rule R qualifies when both hold:
- at least 3 R alerts sit at candidates whose final is `rejected`, or on a `fixed` pin inside the case's declared
fix range, across at least 2 distinct repositories;
- no R alert sits at an `agreed` candidate or a `site_match` site.
- **M2, missed declared sites** (`shadow -> enabled`). Shadow rule R qualifies when all three hold:
- its shadow alerts hit at least 2 `agreed` candidates with `site_match == true`, across at least 2
repositories;
- no enabled rule alerts at those candidates;
- R has no alert on a `fixed` pin at the same function and at most 1 alert at a `rejected` candidate.
- **M3, frequently disputed families.** Families with at least 5 candidates and a dispute rate of at least 0.3 go
to `signals.json` as advisory input for plane routing (for example, scheduling pass `c` up front). They are not
an edit and never reach the validator.
**Provenance.** Each proposal's `rationale` is `memory:<proposal_id>`, where `proposal_id` = sha256 of
(rule, edit key, sorted evidence row ids, thresholds). The sidecar `memory_proposals.jsonl`, next to the journal,
holds:
- `proposal_id`, `edit_key`, `rule_id`
- `evidence` (the row ids plus their repo, pin, run and task)
- the counts that met each threshold
- `excluded` (populations and pins removed by the guard, with counts)
- the store's `index.jsonl` digest at proposal time
`evolve._record` (`evolve.py:366-396`) adds `"evidence": proposal_id` to an edit's journal entry only when the
edit came from memory. Otherwise the journal is unchanged byte for byte. A reader follows journal, then sidecar,
then store rows, then the run directory.
**Novelty.** `journal.reverted_edit_keys` does not block a rule edit that carries a rationale (`journal.py:36`), so
a memory proposal would otherwise come back every round. The proposer itself skips any key whose reverted journal
entry has the same evidence digest. New evidence is the "new rationale" that allows a retry.
**Train-on-test guard.** A row is excluded from proposals when any of these holds:
- (a) its `population` is one of `--qualify-population`, which is also where the proposal will be gated or
qualified;
- (b) its (repo, pin) is a case of the benchmark manifest being gated;
- (c) its (repo, pin) is a `split = "holdout"` pair of the pair catalog, which the per-profile holdout clause
(`evolve.py:204-214`) measures.
This is the rule the closed-loop leak broke, where a holdout pair taught the shape that "recovered" it. Here the
filter runs before any rule sees a row. Concretely, rows from `validation-46` (population
`independent-v2/validation`) never propose edits that are then qualified on `independent-v2/validation`. The
tie-break was designed on that set, so it is development evidence only.
### 6. The loop as a plane Run (Requirement 6.3)
`ousast plane workspaces --validation-set ... --loop` extends the generated Run. It also emits a Workspace for each
case's fixed pin (the population's `fixed` sha, already read by `generate.fix_ranges`, `generate.py:89`):
| Task (per case unless noted) | Model | Budget | Inputs -> outputs |
| --- | --- | --- | --- |
| `<case>-facts`, `-va`, `-vb`, `-agree`, `-vc`, `-final` | verify passes: `deepseek-flash`; others none | as generated today (`generate.verify_budget`) | unchanged |
| `<case>-alerts` (new) | none | `{0, 0}`, `serialize: engine` | vulnerable + fixed Workspaces -> `alerts.jsonl` (quick-mode `quick_scan_findings` with enabled and shadow rules, function-attributed) |
| `<case>-remember` (new) | none | `{0, 0}` | facts, va, vb, vc, final, alerts -> `memory.jsonl`, `facts.json` |
| `loop-measure` (once) | none | `{0, 0}` | every `remember` + `memory-snapshot/rows.jsonl` -> `measure.json` (the increment metrics: declared sites agreed, cost per candidate, dispute rates) |
| `loop-propose` (once) | none | `{0, 0}` | measure, snapshot, journal -> `proposals.jsonl`, `memory_proposals.jsonl`, `signals.json` |
| `loop-improve` (once) | none | `{0, 0}`, `serialize: engine` | proposals; Workspace = this repository at the commit under test -> `gate.json` (the `RoundOutcome`), `journal.json`, `rule_policy.json` (only when accepted) |
Before the Run starts, `memory-snapshot` is seeded into the run directory like reused facts. It holds the store
rows after the guard, since a sandboxed task cannot read the host store. A `{usd: 0, calls: 0}` budget makes
`budget.py` refuse any call, so a model-free task that tries one fails loudly rather than spending. Attribution
per task is the existing `ousast plane status` table.
**The gate's verdict** lands in `~/ousast-results/plane/<run>/loop-improve/gate.json` and is ingested as
`proposal_outcome` rows. An accepted `rule_policy.json` is adopted into the repository only by a maintainer
commit, as any ledger change is today (README "Rule-level loop").
### 7. Documentation and specs (Requirement 4)
- **README.** Replace "Configuring the HarnessX agentic plane" (README.md:293-329) with "The agentic plane (ax)":
what runs there (repo-facts, verify, agree, memory), `ousast plane run|status|remember`, a pointer to
`ops/ax/README.md`, and one tagged line saying HarnessX was retired. Rename "The HarnessX self-improving cycle"
(:330) to "The self-improving cycle". Its calibration ledger and `ousast improve` content is not HarnessX and
stays. The MetaAgent paragraph (:393-400) is replaced by the memory proposer (`--memory`, provenance, guard).
Drop the `panel_model`/`decider_model` lines at :227-228.
- **`docs/examples.md` section 6** becomes "Running the agentic plane", with the plane doctor, run and status
commands.
- **`docs/threat-model.md:17-20, 32-34`**: egress is the per-task EgressPolicy (receiver, Git hosts, Model
`egress-hosts`, passthrough only). Credentials travel only in the start request. Spend is the per-task
`usd`/`calls` budget in the Run.
- **Retirement notes.** A dated note ("Retired 2026-..: HarnessX was removed by `harnessx-removal`; the agentic
plane is ax (`ai-service-plane`), proposals come from the plane memory. History below is unchanged.") goes at
the top of the requirements (and design, where present) of `harnessx-self-improving-rulesets`,
`learning-harness`, `three-stage-scan` and `openrouter-sast-harness`, and in `.kiro/steering/overview.md` near
:175. The other specs that mention HarnessX in passing keep their history untouched.
- `RELEASE_NOTES.md` gets one entry listing the retired keys. Kiro tooling stays out of user docs.
## Data Models
- `memory.jsonl` (remember output) and `repos/<repo>/<pin>.jsonl`: the rows above, one JSON object per line,
sorted keys, stable `id` = sha256(kind, run, task, candidate|path|rule_id+line).
- `index.jsonl`: `{"run", "task", "sha256", "rows", "ingested"}`.
- `memory_proposals.jsonl`: `{"proposal_id", "edit_key", "rule_id", "rule": "M1|M2", "evidence": [...],
"counts": {...}, "thresholds": {...}, "excluded": {...}, "store_digest"}`.
- Journal edit entry: the existing fields (`evolve.py:381-384`) plus `"evidence"` only for memory edits.
- Equality record: `env.json` `{"commit", "freeze_sha256", "harnessx_importable": false, "tree_sitter": true,
"inputs": {path: bytes}, "exclusions": [...]}`.
## Error Handling
- `RetiredConfigError`: exit 2 with the key and its replacement, before any stage runs.
- A `[harnessx]` section produces exactly one warning, and loading continues.
- `memory.ingest` on a run without `remember` outputs is a no-op that says so. A malformed row fails the ingest
and names the file and line, and nothing partial is written: rows are staged in a temporary file and renamed.
- A low-disk refusal names the store and the free space. The plane run's result is unaffected, and ingest can be
repeated with `ousast plane remember`.
- `seed` never overwrites an existing task directory or a non-pending state. A facts file whose sha256 no longer
matches is dropped from the store with a warning, and that task recomputes.
- `propose_from_memory` with an empty store after the guard returns no proposals, and `run_round` reports
`no_proposals` as today.
- The equality script exits non-zero on the first failed instrument check. It also exits non-zero on any
difference outside the exclusion list and prints the first differing path and key.
## Testing Strategy
- `tests/test_config.py`: the one-warning section and the retired keys (above).
- `tests/test_no_harnessx_references.py`: the tagged-only search of Requirement 2.4.
- `tests/test_plane_memory.py`: ingest is idempotent and content-addresses identical facts. `seed` marks a facts
task done and serves its file, and does not for a changed pin, candidate digest or image. Low-disk refusal
(monkeypatched `disk_usage`).
- `tests/test_plane_remember.py`, `test_plane_alerts.py`, `test_plane_loop.py`: fixture run directories built from
the recorded validation-46 shapes. `remember` rows match `agree`'s per-candidate rows. `alerts` attributes
functions. `measure` reproduces the increment's recorded metrics on the recorded artifacts. `propose` and
`improve` work end to end on a fixture manifest.
- `tests/test_improve_memory.py`:
- M1 and M2 fire at their thresholds and not below.
- A disputed candidate counts neither way.
- The guard removes rows by population, manifest case and holdout pair, so a proposal that exists only because
of a holdout row disappears.
- Provenance ids are stable across runs.
- A reverted memory edit is not re-proposed on the same evidence, and is re-proposed on new evidence.
- `run_round` without proposals writes a journal byte-identical to today's.
- `tests/test_plane_reconciler.py`: `mark_done` under the lock. The existing under-500-lines test stays.
- The existing gate and pair tests, `test_benchmark_output_is_byte_identical_to_golden_baseline`
(`test_gate.py:104`) and the equality script (section 3) are the regression net.
## Requirement Traceability
| Requirement | Design section | Verified by |
| --- | --- | --- |
| 1.1 fate of every capability | 1 (table) | this document; commit 5 |
| 1.2 moved capabilities reachable, Model per task, fixture test | 1 (existing `verify`/`agree`), 4-6 (new tasks are Model-free) | `test_plane_verify.py`, `test_plane_tiebreak.py`, new plane task tests |
| 1.3 retired keys fail naming the replacement | 1 (`RetiredConfigError`) | `test_config.py` |
| 2.1 modules and guards gone | 2 Source | commit 6; audit test |
| 2.2 extra, override, lock gone; installs succeed | 2 Packaging | fresh-venv `pip install .` / `'.[semantic]'` |
| 2.3 tests deleted or renamed; audit regenerated | 2 Tests, Module audit | `test_module_audit.py` |
| 2.4 search returns only retirement notes | 2 Search | `test_no_harnessx_references.py` |
| 3.1 baseline without the extra | 3 | commit 1 record |
| 3.2 byte-identical after, exclusions written | 3 Comparison | commit 7 record |
| 3.3 `[harnessx]` loads with one warning | 1 | `test_config.py` |
| 4.1-4.3 docs, retirement notes, Kiro out of user docs | 7 | commit 8 review |
| 5.1-5.2 order, green steps, no deletion before replacement | Commit sequence | per-commit gates |
| 6.1 store keyed by repo + pin, fact reuse | 4 | `test_plane_memory.py`, `test_plane_remember.py` |
| 6.2 deterministic proposals through validator and gate | 5 | `test_improve_memory.py` |
| 6.3 loop as a plane Run with per-task Models and budgets | 6 | `test_plane_loop.py`; `ousast plane status` attribution |
| 6.4 provenance and train-on-test guard | 5 | `test_improve_memory.py` |
## Commit sequence (Requirement 5), mapped to future tasks
Each commit is gated on the full suite's own exit code, `ruff` and `mypy`.
1. **Baseline** (3.1): the equality script and `benchmarks/measurements/<date>-harnessx-removal-baseline/`,
recorded with the extra uninstalled.
2. **Memory store and `remember`** (6.1): `plane/memory.py`, `tasks/remember.py`, `reconciler.mark_done`, seed and
ingest wiring in `cli._plane`, generator annotations, fixture tests.
3. **Memory proposer** (6.2, 6.4): `improve/memory.py`, `run_round(proposals=...)`, `ousast improve --memory`,
sidecar and journal evidence, guard tests.
4. **Loop Run** (6.3): `alerts`, `measure`, `propose` and `improve` tasks, `--loop` generation, fixed-pin
Workspaces, fixture tests.
5. **Retire the scan paths** (1, 3.3): the `cli.py`, `fusion.py`, `verify_judge.py` and `stage_processors.py`
changes, `RetiredConfigError`, the one warning, and the test renames that follow. At this point nothing imports
`harness_ext`/`hunter_harness`. Their replacements (plane tasks, and the memory proposer from 2-4) have landed.
6. **Delete code and packaging** (2): the two modules, the extra, the mypy override, `uv.lock`, the deleted tests,
the fixture rename, audit rows and the manifest regeneration, and the reference search test.
7. **Equality proof** (3.2): rerun the script, commit the comparison record next to the baseline, and link it in
the commit message.
8. **Docs and specs** (4): the README, `docs/examples.md`, `docs/threat-model.md`, retirement notes, steering and
release notes.
## Risks
- **Existing users of the LLM scan paths.** There is no `--llm` flag. Users who set `[models] verifier` or
`[fusion] panel_model` get a hard error, which is intended (1.3). Users who installed the extra and set
`[models] hunter` lose the second (HarnessX) hunter without an error. Only the release note covers that,
because the key is still valid. No measurement suggests either path found anything the remaining paths miss.
The absence of a measurement is itself the risk.
- **Plane precision is unsolved.** `plane-increment-2.json` agrees 28 candidates, of which 16 are declared
sites. The other 12 are unadjudicated and several may be false. M1 treats a `rejected` final as negative
evidence, while plane recall is 16/20, so "rejected" is not "safe". The thresholds (2 or more repositories, zero
agreed hits) and the unchanged recall/FP gate are the guard. Precision work (judge design) remains out of scope.
- **Evidence is thin.** Today's store would hold 15 cases from one validation population, which is exactly the
population the guard excludes for its own qualification. Expect `no_proposals` until a second population has
run through the loop.
- **Disk.** The home volume is 98% full (2.4 GB free). One validation-46-sized Run adds about 3 MB of facts
(content-addressed; about 0.1 MB when pins are unchanged) plus about 0.5 MB of rows. Ingest refuses below 1 GiB
free. The store has no eviction in this spec.
- **The baseline measures the environment as well as the code.** `.venv` has `harnessx` installed today. A
baseline taken without re-syncing would include it. The script's `find_spec` and freeze checks exist for that.
- **Seeding writes reconciler state from outside the reconciler.** It is confined to `mark_done` under the run
lock. The reconciler's line budget (465 of 500) leaves no room for fact reuse inside the reconciler.
@@ -1,125 +0,0 @@
# Requirements Document
## Introduction
HarnessX is the optional agentic plane (`openultrasast[harnessx]`): an LLM hunter pool, an LLM judge and LLM
fusion panels, each behind a capability guard with a deterministic fallback, plus a lazy bridge that hosts
deterministic stages under a HarnessX processor. The ai-service-plane increment met its gate on google/ax
(`benchmarks/independent/plane-increment-2.json`), which is the condition the maintainer set on 2026-09-29:
"goal is to remove harnessx and to shift to ax fully once we establish we have value." This feature removes
HarnessX from the product and moves every LLM capability it carried either onto the plane or into a recorded
retirement. Inventory: `brief.md` (verified 2026-09-30).
Maintainer, 2026-09-30: "Please remove harnessx completely. the memory layer that saves tokens takes over the
evolutionary improvements as well as loop engineering." So nothing of HarnessX stays, not even as an optional
path, and the self-improving cycle HarnessX was meant to drive (the LLM `MetaAgent.evolve` proposer plugged into
`improve/evolve.py`) is carried by the plane's memory layer instead: what runs record (repository facts,
verdicts, disputes, costs) becomes the input to proposals and to the run-measure-propose-gate loop.
## Boundary Context
- **In scope**: the HarnessX-only modules (`harness_ext.py`, `hunter_harness.py`); the guarded branches in
`cli.py` (scan `--llm` path, `HxScanOrchestrator`), `verify_judge.py` (judge path), `fusion.py` (LLM panels),
`stage_processors.py` (`host_under_harnessx`), `config.py` (`HarnessxConfig`); the `harnessx` extra, its mypy
override and lock entry; the tests that exercise HarnessX; README and docs sections; dated retirement notes in
the specs and steering that name HarnessX.
- **Also in scope**: the plane memory layer as the source of evolutionary proposals and the loop (Requirement 6),
replacing the HarnessX proposer.
- **Out of scope**: changing the deterministic engine, rulesets or `evolve`'s validator and gate (proposals change
source, not how they are validated and gated); new detection logic; qualification on an independent population; the plane's own precision work (judge design) --
where a capability moves to the plane, this spec wires it, it does not improve it.
- **Adjacent expectations**: every deterministic path (quick mode, `regress`, `benchmark`, `gate`, `evolve`)
produces byte-identical output before and after; `vulnerabilities-over-plumbing` applies, so capabilities are
kept or retired by what they detect, reported in TP/FP terms where a measurement exists.
## Requirements
### Requirement 1: HarnessX is removed completely; the plane is the only agentic path
**User Story:** As the maintainer, I want no HarnessX path left, optional or not, and every LLM capability either
on the plane or explicitly retired, so that nothing is lost silently.
#### Acceptance Criteria
1. No HarnessX code path remains, optional or guarded. The design lists each capability -- LLM hunter pool
(`ousast scan --llm`), LLM judge (`verify_judge`), LLM fusion panels (`fusion`), deterministic stages hosted
under HarnessX (`host_under_harnessx`), the LLM evolution proposer -- with its fate: carried by a plane task
(named), carried by the memory layer (Requirement 6), or retired with the deterministic path kept.
2. A capability moved to the plane is reachable from the CLI through the plane (`ousast plane run` with a
documented Run, or a CLI flag that submits one), binds its own Model per task (ai-service-plane Req 7), and
has a fixture test like the other plane tasks.
3. A retired capability's CLI flags and config keys fail with a message that names the replacement or says it
was retired, never a silent no-op.
### Requirement 2: HarnessX code, packaging and tests are gone
**User Story:** As the maintainer, I want no HarnessX code path left to maintain.
#### Acceptance Criteria
1. `harness_ext.py` and `hunter_harness.py` are deleted; no module imports `harnessx`; `use_harnessx`
parameters, `has_harnessx` checks and `host_under_harnessx` are removed; the deterministic fallbacks become the
only code path where a capability is retired.
2. The `harnessx` extra, the mypy override for `harnessx.*` and the lock entry are removed; `pip install .` and
`pip install '.[all]'` (or whatever extras remain) succeed.
3. Tests that only exercise HarnessX are deleted; tests that assert fallback behaviour are kept and renamed to
describe the behaviour, not the absence of HarnessX; the module audit is regenerated with no orphan.
4. A search for `harnessx` (case-insensitive) in `src/`, `tests/`, `pyproject.toml` and the README returns only
the retirement notes of Requirement 4.
### Requirement 3: Deterministic behaviour is unchanged, and proven so
**User Story:** As the maintainer, I want evidence, not assurance, that removing HarnessX changed nothing a
deterministic run produces.
#### Acceptance Criteria
1. Before any deletion, a baseline is recorded without the extra installed: the full host suite, and the outputs
of `ousast regress` and one `ousast benchmark` replay on the development pins, stored under
`benchmarks/measurements/`.
2. After the removal the same commands produce byte-identical outputs (timestamps and durations excluded, and
the exclusion list is written down); any difference blocks the merge.
3. A config file that still carries a `[harnessx]` section loads with one warning that names the plane, and the
section is otherwise ignored.
### Requirement 4: Documentation and specs tell the new story
**User Story:** As a reader of the README or a spec, I want to learn that the agentic plane is ax, not HarnessX.
#### Acceptance Criteria
1. The README sections "Configuring the HarnessX agentic plane" and "The HarnessX self-improving cycle" are
replaced by a short section on the ax plane that points at `ops/ax/README.md`; `docs/examples.md` section 6 and
`docs/threat-model.md` (egress and spend caps) describe the plane's egress policy and budgets instead.
2. Specs that name HarnessX (`harnessx-self-improving-rulesets`, `learning-harness`, `three-stage-scan`,
`openrouter-sast-harness`) and `.kiro/steering/overview.md` get a dated retirement note at the top; their
history is not rewritten.
3. Maintainer-only Kiro tooling stays out of user docs (standing rule).
### Requirement 5: The removal lands in one reviewable sequence
**User Story:** As the maintainer, I want to review the removal in steps that each leave main green.
#### Acceptance Criteria
1. Order: baseline (3.1) -> capabilities moved or retired (1) -> code and packaging removed (2) -> equality proof
(3.2) -> docs and specs (4). Each step is a commit gated on the full suite's own exit code, ruff and mypy.
2. No step deletes code whose replacement (Requirement 1) has not landed and passed its fixture test.
### Requirement 6: The memory layer carries evolutionary improvement and loop engineering
**User Story:** As the maintainer, I want the token-saving memory layer to be what the system learns from, so
that improvement proposals and the improvement loop come from recorded evidence instead of a HarnessX agent.
#### Acceptance Criteria
1. Plane runs persist their memory beyond the run directory in one store keyed by repository and pin: repository
facts (`repo-facts`), per-candidate verdicts per pass with the final decision, disputes and tie-breaks, and cost
and tool turns per unit; later runs reuse facts for an unchanged pin instead of recomputing them.
2. `evolve`'s proposals are generated from that store (for example: families or rules whose candidates are often
disputed, missed declared sites, repeated false alerts), deterministically and without a HarnessX agent; they
pass through the existing validator and gate unchanged.
3. The improvement loop -- run, measure, propose, validate, gate -- is expressible as a plane Run whose tasks bind
their own Models and budgets (ai-service-plane Req 7), so each iteration's token spend is attributed per task.
4. A proposal records which memory entries it came from, so a later reader can trace every accepted change to the
evidence behind it; proposals never train on the population used to qualify them (the train-on-test rule).
-22
View File
@@ -1,22 +0,0 @@
{
"feature_name": "harnessx-removal",
"created_at": "2026-09-30T08:00:00Z",
"updated_at": "2026-09-30T16:00:00Z",
"language": "en",
"phase": "implementation-complete",
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": true
}
},
"ready_for_implementation": true
}
-62
View File
@@ -1,62 +0,0 @@
# Implementation Plan
Order follows the design's commit sequence (Requirement 5): nothing is deleted before its replacement has landed
and passed its fixture test. Every task is a commit gated on the full suite's own exit code, `ruff check`,
`ruff format --check` and `mypy`; the reconciler stays under 500 lines.
- [x] 1. Baseline without the extra
- Equality script (design §3) with its `find_spec("harnessx")` and freeze checks; re-sync `.venv` without the
`harnessx` extra first.
- Record `benchmarks/measurements/<date>-harnessx-removal-baseline/`: full suite, `ousast pairs --json`, the
`python-vulnerable` quick benchmark, one standard-mode scan, `ousast improve --dry-run`; the exclusion list
written next to it.
- _Requirements: 3.1, 5.1_
- [x] 2. Memory store and the `remember` task
- `plane/memory.py`: the `MemoryStore` interface with `FileStore` and `S3Store` (object metadata/tags,
S3 Select JSON queries probed with a local fallback, versioned provenance, lifecycle, presign stubs;
`OUSAST_MEMORY` selects; S3 settings from `.env`; boto3 as an optional extra); JSON rows per
repository + pin, facts by content hash, `FileStore` ingest refuses below 1 GiB free,
`plane/tasks/remember.py` (model-free), `reconciler.mark_done` under the run lock, seed and ingest wiring in
`cli._plane`, generator annotations; fact reuse when repository, pin, candidate set and runner image match.
- Fixture tests: rows from a recorded Run, reuse on an unchanged pin, recompute on a changed one, disk refusal.
- _Requirements: 6.1, 6.4_
- [x] 3. Proposals from memory
- `improve/memory.py` with rules M1 (demote) and M2 (promote) and the advisory disputed-family signal;
`run_round(proposals=...)`; `ousast improve --memory` (opt-in; default output byte-identical); provenance per
proposal in the sidecar and journal; the train-on-test guard (qualification population, gated benchmark cases,
holdout pairs).
- Tests: each rule on crafted rows, the guard drops excluded rows, provenance round-trips, default `improve`
unchanged.
- _Requirements: 6.2, 6.4_
- [x] 4. The loop as a plane Run
- Tasks `alerts`, `measure`, `propose`, `improve` (no Model, zero budget); `--loop` Run generation; fixed-pin
Workspaces; the gate's verdict in `<run>/loop-improve/gate.json`; adoption stays a maintainer commit.
- Fixture tests with the fake ax; one live loop Run on the local ax recorded (no model spend beyond the verify
passes it reuses from memory).
- _Requirements: 6.3_
- [x] 5. Retire the HarnessX scan paths
- `cli.py`, `fusion.py`, `verify_judge.py`, `stage_processors.py` changes; `RetiredConfigError` for
`[models] verifier`, `[fusion] panel_model`/`decider_model`; one warning for a `[harnessx]` section (the only
tagged retirement text allowed in `src`, enforced by a test); tests renamed to the behaviour they assert.
- _Requirements: 1.1, 1.2, 1.3, 3.3_
- [x] 6. Delete HarnessX code and packaging
- Delete `harness_ext.py`, `hunter_harness.py`, `verify_judge.py`'s judge path, the HarnessX-only tests; remove
the extra, the mypy override and the `uv.lock` entry; rename the conftest fixture; audit rows and manifest; the
reference search test (Req 2.4).
- _Requirements: 2.1, 2.2, 2.3, 2.4_
- [x] 7. Equality proof
- Rerun the baseline script; commit the comparison record next to the baseline; any difference outside the
exclusion list blocks the merge.
- _Requirements: 3.2_
- [x] 8. Documentation and specs
- README ax-plane section pointing at `ops/ax/README.md`; `docs/examples.md` §6 and `docs/threat-model.md`
(egress policy, budgets); dated retirement notes in the four specs and `.kiro/steering/overview.md`; release
note naming the silent loss of the second hunter for users who set `[models] hunter`.
- _Requirements: 4.1, 4.2, 4.3_
@@ -1,96 +0,0 @@
# DocuHarnessX ↔ OpenUltraSAST — Blueprint Alignment
How OpenUltraSAST's HarnessX integration (Phase 3+) aligns with **DocuHarnessX**
(`/home/mc/Source/DocuHarnessX`), the author's other HarnessX project. Both are HarnessX
bundles steered from claude-code/opencode via Kiro spec-driven development. Goal: one shared
architecture the steering layer drives identically.
## Orientation
DocuHarnessX is **agent-first** — an 8-stage pipeline (`ingest → analyze → classify → plan →
write → review → assemble → deploy`) that *is* the agent loop. OpenUltraSAST is a
**deterministic-pipeline + agentic-plane hybrid** — a sync `HarnessRuntime` driving 13 stages,
with HarnessX confined to the one stage that is genuinely an agent loop (the hunter pool). They
converge on the same load-bearing patterns and diverge only where SAST's determinism and
supply-chain posture require it.
**What "self-improving multi-agent system" means in DocuHarnessX (important):** it does **not**
use `meta_harness` / `MetaAgent.evolve` at all (the `Train` loop is explicit future / out of
scope). Its self-improvement is realized two ways: (1) an in-product **fail-closed LLM-judge
quality firewall** (COBESY: fixed criteria, threshold-re-derived verdicts, unavailable → fail),
and (2) the **claude-code/opencode Kiro steering loop** (fresh implementer → adversarial
reviewer → debugger subagents per task). OpenUltraSAST inherits both substrates and *additionally*
builds the `MetaAgent.evolve` loop DocuHarnessX leaves unbuilt.
## Shared blueprint (the spine)
| Concern | DocuHarnessX | OpenUltraSAST | Verdict |
|---|---|---|---|
| Composition seam | `make_docgen` in `docuharnessx/bundle.py` — the only HarnessX import site; `control \| stages_builder()`; returns model-less `HarnessConfig` | `build_sast()` in `harness/build_sast.py` (mirrors `examples/coding/build_coding`), lazy behind `_has_harnessx()` | **shared** |
| Model binding | CLI `prepare_run()`: `model_config.agentic(make_docgen(...))`; model on `harness.model_config.main`, never in config | `HxScanOrchestrator`: `asyncio.run(model_config.agentic(cfg).run(ScanTask(...)))` | **shared** |
| Processor base | `NoOpStage(MultiHookProcessor)`, content-neutral on `step_end`, real module-level classes for `_target_` resolution | `MultiHookProcessor(skip_model=True)` + `SlotContractMixin`, real module-level classes | **shared** |
| Inter-stage seam | `RunContext` over append-only slot constants; frozen versioned handoff dataclasses | typed `sast.*` slots + `SlotContractMixin` (`reads_slots`/`writes_slots`) | **shared** |
| Domain / "ontology" seam | `.docuharnessx/ontology.yaml` → `Vocabulary` (Role×Subject×Intent), loaded at run start | central **CWE policy + ruleset** (`policy/CWE_Score.tsv` + `ruleset/*.toml` + `rule_policy.json`), loaded at run start | **analogous** |
| Acceptance firewall | COBESY all-of gate; judge `passed` untrusted/re-derived; unavailable → fail | `VerifyProcessor` (llm-judge + structural fallback, rejects unknown reachability, fail-closed) | **analogous** |
| Steering | Kiro SDD; `CLAUDE.md`; `.kiro/{steering,specs}`; `kiro-impl` fresh-subagent + adversarial review; `kiro-verify-completion` | same Kiro convention; `.kiro/specs/harnessx-self-improving-rulesets/*`; OpenCode steers, kiro-* maintainer-only | **shared** |
| Self-improvement (`evolve`) | **none** — future seam | `improve/evolve.py → MetaAgent.evolve`, bounded `rule`/`policy` levers, `rule_policy.json` ledger | **diverges (OUS builds it)** |
| Dependency posture | `harnessx` direct git dep + pyyaml | core zero-dep; `harnessx` optional SHA-pinned extra, lazy | **diverges (by design)** |
## Three principled divergences (and reconciliation)
1. **Agent-first vs hybrid.** OUS keeps the sync `HarnessRuntime` through Phases 0–4 and confines
HarnessX to the hunter pool (the runtimes are "incompatible at every load-bearing point —
bridge, do not map"). Reconcile by treating `build_sast()` as building **only the hunter
sub-harness**, applying DocuHarnessX's `make_docgen` pattern *at the hunter-pool boundary*.
Optional Phase 5 converges OUS toward DocuHarnessX's agent-first end-state (retire
`HarnessRuntime`) — the divergence is temporal, and DocuHarnessX is the north star.
2. **Editable ontology vs authoritative severity.** DocuHarnessX's whole vocabulary is editable
presets. OUS splits its domain seam into a **mutable detection vocabulary** (`ruleset/*.toml`,
loop- and human-editable) and an **immutable severity/scope authority** (`CWE_Score.tsv`,
verycode-upstream, the loop must never edit). Where DocuHarnessX relies on convention ("never a
hardcoded literal"), OUS relies on a **validator** (`EvolveValidator._policy` forbids editing the
0–5 severity) because a loop, not just a human, edits it.
3. **Firewall-as-reward vs reward/feasibility split.** DocuHarnessX's firewall *is* the signal
(binary pass/fail). OUS must feed `MetaAgent.evolve`, so it keeps a **scalar reward**
(`project_score`) **separate from** a **hard feasibility constraint** (≥90% recall / <10% FP),
un-folded so the loop can't game the number. Reuse DocuHarnessX's fail-closed firewall verbatim
at the *per-finding* `VerifyProcessor` granularity; layer the reward/feasibility split on top.
## Phase 3 blueprint for OpenUltraSAST (mirror DocuHarnessX)
- **`build_sast()` factory** = the single HarnessX composition import site (DocuHarnessX's "blast
radius is this file" rule on `bundle.py`); `control | hunter_stages_builder()`; raised loop
thresholds (large-repo scanning revisits shapes, like large-repo docs); model bound later in
`HxScanOrchestrator`. OUS delta: the `_has_harnessx()` guard + raise-then-fallback to
`run_hunter_pool()`.
- **Ontology seam** = CWE policy + ruleset as OUS's "ontology": `load_ruleset(path, ledger)` +
`load_policy(tsv)` loaded into `sast.*` slots at run start; `resolve_severity()` is the single
severity authority (mirrors DocuHarnessX's "every transform reads the loaded store, never a
literal"); absent ledger → byte-identical baseline; unmapped enabled-rule CWE → fail-loud
(stricter than DocuHarnessX, because severity is authoritative).
- **Processor set**: register HarnessX classes directly **only inside the hunter sub-harness**
(LoopDetection, CostGuard/TokenBudget, ToolFailureGuard, ToolFilter, SelfVerify, EpisodeMetrics/
OTel/Checkpoint); everything deterministic is a `skip_model=True` `MultiHookProcessor` over slots
(SarifIngest/Preprocess/EntryPoint/Rank/Calibrate/QuickFindings/PolicyScoring/Verify/Score) with
stable `make_<stage>_processor()` factories so a spec replaces exactly one stub in place.
- **`MetaAgent.evolve` loop** (Phase 4, on the same substrate): two bounded levers (`rule`,
`policy`); reward = `project_score`; feasibility = recall/FP gate (un-folded); signal path
benchmark per-rule precision/recall + `rule_policy_delta` → `rule_signals.json` →
`MetaAgent.evolve`; gate = validity (canonicalize → contract → `_rules`/`_policy` → replay) →
novelty/evidence → re-benchmark → attribution → accept or **byte-for-byte revert** (the
structural analog of DocuHarnessX's `DEFAULT_UNAVAILABLE_VERDICT="fail"`); ledger =
`rule_policy.json` (OUS's resume-safe analog of DocuHarnessX's markdown round journal).
- **Steering surface**: reuse DocuHarnessX's `kiro-*` skills verbatim (maintainer-only); add
SAST-domain `.kiro/steering/` rules pinning the detection gate as a hard invariant, the zero-dep
guard, and the `pip --dry-run` audit gate; traceable per-module docstrings citing spec Req/Decision
numbers.
## Net
The two projects already share the spine — one composition factory, model bound in the CLI via
`ModelConfig.agentic`, content-neutral processors over typed slots, a project-loaded domain seam,
a fail-closed acceptance firewall, and the Kiro fresh-subagent steering loop. OpenUltraSAST diverges
in exactly three principled places (hybrid runtime confining HarnessX to the hunter pool; it *builds*
the `MetaAgent.evolve` loop with a reward/feasibility split + `rule_policy.json` ledger; zero-dep core
with HarnessX opt-in). Adopting DocuHarnessX's load-bearing patterns at the points above gives the
two projects one architecture that opencode/claude-code steers identically.
@@ -1,116 +0,0 @@
# HarnessX Dependency Dry-Run Audit (Task 5.1)
Recorded realized transitive dependency graph and the accept-vs-vendor decision for
adopting HarnessX (`git+https://github.com/Darwin-Agent/HarnessX.git`) in Phase 3.
## Method
```bash
uv venv /tmp/hx-audit --python 3.11
uv pip install --dry-run --python /tmp/hx-audit/bin/python <HarnessX checkout>
```
- **118 transitive packages** resolved from HarnessX's **30 direct** `[project.dependencies]`
(all mandatory upstream; the only extra is `dev`).
- Pinned versions observed: `harnessx 0.1.0`, and engine image `registry.dagger.io`/SDK
unrelated; LLM SDKs `anthropic 0.111.0`, `openai 2.43.0`, `litellm 1.89.3`.
### Realized transitive graph (118)
```
aiofiles aiohappyeyeballs aiohttp aiosignal annotated-doc annotated-types anthropic
antlr4-python3-runtime anyio attrs bracex certifi cffi charset-normalizer click cryptography
distro docker dockerfile-parse docstring-parser e2b et-xmlfile fastapi fastuuid filelock
frozenlist fsspec googleapis-common-protos greenlet grpcio h11 h2 harnessx hf-xet hpack
html2text httpcore httptools httpx httpx-sse huggingface-hub hydra-core hyperframe idna
importlib-metadata jinja2 jiter jsonschema jsonschema-specifications litellm loguru lxml
markdown-it-py markupsafe mcp mdurl multidict omegaconf openai openpyxl opentelemetry-api
opentelemetry-exporter-otlp opentelemetry-exporter-otlp-proto-common
opentelemetry-exporter-otlp-proto-grpc opentelemetry-exporter-otlp-proto-http opentelemetry-proto
opentelemetry-sdk opentelemetry-semantic-conventions packaging pdfminer-six pdfplumber pillow
playwright prompt-toolkit propcache protobuf pycparser pydantic pydantic-core pydantic-settings
pyee pygments pyjwt pypdfium2 python-dateutil python-docx python-dotenv python-multipart
python-pptx pyyaml referencing regex reportlab requests rich rpds-py shellingham six sniffio
sse-starlette starlette structlog tiktoken tokenizers tqdm typer typing-extensions
typing-inspection urllib3 uvicorn uvloop watchfiles wcmatch wcwidth websockets xlsxwriter yarl zipp
```
## Key finding: heavy backends are lazy; ~half the tree is dead weight
Every heavy backend is **lazily imported** — `docker`/`e2b` are `TYPE_CHECKING`-guarded and
imported inside provider factories; `playwright` is imported inside `_get_local_page()`;
office/PDF libs inside `tools/builtin/read.py` parse functions; `sandbox/__init__.py` registers
docker/e2b via `try/except`. The `gateway/` is a **separate distribution**. So the 118-package
tree is an **install-time** cost; at **runtime only what a given agent path touches is loaded**.
This is the answer to "I don't need most of these": neither OpenUltraSAST (SAST) nor the
author's DocuHarnessX (docs) loads the bulk of it — HarnessX simply declares every capability as
a *mandatory* dependency.
| Category | Packages | SAST verdict |
|---|---|---|
| LLM SDKs | anthropic, openai, litellm, tiktoken, tokenizers, huggingface-hub, hf-xet, jiter, regex, tqdm, fsspec | **needed** |
| HTTP / async / pydantic core | httpx(+core/h11/h2/hpack/hyperframe), anyio, aiohttp(+glue), pydantic(+core/settings), certifi, urllib3, requests, sniffio | **needed** |
| Config / CLI / logging | hydra-core, omegaconf, antlr4-runtime, typer, click, rich, prompt-toolkit, structlog, loguru, jsonschema(+glue) | **needed** |
| Browser automation | **playwright, pyee, greenlet, html2text** | **dead weight** (+ out-of-band Chromium binary download via `playwright install`) |
| Sandbox backends | docker, dockerfile-parse, **e2b** | docker = useful (isolate hunter); **e2b = dead weight** |
| Web gateway / Lab UI | **fastapi, uvicorn, starlette, sse-starlette, httptools, websockets, uvloop, watchfiles, python-multipart, aiofiles** | **dead weight** (server surface; never started by a headless embed) |
| gRPC + OpenTelemetry | grpcio, protobuf, googleapis-common-protos, opentelemetry-* (8) | **optional** (tracing only) |
| Office / PDF | **python-pptx, python-docx, openpyxl, reportlab, pdfplumber, pdfminer-six, pypdfium2, pillow, xlsxwriter, lxml, et-xmlfile** | **dead weight** (SAST reads source, not documents) |
| MCP | mcp, httpx-sse | optional (only if the agent calls MCP tools) |
Roughly **55–60 packages are opt-in capability weight** pulled in mandatorily, including a
browser binary and a Docker SDK — an awkward footprint for a supply-chain auditing tool.
## Decision
**ACCEPT HarnessX as the optional, SHA-pinned extra `openultrasast[harnessx]`, lazy-imported
behind `_has_harnessx()`. Keep a pre-specified vendored lean subset as a documented, triggered
fallback. Pursue upstream dependency-extras as the preferred long-term slimming path.**
Rationale:
1. **Footprint is fully opt-in.** Core `dependencies = []` stays zero-dep; the governance /
scoring / benchmark / detection planes never pull HarnessX. Only `openultrasast[harnessx]`
installs the tree, and lazy imports keep heavy backends cold unless the path runs.
2. **Author precedent.** DocuHarnessX already ships `harnessx` as a direct git dependency — the
`ModelConfig.agentic` integration is proven and maintained. OpenUltraSAST reuses the
mechanism but tightens the posture (opt-in, not direct).
3. **Lower maintenance than vendoring.** Accept + SHA-pin gives reproducibility without a fork;
vendoring is paid only if the *installed* graph is itself judged unacceptable.
### Preferred long-term: upstream dependency-extras (helps both projects)
The clean fix for "I don't need most of these" is to modularize HarnessX's own dependencies into
extras — e.g. `harnessx[browser]`, `harnessx[sandbox-e2b]`, `harnessx[gateway]`, `harnessx[office]`,
`harnessx[otel]` — leaving a lean agent-runtime core (`anthropic`/`openai`/`litellm`/`pydantic`/
`httpx`/`hydra`/`structlog`/`mcp`). Since the author owns the `Darwin-Agent/HarnessX` line, this is
actionable and benefits DocuHarnessX equally. OpenUltraSAST would then depend on
`harnessx` (lean core) under its `[harnessx]` extra and add only the subsets it needs.
### Vendored lean subset (fallback, fully specified)
If triggered, vendor `harnessx/` only:
- **KEEP**: `core/`, `processors/{control,evaluation,observability,tools,context,memory,multi_model}`,
`meta_harness/`, `rl/`, `providers/`, `tools/builtin/` (minus `browser.py`/`web_*`),
`sandbox/{base,local,docker}.py`, plus `config`/`tracing`/`plugins`/`bundles`/`workspace` as needed.
- **REMOVE**: `frontend/`, `gateway/`, `harnessx/api/` (drops the web-server stack), `examples/`,
`docs/`, `benchmarks/`, `container/`, `scripts/`, `recipe/` (incl. the `recipe/verl_harnessX/verl`
submodule — clone non-recursive regardless), `extensions/skills/{docx,pdf,pptx,xlsx}` (drops
office/PDF), `harnessx/sandbox/e2b.py` (drops e2b), `harnessx/tools/builtin/browser.py`
(drops playwright + browser binary). Net ≈ 40–60% fewer packages, no browser binary.
## Residual risks (security-tool-specific) and mitigations
| Risk | Mitigation |
|---|---|
| `playwright install` fetches a Chromium binary out-of-band (unaudited, breaks hermetic builds) | Browser path is lazy and never invoked; document that `[harnessx]` does not trigger a browser download; lean subset deletes `browser.py` |
| `docker` SDK can reach the Docker daemon socket (privilege boundary) | Docker sandbox **default-off**, `LocalSandbox` default; run untrusted target code only inside the sandbox; SHA-pin docker/cryptography/cffi |
| FastAPI/uvicorn listening-socket surface in a security tool | `api/` server only starts on the explicit `serve` path; add a guard test asserting no server starts on import; lean subset removes `api/` |
| 118-package CVE surface in a supply-chain tool | SHA-pin the entire extra; keep core **zero-dep**; add a **zero-dep CI guard test** (importing `openultrasast` without the extra pulls nothing outside stdlib) |
| `recipe/verl` git submodule recursion | Init non-recursively; never in the accept-as-extra install path |
> Decision date 2026-06-22. The `pip install --dry-run` audit is itself the Phase-3 gate: it is
> re-run before the packaging task (5.2), and if the realized graph pulls a browser binary /
> Docker SDK / FastAPI stack as *mandatory* (non-lazy) deps judged unacceptable, the spec switches
> to the vendored lean subset under the same extra name.
@@ -1,614 +0,0 @@
# Design Document
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Overview
**Purpose.** This feature splits OpenUltraSAST's single overloaded regex layer — which today is detector, severity authority, and volume knob at once — into three governed owners: **rules detect**, a **central CWE policy decides severity and scope**, and a **single project score** becomes the optimization target. On top of that governance plane it adopts the HarnessX runtime exactly where it pays off — the LLM hunter loop, the LLM-judge verifier, and a meta-harness self-improvement loop that adapts rules and policy gates under hard bounds.
**Users.** OpenUltraSAST maintainers gain a staged, reversible HarnessX adoption and an automatic, gated rule/policy tuning loop. Security policy owners gain a versioned, central CWE-to-severity policy. Security reviewers and CI consumers gain a bounded 0-100 AppSec project score with a two-condition gate.
**Impact.** Today `findings.PatternRule` carries a string `severity`, `quick_scan_findings` reads a module-level `PATTERN_RULES`, `calibration.py` only demotes ranking (floored at `MIN_CALIBRATED_PRIORITY = 0.1`, keyed on `tags[0]`), `benchmark.py:368` writes an inert `next_improvement_candidate="rules_or_language_hunter"`, and `harness.HarnessRuntime.run_stage` is a sync dict-state driver. This design turns rules into TOML data, removes `PatternRule.severity`, sources severity from a vendored verycode `CWE_Score.tsv`, computes a project score, makes the benchmark actionable, and confines HarnessX behind an optional, SHA-pinned, lazy-imported extra — while `dependencies = []` and `requires-python = ">=3.11"` stay intact and the **≥90% recall / <10% FP** detection gate holds at every phase.
### Goals
- Rules become versioned data (`ruleset/<lang>/*.toml`) with a loop-controlled `status` and no rule-local severity.
- One CWE = one governed severity, sourced from `policy/CWE_Score.tsv` via `resolve_severity()`.
- A single 0-100 `project_score` from `policy.severity × reachability`, with a two-condition CI gate.
- HarnessX adopted at the hunter loop, the verifier, and the meta-harness, behind `openultrasast[harnessx]`.
- A bounded self-improvement loop (`rule`, `policy` levers) that auto-reverts byte-for-byte under the gate.
- The governance + scoring + benchmark planes stay **zero-dependency** and **byte-identical by default** through Phases 0-2.
### Non-Goals
- The loop authoring or rewriting detection pattern text (human PR only).
- The loop editing the authoritative 0-5 CWE severity (verycode-upstream authority).
- A full single-step HarnessX rewrite of all deterministic stages and weight-level RL (deferred to optional Phase 5+).
- Changing the reachability vocabulary, which stays exactly `reachable | inferred-file-surface | unknown`.
- Dynamic/DAST scoring — report-only for a static tool.
## Boundary Commitments
### This Spec Owns
- The **RulesetStore** (`ruleset/store.py` + `ruleset/<lang>/*.toml`): `load_ruleset()`, the `PatternRule` schema (detection-only), and the `enabled|shadow|disabled` status vocabulary.
- The **PolicyStore** (`policy/verycode.py` + `policy/CWE_Score.tsv`): `load_policy()`, `CwePolicy`, `resolve_severity()`, and the CWE-120→CWE-121 remap.
- The **ScoreModel** (`scoring/project_score.py`): `SEV_WEIGHT`, `REACH_MULT`, `finding_penalty`, `project_score`, the score artifact, and the two-condition CI gate.
- The **loop ledger** (`.openultrasast/calibration/rule_policy.json`): the loop-owned overlay of `status`/evidence-floor/threshold/precision.
- The **HarnessX seam**: `harness/build_sast.py`, `HxScanOrchestrator`, the `MultiHookProcessor` SAST processors, the `SlotContractMixin`, and the `improve/` meta-harness driver.
- The **benchmark→signal** conversion: `rule_id` attribution, per-rule precision/recall, `rule_policy_delta`, and `rule_signals.json`.
### Out of Boundary
- The detection-pattern **content** of rules (authored by humans via PR; the loop may only toggle status / raise floors).
- The authoritative **0-5 severity** numbers in `CWE_Score.tsv` (verycode-upstream; PR gaps there).
- Upstream **HarnessX internals** beyond the minimal meta-harness patch (`_VALID_LEVERS`, `_render_task_brief`, `EvolveValidator`, changeset buckets, `contract_autocheck`).
- The DAST/dynamic execution engine; dynamic-only CWEs are report-only here.
- Retiring `HarnessRuntime` and adopting `rl/` — explicitly Phase 5+, optional.
### Allowed Dependencies
- **Stdlib only** for the governance/scoring/benchmark planes: `tomllib`, `csv`, `json`, `math`, `dataclasses`.
- **HarnessX** only behind the `[harnessx]` extra, pinned to a commit SHA, imported lazily behind `_has_harnessx()`.
- The existing `HarnessRuntime` as the top-level sync driver through Phases 0-4 (not removed by this spec until Phase 5).
- The existing reachability vocabulary emitted by `findings.py` (consumed, never redefined).
### Revalidation Triggers
- **Contract shape changes** to `PatternRule` (removing `severity`), `CwePolicy`, `VerificationResult`, `BenchmarkMiss`/`BenchmarkFalsePositive` (adding `rule_id`), or the `project_score` artifact JSON.
- **Data ownership change**: severity moving from rule → policy; calibration re-keying from `tags[0]` → `rule_id`.
- **Dependency direction change**: any new import of HarnessX outside a lazy `_has_harnessx()` guard; any addition to core `dependencies`.
- **Runtime prerequisite change**: the startup CWE-resolution invariant (fail-loud if an enabled rule's CWE is unmapped); the `pip install --dry-run` audit gate before Phase 3.
- **Lever surface change**: any addition to `journal._VALID_LEVERS` or the `EvolveValidator` bounds.
## Architecture
### Existing Architecture Analysis
| Existing element | File | Constraint to respect / debt to fix |
|---|---|---|
| `HarnessRuntime.run_stage(name, callback)` | `harness.py:74` | Sync, dict `state` (`{"degradations": []}`), single `HarnessEvent` (`scan_id`), `ProcessorSpec.reads/writes` allow-list. Incompatible with HarnessX async-generator + message-window contract — **bridge, do not map**. Stays as the top-level driver through Phase 4. |
| `PatternRule.severity: str` + module-level `PATTERN_RULES` | `findings.py:31,34` | The fused defect: regex carries its own severity string ("high"/"medium"/"low") and is the only volume control. **Remove `severity`; externalize patterns to TOML.** |
| `quick_scan_findings(root, targets, rankings)` | `findings.py` / `cli.py:126` | Reads `PATTERN_RULES` implicitly. Becomes `quick_scan_findings(..., ruleset)` wired like `static_hints`. |
| `calibrate_rankings` demotion | `calibration.py:146` | `max(MIN_CALIBRATED_PRIORITY, calibrated - 0.5)`, keyed on `finding.tags[0]` (`calibration.py:262`). Demotes ranking, never the rule. **Re-key to `rule_id`; write status flips to ledger.** |
| `verify_findings` rejects on `reachability_status == "unknown"` | `verification.py:91` | Structural verifier. Keep as **zero-dep fallback**; add LLM-judge path. |
| `benchmark.py:368` `next_improvement_candidate="rules_or_language_hunter"` | `benchmark.py` | Inert stub written to `calibration_records.json`. **Make actionable** via `rule_id` attribution + `rule_policy_delta`. |
| `cli.py` `run_stage(...)` call chain | `cli.py:104-145` | The 13-stage sync pipeline. Stages migrate to HarnessX hooks incrementally; the hunter pool first. |
### Architecture Pattern & Boundary Map
**Selected pattern.** Three planes with one-directional authority: **governance data drives execution; execution emits signals; the improvement loop edits governance under a gate.** Nothing in execution mutates governance directly. The governance + scoring planes are **pure stdlib, zero HarnessX** (non-negotiable, so CI gating runs in the default install). HarnessX lives only in the execution and improvement planes, behind the extra.
**Boundaries.** RulesetStore detects, PolicyStore decides severity/scope, ScoreModel optimizes — three owners, three change channels (human PR for patterns/severity; loop-via-`EvolveValidator` for status/floors/score-constants).
```mermaid
flowchart TB
subgraph GOV["GOVERNANCE PLANE — stdlib, versioned, audited, ZERO-DEP"]
direction LR
POL["PolicyStore<br/>policy/verycode.py<br/>CWE_Score.tsv -> CwePolicy<br/>severity 0-5, static/dynamic"]
RUL["RulesetStore<br/>ruleset/store.py<br/>*.toml: pattern, cwe, status"]
LED["rule_policy.json<br/>loop-owned: status/floor/precision"]
SCO["ScoreModel<br/>scoring/project_score.py<br/>policy-weighted -> 0-100 + gate"]
FPL["FP ledger<br/>false_positive_learnings.json"]
end
subgraph EXE["EXECUTION PLANE — HarnessX runtime, OPTIONAL EXTRA"]
direction LR
DET["Deterministic stages<br/>skip_model MultiHookProcessors"]
HUNT["hunter_pool<br/>HxScanOrchestrator (real LLM loop)"]
VER["VerifyProcessor<br/>llm_judge helpers (+ structural fallback)"]
PSP["PolicyScoringProcessor + ScoreProcessor"]
end
subgraph IMP["IMPROVEMENT PLANE — meta_harness, OPTIONAL EXTRA"]
EVO["improve/evolve.py -> MetaAgent.evolve()<br/>levers: rule | policy"]
BEN["BenchmarkHarness<br/>recall/FP + per-rule attribution"]
end
POL --> DET & PSP & SCO
RUL --> DET & HUNT
LED --> RUL
DET --> HUNT --> VER --> PSP --> SCO
VER --> FPL
EXE -->|trajectories + verdicts| BEN
BEN -->|rule_signals.json + deltas| EVO
EVO -->|gated edits| RUL & POL & LED
SCO -->|gate verdict| CI[("CI gate")]
BEN -->|>=90% recall / <10% FP| CI
```
**New components rationale.** PolicyStore/ScoreModel/RulesetStore exist to break the fused regex defect; `HxScanOrchestrator`/`VerifyProcessor` exist to host the agent loop and judge; `improve/evolve.py`+`EvolveValidator` exist to make adaptation safe (bounded, gated, reversible). **Steering compliance:** zero-dep default install preserved; detection gate kept as a hard constraint separate from the score reward.
### Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|-------|------------------|-----------------|-------|
| CLI | `cli.py` (`_run_scan`) | Wires ruleset/policy; Phase 3 routes hunter pool to `HxScanOrchestrator` | Unchanged entrypoints when extra absent |
| Governance | Python 3.11 stdlib (`tomllib`, `csv`, `dataclasses`) | RulesetStore + PolicyStore load | Zero-dep, fail-loud on unmapped CWE |
| Scoring | Python stdlib `math` | `project_score` exponential decay | Always on, zero-dep |
| Execution runtime | HarnessX `@ git+...@<SHA>` (optional extra) | Hunter loop, judge verifier, processors | Lazy import behind `_has_harnessx()` |
| Self-improvement | HarnessX `meta_harness` (extra/vendored) | `MetaAgent.evolve` + `EvolveValidator` + journal | 5/6-hook minimal patch |
| Data / Storage | TOML rulesets, TSV policy, JSON ledger/artifacts | Versioned governance + loop overlay | All `_target_`/JSON-serializable for resume |
| Events | HarnessX lifecycle hooks (`on_task_start`…`on_task_end`) | Stage→hook mapping | `HarnessRuntime` sync driver retained Phases 0-4 |
| Packaging | `pyproject.toml` `[project.optional-dependencies]` | `harnessx` extra, pinned SHA | Core `dependencies = []` unchanged |
## File Structure Plan
### Directory Structure
```
src/openultrasast/
├── ruleset/
│ ├── store.py # load_ruleset()/write_ruleset(); PatternRule (NO severity); status/min_evidence_level/precision_estimate/version
│ ├── python/*.toml # rules-as-data, one [[rule]] per detector
│ ├── c/*.toml # incl. CWE-120 -> CWE-121 remapped buffer rules
│ └── <lang>/*.toml # same pattern per language
├── policy/
│ ├── verycode.py # load_policy() -> dict[str, CwePolicy]; resolve_severity(); CwePolicy dataclass
│ └── CWE_Score.tsv # vendored verycode data (167 CWEs; header "Flaw Severity " has trailing space)
├── scoring/
│ └── project_score.py # SEV_WEIGHT, REACH_MULT, finding_penalty(), project_score(), gate()
├── harness/ # Phase 3 (extra)
│ ├── build_sast.py # build_sast() factory mirrors examples/coding/build_coding
│ ├── task.py # ScanTask(BaseTask)
│ ├── run.py # HxScanOrchestrator: asyncio.run around model_config.agentic(cfg).run(...)
│ ├── slot_contract.py # SlotContractMixin: reads_slots/writes_slots on MultiHookProcessor
│ └── processors.py # SarifIngest/Preprocess/EntryPoint/Rank/Calibrate/QuickFindings/PolicyScoring/Verify/Score processors
└── improve/ # Phase 4 (extra)
├── evolve.py # orchestrator: benchmark -> delta -> rule_signals.json -> MetaAgent.evolve() -> re-benchmark -> accept/revert
├── rule_validator.py # EvolveValidator._rules / ._policy bound checks
└── rule_autocheck.py # ContractAutoCheckProcessor _check_rule/_check_policy branches
.openultrasast/calibration/
└── rule_policy.json # loop-owned ledger: status/threshold/evidence-floor/precision per rule_id
```
### Modified Files
- `findings.py` — extract `PATTERN_RULES` to TOML; **delete `PatternRule.severity`**; `quick_scan_findings(root, targets, rankings, ruleset)` takes ruleset arg.
- `calibration.py` — re-key from `tags[0]` (line 262) to `rule_id`; write `rule_policy.json` status flips, not only ranking demotion (line 146).
- `benchmark.py` — add `rule_id` to `BenchmarkMiss`/`BenchmarkFalsePositive`; per-rule precision/recall in `BenchmarkMetrics`; emit `rule_policy_delta`; replace the inert stub at line 368; add `BenchEvaluator` subclass.
- `verification.py` — add LLM-judge path (`VerifyProcessor` over `llm_judge` helpers); keep structural verifier (`reachability_status == "unknown"` reject, line 91) as zero-dep fallback.
- `config.py` — ruleset/policy/score paths + gate thresholds (`K`, `MIN_SCORE`, `min_emit_priority`, `min_emit_precision`).
- `cli.py` — `_run_scan` wires ruleset/policy; Phase 3 routes the `hunter_pool` `run_stage` call (line 122) to `HxScanOrchestrator`.
- `harness.py` — retired in Phase 5 only.
- `pyproject.toml` — add `[project.optional-dependencies] harnessx = ["harnessx @ git+https://github.com/Darwin-Agent/HarnessX.git@<SHA>"]`; core `dependencies = []` unchanged.
- `harnessx/meta_harness/{journal.py, agent.py, validate_workflow.py, processors/contract_autocheck.py}` — vendored/patched seam (Phase 4).
## System Flows
### Scan pipeline on HarnessX (Phase 3 seam)
The top-level driver stays sync (`HarnessRuntime`); only the `hunter_pool` stage opens an async surface via `asyncio.run`. Deterministic stages run as `skip_model=True` processors carrying work in typed slots; the `SlotContractMixin` validates `reads_slots`/`writes_slots` on every hook (preserving the `ProcessorSpec.reads/writes` discipline). If a HarnessX capability is unavailable at runtime, the stage falls back to its existing equivalent and records a degradation in the manifest.
```mermaid
flowchart TD
A["on_task_start: SarifIngestProcessor<br/>-> slot sast.static_hints"] --> B["Preprocess -> sast.targets"]
B --> C["EntryPoint -> sast.entry_points"]
C --> D["on_step_start: Rank -> sast.rankings"]
D --> E["Calibrate (MemoryRetrieval pattern over FP ledger)<br/>demotes sast.rankings"]
E --> F{"HarnessX extra<br/>present & capable?"}
F -->|yes| G["hunter_pool: HxScanOrchestrator<br/>asyncio.run(model_config.agentic(cfg).run(ScanTask))<br/>control/tools/obs processors -> StatefulTrajectory"]
F -->|no / capability missing| G2["fallback run_hunter_pool()<br/>record degradation in manifest"]
G --> H["QuickFindings: regex over load_ruleset()<br/>-> sast.findings (NO severity yet)"]
G2 --> H
H --> I["PolicyScoringProcessor: resolve_severity(policy, rule.cwe)<br/>+ emission decision; drop shadow findings"]
I --> J{"reachability_status<br/>== unknown?"}
J -->|yes| K["VerifyProcessor: REJECTED (structural fallback)<br/>or llm_judge verdict"]
J -->|no| K2["VerifyProcessor: llm_judge build/parse/call<br/>(structural fallback if extra absent)"]
K --> L["record_calibration: lesson/missing -> FalsePositiveLearning<br/>+ per-rule outcome -> rule_policy.json"]
K2 --> L
L --> M["on_task_end: ScoreProcessor -> project_score + gate"]
M --> N["report / sarif / manifest (incl. score block)"]
```
### Self-improvement loop
The benchmark is the **regression suite governing the data stores**. The objective is `maximize project_score subject to recall ≥ 90% AND FP < 10%` as a hard feasibility constraint — kept separate so the loop cannot "improve" the number by shadowing rules that hurt real security. Every change is `shadow`-staged first, novelty/evidence/replay-gated, and reverted byte-for-byte on any failure.
```mermaid
sequenceDiagram
participant B as BenchmarkHarness
participant O as improve/evolve.py
participant M as MetaAgent.evolve()
participant V as EvolveValidator (_rules/_policy)
participant J as journal (compute_attribution/fill_gating)
participant L as rule_policy.json
B->>O: misses(recall) + FPs/verifier-rejects(precision) + per-rule metrics
O->>O: write rule_signals.json [{rule_id, signal, cwe, evidence, suggested_action}]
O->>M: evolve(brief + rule_signals.json)
M->>M: write ruleset/*.toml + policy/overrides.toml + candidates.md + journal Round N
M->>V: validity(canon -> contract -> _rules/_policy -> replay) + policy(novelty + evidence)
alt validation fails
V-->>M: StrictValidationError(kind="rule_change") -> round rejected
else passes
V->>B: re-run benchmark on candidate
B->>J: passed_now vs passed_before
J->>J: compute_attribution(predicted): flipped/still_F/regressed/still_T/absent
alt recall<90% OR FP>=10% OR score regressed OR unpredicted regression
J->>L: fill_gating(reverted) -> restore config byte-for-byte
else
J->>L: fill_gating(accepted) -> persist status/floor/precision
end
end
```
## Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|-------------|---------|------------|------------|-------|
| 1.1, 1.4 | Hunter pool on real `Harness.run()`; async confined to one stage | `HxScanOrchestrator` | `run.py:HxScanOrchestrator.run()` | Scan pipeline |
| 1.2 | Verify via llm-judge + structural fallback | `VerifyProcessor` | `processors.py:VerifyProcessor` | Scan pipeline |
| 1.3 | Extra absent → unchanged quick/standard | `cli._run_scan`, `_has_harnessx()` | capability guard | Scan pipeline (`F` branch) |
| 1.5 | Slot allow-list validated per hook | `SlotContractMixin` | `slot_contract.py` | Scan pipeline |
| 1.6 | Deterministic stages `skip_model` | SAST processors | `MultiHookProcessor(skip_model=True)` | Scan pipeline |
| 1.7 | Event/`scan_id` rename to HarnessX hooks/`run_id` | `build_sast`, journal | hook map table | Scan pipeline |
| 1.8 | Capability-missing fallback + manifest degradation | `HxScanOrchestrator` | manifest `degradations` | Scan pipeline (`G2`) |
| 2.1, 2.4 | Rules-as-data; 3 statuses; no rule severity | RulesetStore | `load_ruleset()`, `PatternRule` | — |
| 2.2, 2.3 | Severity from policy; legacy strings discarded | PolicyStore, `PolicyScoringProcessor` | `resolve_severity()` | Scan pipeline (`I`) |
| 2.5 | Shadow excluded from report+score | `PolicyScoringProcessor`, ScoreModel | emission filter | Scan pipeline (`I`,`M`) |
| 2.6 | CwePolicy 0-5 + static/dynamic; dynamic report-only | PolicyStore | `CwePolicy` | — |
| 2.7 | Fail-loud on unmapped enabled-rule CWE | PolicyStore startup invariant | `load_policy()` check | — |
| 2.8 | CWE-120 → CWE-121 remap | RulesetStore | TOML data | — |
| 2.9, 5.9, 7.5 | Governance/score/benchmark zero-dep | all GOV components | stdlib-only | — |
| 3.1 | Two levers `rule`/`policy` | `improve/evolve.py` | `_VALID_LEVERS` | Self-improvement |
| 3.2, 3.3 | No pattern text / no 0-5 severity edits | `EvolveValidator` | `_rules`/`_policy` | Self-improvement |
| 3.4 | No enabled→disabled jump (shadow mandatory) | `EvolveValidator._rules` | bound check | Self-improvement |
| 3.5 | Parse/status/bounds/dup/CWE-resolve checks | `EvolveValidator._rules` | validity phase | Self-improvement |
| 3.6 | Novelty gate on reverted re-proposals | `journal.check_novelty` | novelty gate | Self-improvement |
| 3.7 | Replay smoke gate (ruleset boots) | `run_synthetic_task_smoke_gate` | replay phase | Self-improvement |
| 3.8, 3.9 | `StrictValidationError`; journal each edit | `EvolveValidator`, journal | `fill_gating` | Self-improvement |
| 4.1, 4.2 | `rule_id` attribution; per-rule metrics; delta artifact | BenchmarkHarness | `rule_policy_delta` | Self-improvement |
| 4.3 | Delta → `rule_signals.json` (miss/fp) | `improve/evolve.py` | signal writer | Self-improvement |
| 4.4, 4.5 | Hard accept gate; byte-for-byte revert | `improve/evolve.py`, journal | `fill_gating(reverted)` | Self-improvement |
| 4.6 | Auto-shadow precision-draggers | `improve/evolve.py` | status flip | Self-improvement |
| 4.7 | Coverage-gap nominates rule (human PR) | `improve/evolve.py` | `rule_signals.json` | Self-improvement |
| 4.8, 6.5 | Score=reward, gate=feasibility (separate) | `improve/evolve.py` | objective split | Self-improvement |
| 5.1, 5.2 | 0-100 score; reachability multipliers | ScoreModel | `project_score()` | Scan pipeline (`M`) |
| 5.3 | Dynamic-only/unmapped → 0 penalty, report-only | ScoreModel | `finding_penalty()` | Scan pipeline (`M`) |
| 5.4 | Shadow excluded from score input | ScoreModel | pre-call filter | Scan pipeline (`M`) |
| 5.5 | Score artifact + manifest block | ScoreModel | score JSON | Scan pipeline (`N`) |
| 5.6 | FP lowers effective reachability, not delete | `calibration.py` | reachability knob | Self-improvement |
| 5.7 | Hard gate: sev5 + reachable → fail | CI Score Gate | `gate()` | — |
| 5.8 | Score gate advisory-first, then blocking | CI Score Gate | `gate()` | — |
| 6.1, 6.4, 6.6 | Per-merge recall/FP gate + HX tolerance | CI Pipeline | benchmark gate | — |
| 6.2 | Byte-identical baseline Phases 0-2 | BenchmarkHarness | golden diff | Migration |
| 6.3 | Gate must pass on re-benchmarked candidate | `improve/evolve.py` | accept gate | Self-improvement |
| 7.1, 7.6 | Empty core deps; pinned extra; non-recursive submodule | Packaging Config | `pyproject.toml` | Migration |
| 7.2 | Lazy import behind capability check | `_has_harnessx()` | guard | — |
| 7.3, 7.4, 7.7 | Dry-run audit; vendored fallback trigger; pre-specified | Packaging Config | audit gate | Migration (Phase 3 gate) |
## Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|-----------|--------------|--------|--------------|--------------------------|-----------|
| RulesetStore | Governance | Rules-as-data + status | 2.1,2.4,2.8 | tomllib (P0), rule_policy.json (P1) | State |
| PolicyStore | Governance | CWE→severity/scope authority | 2.2,2.6,2.7 | CWE_Score.tsv (P0) | State |
| ScoreModel | Scoring | 0-100 score + gate | 5.1-5.9 | PolicyStore (P0) | Batch, State |
| HxScanOrchestrator | Execution | Hunter loop on HarnessX | 1.1,1.4,1.8 | HarnessX (P0), RulesetStore (P1) | Service, Event |
| PolicyScoringProcessor | Execution | Resolve severity + emission | 2.2,2.3,2.5 | PolicyStore (P0) | Event |
| VerifyProcessor | Execution | LLM-judge + structural fallback | 1.2 | llm_judge (P1) | Event |
| EvolveValidator (`_rules`/`_policy`) | Improvement | Bound-check loop edits | 3.2-3.8 | journal (P0), PolicyStore (P1) | Service |
| improve/evolve.py | Improvement | Benchmark→signal→evolve→gate | 4.1-4.8,6.3 | BenchmarkHarness (P0), MetaAgent (P0) | Batch |
### Governance Layer
#### RulesetStore
| Field | Detail |
|-------|--------|
| Intent | Load detection rules from versioned TOML; carry loop-controlled status; carry no severity |
| Requirements | 2.1, 2.4, 2.8, 2.9 |
**Responsibilities & Constraints**
- `load_ruleset(path) -> tuple[PatternRule, ...]` replaces module-level `PATTERN_RULES`; loaded with `tomllib` (zero-dep).
- `PatternRule` carries `rule_id`, `languages`, `cwe`, `tags`, `pattern`, `status`, `min_evidence_level`, `precision_estimate`, `version` — **no `severity` field**.
- `status ∈ {enabled, shadow, disabled}` is the volume control; the loop overlays `status`/`min_evidence_level`/`precision_estimate` from `rule_policy.json`.
- C/C++ buffer rules (`c-unsafe-copy`, `c-unsafe-memory-copy`, `c-unsafe-scanf`) declare `cwe = "CWE-121"` (remapped from unmapped CWE-120).
**Dependencies** — Inbound: `quick_scan_findings` (P0). Outbound: `rule_policy.json` overlay (P1). External: `tomllib` (P0).
**Contracts**: State [x]
##### Service Interface
```python
def load_ruleset(path: Path, ledger: Path | None = None) -> tuple[PatternRule, ...]: ...
def write_ruleset(path: Path, rules: Iterable[PatternRule]) -> None: ...
@dataclass(frozen=True)
class PatternRule:
rule_id: str
languages: tuple[str, ...]
cwe: str # the ONLY policy join key
tags: tuple[str, ...]
pattern: str # human-PR-only; loop must never edit
status: str = "enabled" # enabled | shadow | disabled (loop-controlled)
min_evidence_level: str = "static_corroboration"
precision_estimate: float = 0.0
version: str = "1"
```
- **Preconditions:** every loaded rule has a non-empty `pattern` and a `CWE-NNN` `cwe`.
- **Postconditions:** ledger overlay applied; `status`/floors reflect `rule_policy.json`.
- **Invariants:** no `severity` attribute exists; `status` is one of the three literals.
#### PolicyStore
| Field | Detail |
|-------|--------|
| Intent | Authoritative CWE→severity/scope; resolve finding severity |
| Requirements | 2.2, 2.3, 2.6, 2.7, 2.8 |
**Responsibilities & Constraints**
- `load_policy() -> dict[str, CwePolicy]` keyed `"CWE-NNN"` from `CWE_Score.tsv` (167 CWEs; severity column header is literally `"Flaw Severity "` with a trailing space — parse defensively).
- `resolve_severity(policy, rule.cwe, target, ranking)` replaces every literal `rule.severity` read. **Policy always wins**; any legacy rule severity string is discarded.
- **Startup invariant (fail-loud):** every `enabled` rule's `cwe` must resolve in PolicyStore, else the scan aborts. `dynamic`-only CWEs are report-only.
**Dependencies** — Inbound: `PolicyScoringProcessor`, ScoreModel (P0). External: `csv`, `dataclasses` (P0).
**Contracts**: State [x]
##### Service Interface
```python
@dataclass(frozen=True)
class CwePolicy:
flaw_category: str
severity: int # 0..5 — AUTHORITATIVE; overrides any rule string; loop must never edit
static: bool # SAST scope gate
dynamic: bool # DAST scope — report out-of-scope, never scored by static tool
def load_policy(tsv: Path = CWE_SCORE_TSV) -> dict[str, CwePolicy]: ...
def resolve_severity(policy: Mapping[str, CwePolicy], cwe: str, target, ranking) -> int: ...
def assert_rules_resolve(rules, policy) -> None: # raises at startup if an enabled rule's CWE is unmapped
...
```
- **Invariants:** one CWE ⇒ one severity; two rules for the same CWE always receive identical severity.
### Scoring Layer
#### ScoreModel
| Field | Detail |
|-------|--------|
| Intent | Compute a 0-100 project score and the two-condition gate verdict |
| Requirements | 5.1-5.9 |
**Responsibilities & Constraints**
- Pure stdlib (`math`), always on, zero-dep — runs in the default install with no HarnessX.
- Excludes `shadow`-status findings before computing; assigns 0 penalty to dynamic-only/unmapped CWEs (reports them instead).
- The `REACH_MULT` keys are exactly the three values `findings.py` emits; the reachability multiplier is the **FP-calibration knob** (a confirmed FP lowers effective reachability, never deletes a rule).
- Emits the scan-level artifact and the manifest score block; computes the hard gate (sev5 + reachable) and the score gate (advisory-first).
**Contracts**: Batch [x] / State [x]
##### Service Interface
```python
SEV_WEIGHT = {5: 50, 4: 25, 3: 10, 2: 2, 1: 1, 0: 0}
REACH_MULT = {"reachable": 1.0, "inferred-file-surface": 0.6, "unknown": 0.4}
def finding_penalty(f, rule_cwe: str, policy) -> float:
pol = policy.get(rule_cwe)
if pol is None or not pol.static: # dynamic-only / unmapped -> report, don't score
return 0.0
return SEV_WEIGHT[pol.severity] * REACH_MULT.get(f.reachability_status, 0.4)
def project_score(findings, rule_cwe_by_id, policy, K: float = 60.0) -> int:
# shadow-status findings excluded before this call
P = sum(finding_penalty(f, rule_cwe_by_id[f.finding_id], policy) for f in findings)
return round(100 * math.exp(-P / K)) # P=0 -> 100; one critical-reachable -> ~43
def gate(findings, score, *, min_score: int, block_severity_reachable: int = 5,
blocking: bool) -> dict: ...
```
- **Postconditions:** artifact JSON (see Data Models) written next to per-finding output and merged into the manifest.
### Execution Layer
#### HxScanOrchestrator
| Field | Detail |
|-------|--------|
| Intent | Run the hunter pool on a real HarnessX agent loop, confining async to one stage |
| Requirements | 1.1, 1.4, 1.7, 1.8 |
**Responsibilities & Constraints**
- Invoked from the existing `hunter_pool` `run_stage` callback (`cli.py:122`); wraps `asyncio.run(model_config.agentic(harness_config).run(ScanTask(...)))`.
- `build_sast()` mirrors `examples/coding/build_coding`; `ModelConfig(main=hunter, judge=…, ranker=…, patcher=…).agentic(cfg)` keeps model info out of `HarnessConfig`.
- Each hunter sub-harness registers `LoopDetectionProcessor`, `CostGuardProcessor`/`TokenBudgetProcessor`, `ToolFailureGuard`, `ToolFilterProcessor`, `SelfVerifyProcessor`, `EpisodeMetricsProcessor`/`OTelProcessor`/`CheckpointProcessor`.
- Produces a `StatefulTrajectory` per target; rewards backfilled from verifier verdicts. On capability-missing, falls back to `run_hunter_pool()` and appends to manifest `degradations`.
**Dependencies** — External: HarnessX core + processors (P0, lazy). Inbound: `cli._run_scan` (P0).
**Contracts**: Service [x] / Event [x]
##### Event Contract
- Hook mapping: `stage_start`→`on_step_start`, `stage_end`→`on_step_end`, `scan_id`→`session_id`/`run_id`; `before/after_model|tool` direct. `HARNESSX_CONTRACT_MODE` (message-window) + `OUS_SLOT_CONTRACT` (key-level via `SlotContractMixin`) split the old single `contract_mode`.
#### PolicyScoringProcessor
| Field | Detail |
|-------|--------|
| Intent | Resolve policy severity per finding and make the emission decision |
| Requirements | 2.2, 2.3, 2.5 |
**Responsibilities & Constraints**
- Runs `on_after_tool`/`on_step_end`; reads `sast.rankings` + PolicyStore; writes resolved severity + emit-decision.
- Drops `shadow` findings from the reported set and the score input while recording their outcome for precision tracking.
- Applies `min_emit_priority`/`min_emit_precision` thresholds — the new volume controls that replace "every regex match becomes a finding".
**Contracts**: Event [x]
#### VerifyProcessor
| Field | Detail |
|-------|--------|
| Intent | LLM-judge verification with a zero-dep structural fallback |
| Requirements | 1.2 |
**Responsibilities & Constraints**
- Wraps `build_judge_prompt`/`parse_judge_response`/`_call_judge` (`llm_judge.py`) as a `judge`-role sub-harness; maps the strict-JSON verdict (`plausible|unsupported|hedging|…` + `lesson`/`missing`) to `VerificationResult`.
- **Fallback:** the existing structural verifier (`verification.py:91`, rejects on `reachability_status == "unknown"`) is used verbatim when the extra is absent or the judge is unavailable.
**Contracts**: Event [x]
### Improvement Layer
#### EvolveValidator (`_rules` / `_policy`)
| Field | Detail |
|-------|--------|
| Intent | Enforce the bounded-lever safety contract on every loop edit |
| Requirements | 3.2-3.8 |
**Responsibilities & Constraints** (the gate — runs in the *validity* phase beside `_contract`/`_replay`)
- `_rules`: edit parses; `status ∈ {enabled,shadow,disabled}`; `tighten`/`loosen` adjusts only threshold/evidence-floor **within configured bounds**; `add` doesn't duplicate a `rule_id`; **every `cwe` resolves in PolicyStore**; **pattern text unchanged** (reject otherwise); **no `enabled→disabled` jump** (require `shadow`).
- `_policy`: touches only evidence floor, scope, `K`/`MIN_SCORE` within bounds; **never the 0-5 severity**.
- On any failure: `raise StrictValidationError(kind="rule_change")` → the whole round is rejected.
**Contracts**: Service [x]
##### Service Interface
```python
_VALID_LEVERS = frozenset({
"configuration", "control", "action", "instruction",
"rule", # status/evidence-floor/threshold in ruleset/ — NEVER pattern text
"policy", # evidence floor + scope + K/MIN_SCORE — NEVER the 0-5 CWE severity
})
class EvolveValidator:
def _rules(self, edit) -> None: ... # raises StrictValidationError(kind="rule_change")
def _policy(self, edit) -> None: ...
```
#### improve/evolve.py (orchestrator)
| Field | Detail |
|-------|--------|
| Intent | Drive benchmark→signal→evolve→re-benchmark→accept/revert under the gate |
| Requirements | 4.1-4.8, 6.3 |
**Responsibilities & Constraints**
- Converts `rule_policy_delta` + FP learnings → `rule_signals.json` (recall side `signal="miss"`; precision side `signal="verifier_reject"|"fp"`); calls `MetaAgent.evolve()`.
- **Acceptance gate (hard):** round accepted **only if** aggregate recall ≥ 90% **AND** FP < 10% **AND** `project_score` did not regress on the held set **AND** no unpredicted regression; otherwise `fill_gating(outcome="reverted")` restores config byte-for-byte.
- Precision-dragging rules are **auto-shadowed**, never deleted; coverage gaps nominate a rule via `rule_signals.json` (pattern authoring stays human-PR).
- Objective: `maximize project_score s.t. recall/FP gate` — score is the reward, the gate is the feasibility region; never folded together.
**Contracts**: Batch [x]
## Data Models
### Ruleset policy schema (detection only — TOML)
```toml
[[rule]]
rule_id = "python-sql-injection"
languages = ["python"]
cwe = "CWE-89" # the ONLY policy join key
tags = ["injection"]
pattern = '(\.execute)\s*\(\s*[^,)]*%' # human-PR-only
status = "enabled" # enabled | shadow | disabled (loop-controlled)
min_evidence_level = "static_corroboration" # loop may raise within bounds
precision_estimate = 0.62 # backfilled from benchmarks
version = "3"
```
Status semantics: `enabled` fires/scores/reports; `shadow` fires and records outcomes but is excluded from report and score (mandatory staging state for any loop-proposed change); `disabled` does not fire.
Loop ledger (`rule_policy.json`) — loop-owned overlay, re-keyed on `rule_id`:
```json
{ "python-sql-injection": { "status": "shadow", "min_evidence_level": "dynamic_corroboration",
"precision_estimate": 0.41, "outcome": "accepted", "round": 7 } }
```
### Project-score schema (artifact + manifest block)
```json
{ "project_score": 73, "max_severity": 5, "penalty_total": 30.0,
"by_category": {"Command or Argument Injection": 1, "Cryptographic Issues": 2},
"out_of_scope_dynamic_only": 0,
"unmapped_cwe": [],
"gate": {"min_score": 80, "block_severity_reachable": 5, "blocking": false, "passed": false} }
```
### Consistency & Integrity
- **Authority direction:** governance data is the source of truth; execution reads it, emits findings/signals; only the loop (via `EvolveValidator`) writes `status`/floors/score-constants back, never the pattern or the 0-5 severity.
- **Temporal/versioning:** `PatternRule.version` and `rule_policy.json.round` give per-rule audit; the journal records each lever edit; `fill_gating(reverted)` restores byte-for-byte.
- **Serialization (resume-safe):** all scan slots (`sast.targets`, `sast.findings`, `sast.rankings`, verifications) use plain dicts/dataclasses with `_target_` registration so they survive `wake()`.
## Error Handling
### Error Strategy
| Error type | Condition | Response |
|---|---|---|
| **Startup config error** | An `enabled` rule's `cwe` is unmapped in PolicyStore | `assert_rules_resolve` raises; **fail loud**, abort scan before any work (Req 2.7) |
| **Malformed ruleset/policy TOML** | `tomllib` parse failure | Fail loud at load; CI catches before merge |
| **Loop edit violates bounds** | Pattern edit / severity edit / enabled→disabled / duplicate `rule_id` / unmapped CWE | `EvolveValidator` raises `StrictValidationError(kind="rule_change")`; entire round rejected (Req 3.8) |
| **Reverted re-proposal w/o rationale** | Novelty gate hit | `journal.check_novelty` blocks; loop continues with other candidates (Req 3.6) |
| **Replay smoke gate failure** | Edited ruleset/policy won't boot | Round rejected before benchmark re-run (Req 3.7) |
| **Acceptance-gate failure** | recall<90% OR FP≥10% OR score regressed OR unpredicted regression | `fill_gating(reverted)` restores config byte-for-byte (Req 4.5) |
| **HarnessX capability missing at runtime** | Extra absent or sub-harness unavailable | Stage falls back to existing equivalent; record degradation in manifest (Req 1.8) |
| **Slot contract violation** | Stage reads/writes an undeclared slot | `SlotContractMixin` raises under `OUS_SLOT_CONTRACT=strict`; warns under `warn` (Req 1.5) |
| **Slot serialization loss on resume** | Non-`_target_` slot restores as `None` | Per-slot-type round-trip test guards; `_target_` registration required |
### Monitoring
- `EpisodeMetricsProcessor.episode_summary` → manifest metrics block; `OTelProcessor` OTLP export when configured.
- `HarnessJournal` writes per-run `{run_id}.jsonl` / `_trace.jsonl` / `_state.json`, rotated on `SegmentBoundaryEvent`.
- The score artifact `unmapped_cwe` list surfaces policy-completeness drift; the journal changeset table surfaces every rule/policy lever edit per round.
## Testing Strategy
### Unit Tests
- `load_policy()` parses the `"Flaw Severity "` trailing-space header and yields 167 `CwePolicy` entries with correct `static`/`dynamic` flags.
- `project_score()` boundary cases: P=0 → 100; one severity-5 reachable finding → ~43; one low finding → ~97; shadow findings excluded.
- `resolve_severity()` returns policy severity (not the legacy rule string) and is identical for two rules sharing a CWE.
- `EvolveValidator._rules` rejects: pattern edit, severity edit, `enabled→disabled` jump, duplicate `rule_id`, unmapped CWE.
- `finding_penalty()` returns 0.0 for dynamic-only and unmapped CWEs.
### Integration Tests
- **Byte-identical baseline (Req 6.2):** with the default ruleset and an empty `rule_policy.json`, `benchmark.py` output matches the pre-feature golden corpus exactly.
- **Startup invariant (Req 2.7):** an enabled rule with an unmapped CWE aborts the scan with a loud error.
- **Self-improvement round (Req 4.4/4.5):** a deliberately noisy rule is auto-shadowed when it drags precision; a round that regresses recall is reverted byte-for-byte.
- **Capability fallback (Req 1.8):** extra absent → `quick`/`standard` run through the sync driver unchanged; degradation recorded when a HX stage is requested but unavailable.
- **Slot round-trip (Req on resume):** each scan slot type serializes and restores without loss after `wake()`.
### E2E / Gate Tests
- **CI detection gate (Req 6.1):** the pipeline fails the merge if recall<90% or FP≥10% on the corpus.
- **Two-condition score gate (Req 5.7/5.8):** hard gate fails on a sev5+reachable finding; score gate is advisory until `K`/`MIN_SCORE` are calibrated, then blocking.
- **Zero-dep guard (Req 7.5):** with HarnessX absent, governance/scoring/benchmark planes operate and CI gating runs in the default install.
### Performance / Tolerance
- **HX tolerance (Req 6.4):** an enabled HarnessX-backed stage keeps recall/FP within the configured tolerance of the zero-dep baseline.
- Hunter-loop cost/token budgets enforced by `CostGuardProcessor`/`TokenBudgetProcessor`.
## Security Considerations
- **Supply-chain posture (Req 7):** core `dependencies = []` is preserved; HarnessX ships only as a SHA-pinned optional extra, lazy-imported behind `_has_harnessx()`. A `pip install --dry-run` audit runs before Phase 3; if the realized transitive graph pulls browser binaries / Docker SDK / a FastAPI web stack as *mandatory* deps and maintainers judge it unacceptable for a supply-chain auditing tool, the pre-specified **vendored lean subset** (Option C) is switched in under the same extra name — a packaging change, not a redesign. Submodule init is **non-recursive** so the large `verl` RL fork is never pulled.
- **Loop containment:** the meta-agent can never invent or rewrite detectors (no pattern edits) or rewrite the severity authority (no 0-5 edits); `shadow`-first staging, novelty/evidence/replay gates, and byte-for-byte revert bound its blast radius — directly preventing the "too influential" defect from re-entering via the loop.
- **Gate integrity:** the recall/FP gate is a hard constraint that is never folded into the score reward, so the loop cannot "improve" the number by shadowing rules that hurt real security.
## Migration Strategy
Every phase is independently shippable, test-gated, and reversible. Phases 0-2 deliver governance value at **zero dependency cost** and stay byte-identical by default. Phases 3-4 are the HarnessX adoption behind the extra. Phase 5 is the optional native end-state.
```mermaid
flowchart LR
P0["Phase 0<br/>Policy + Score<br/>zero-dep, gate advisory"] --> P1["Phase 1<br/>Rules-as-data<br/>zero-dep, byte-identical"]
P1 --> P2["Phase 2<br/>Benchmark->signal + ledger<br/>zero-dep"]
P2 --> GATE{"pip --dry-run audit<br/>(D2 decision)"}
GATE -->|graph acceptable| P3B["Phase 3<br/>[harnessx] extra (Option B)<br/>hunter loop + verify"]
GATE -->|graph unacceptable| P3C["Phase 3<br/>vendored lean subset (Option C)<br/>same extra name"]
P3B --> P4["Phase 4<br/>Self-improvement loop live"]
P3C --> P4
P4 --> P5["Phase 5 (optional)<br/>full single-step harness + RL<br/>retire HarnessRuntime"]
```
| Phase | Scope | Dep cost | Merge gate | Reversibility |
|---|---|---|---|---|
| **0 — Policy & score** | Vendor `CWE_Score.tsv`; `policy/verycode.py` + `scoring/project_score.py`; CWE-120→121 remap; startup CWE-resolution invariant; emit score artifact + advisory gate. `HarnessRuntime`/`PATTERN_RULES` unchanged. | **zero** | benchmark byte-identical; score is additive output | gate disable flag |
| **1 — Rules as data** | Extract `PATTERN_RULES`→`ruleset/<lang>/*.toml`; delete `PatternRule.severity`; `load_ruleset()`; `resolve_severity`; add `status` + emit thresholds. | **zero** | benchmark **byte-identical** to Phase 0 | keep `PATTERN_RULES` fallback |
| **2 — Benchmark→signal + ledger** | `rule_id` on misses/FPs; per-rule precision/recall; emit `rule_policy_delta`; `rule_policy.json` ledger; calibration writes status flips re-keyed on `rule_id`. | **zero** | empty ledger == current behavior | empty ledger = no-op |
| **3 — HX execution adoption** | Run `pip --dry-run` audit (D2 gate). Add `[harnessx]` extra (pinned SHA, lazy, zero-dep CI guard); `build_sast()` + `HxScanOrchestrator`; hunter pool on real `Harness.run()`; `VerifyProcessor`; `PolicyScoring`/`ScoreProcessor`. `HarnessRuntime` stays. | **opt-in heavy** | extra-absent: unchanged; extra-present: recall/FP within tolerance | feature flag off |
| **4 — Self-improvement live** | Vendor/patch the meta-harness seam; orchestrator benchmark→delta→signal→`evolve()`→re-benchmark→accept/revert. | **opt-in** | ≥90% recall / <10% FP per-round; auto-revert | don't run `evolve` → ledger-only changes |
| **5 — Full harness + RL (optional)** | Migrate deterministic stages to single-step harness + `SlotContractMixin`; retire `HarnessRuntime`; promote score gate to blocking; optional `rl/`. | **opt-in** | golden-diff vs Phase 3 empty; gate held | per-stage revert |
**Rollback triggers.** Any phase that drops recall<90% or raises FP≥10% on the corpus blocks the merge; any self-improvement round failing the acceptance gate reverts byte-for-byte; an unacceptable `pip --dry-run` graph switches Phase 3 to the vendored subset.
@@ -1,120 +0,0 @@
# Requirements Document
> **Status 2026-10-02:** retired with HarnessX (see the note below). Its task 6.3 modules `stage_processors.py` and `slot_contract.py` were deleted on 2026-10-02: nothing imported them. Kept as decision history; nothing in src, tests or CI points here.
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Introduction
OpenUltraSAST today fuses three responsibilities into one regex layer: detection patterns also carry their own severity strings and act as the sole volume control, so a noisy rule can dominate scan output, set its own criticality, and fire forever with no central governance. This feature splits those responsibilities into three governed owners — rules detect, a central CWE policy decides severity and scope, and a single project score becomes the optimization target — and adopts the HarnessX runtime exactly where it pays off: the LLM hunter loop, the LLM-judge verifier, and a meta-harness self-improvement loop that adapts rules and policy gates under hard bounds.
The work spans five user asks. First, OpenUltraSAST shall adopt HarnessX directly at the hunter loop and migrate the existing scan flow stage-by-stage onto HarnessX processors. Second, detection rules shall become versioned data governed centrally, with severity sourced from the verycode CWE policy rather than from the rule itself. Third, the HarnessX meta-harness shall adapt rules and policy gates within bounded levers. Fourth, benchmark results shall drive that adaptation as actionable signals with a hard acceptance gate. Fifth, a Veracode-style project score shall be computed from policy-weighted, reachability-adjusted findings and gated in CI.
Two cross-cutting constraints govern every part of this feature. The existing detection gate of at least 90% recall and under 10% false-positive rate on the benchmark corpus must never regress, at any phase, for any change. And the HarnessX runtime must be adopted in a way that preserves the project's zero-dependency posture for the governance and scoring planes, so an explicit dependency-weight decision controls whether HarnessX ships as an optional extra or as a vendored lean subset.
## Boundary Context
- **In scope**: detection rules as versioned data; a central CWE-to-severity policy with static/dynamic scope; a single project score (0-100) with a two-condition CI gate; HarnessX adoption at the hunter loop, the verifier, and the self-improvement meta-harness; benchmark-to-signal conversion with per-rule attribution; bounded loop authority over rule status, evidence floors, thresholds, and score constants; a dependency-weight audit and decision; preservation of the recall/FP detection gate.
- **Out of scope**: authoring new detection pattern text by the loop (human pull-request only); editing the authoritative 0-5 CWE severity by the loop (verycode-upstream authority); a full single-step HarnessX rewrite of all deterministic stages and weight-level reinforcement learning (deferred, optional, later phase); changing the reachability vocabulary, which remains exactly `reachable | inferred-file-surface | unknown`; dynamic/DAST scoring, which is report-only for a static tool.
- **Adjacent expectations**: the governance and scoring planes shall run with no HarnessX present and `dependencies = []`; CI gating (score plus benchmark) shall run in the zero-dependency default install; HarnessX, when present, shall be confined behind a capability check and lazy imports so heavy code paths stay cold in the default install.
## Requirements
### Requirement 1: HarnessX Direct Adoption and Scan-Flow Migration
**Objective:** As an OpenUltraSAST maintainer, I want the scan flow to adopt the HarnessX runtime directly at the LLM hunter loop and migrate the existing stages onto HarnessX processors in a staged way, so that the project gains the HarnessX agent loop, journal, and trajectory machinery without a high-risk big-bang rewrite.
#### Acceptance Criteria
1. Where the HarnessX extra is installed, the Scan Orchestrator shall run the hunter pool stage on a real HarnessX `Harness.run()` loop with the HarnessX control, tool, and observability processors registered in the hunter sub-harness.
2. Where the HarnessX extra is installed, the Verify Processor shall produce verification verdicts using the HarnessX llm-judge helpers and shall retain the structural verifier as a zero-dependency fallback.
3. While the HarnessX extra is absent, the Scan Runtime shall execute `quick` and `standard` scans through the existing sync driver with behavior unchanged from the pre-adoption baseline.
4. When the hunter pool stage runs under HarnessX, the Scan Orchestrator shall confine all asynchronous execution to that single stage so that the deterministic stages remain synchronous.
5. The Scan Runtime shall preserve each stage's declared read and write slot allow-list and shall validate slot access on every lifecycle hook, so that the existing provenance and audit discipline is retained under the HarnessX runtime.
6. Where a deterministic stage runs under HarnessX, the Scan Runtime shall execute it with model invocation disabled so that no deterministic stage incurs a model call.
7. When migrating event names, the Scan Runtime shall map the existing `stage_start`/`stage_end` to the HarnessX step-level hooks and `scan_id` to the HarnessX run identifier without losing trace continuity.
8. If a HarnessX-backed stage is enabled and the corresponding HarnessX capability is unavailable at runtime, then the Scan Runtime shall fall back to the existing equivalent stage and shall record the degradation in the scan manifest.
### Requirement 2: Central Security-Affine Ruleset Governance
**Objective:** As a security policy owner, I want detection rules to be versioned data whose severity and scope come from a central CWE policy rather than from the rule itself, so that one CWE maps to one governed severity and no rule can be too influential by setting its own criticality or volume.
#### Acceptance Criteria
1. The Ruleset Store shall load detection rules from versioned data files in which each rule declares a pattern, a CWE, tags, and a loop-controlled `status`, and shall not carry any rule-local severity field.
2. When resolving the severity of a finding, the Policy Scoring Processor shall read severity exclusively from the Policy Store keyed on the rule's CWE and shall discard any severity string carried in legacy rule data.
3. While two enabled rules map to the same CWE, the Policy Scoring Processor shall assign both findings the identical policy-governed severity so that two rules for one CWE can no longer disagree.
4. The Ruleset Store shall support exactly three rule statuses — `enabled`, `shadow`, and `disabled` — where `enabled` fires and scores and reports, `shadow` fires and records outcomes but is excluded from the report and the score, and `disabled` does not fire.
5. When a finding's rule status is `shadow`, the Policy Scoring Processor shall exclude that finding from the reported findings and from the project-score input while still recording its outcome for precision tracking.
6. The Policy Store shall expose, per CWE, an authoritative 0-5 severity plus a static scope flag and a dynamic scope flag, and shall mark dynamic-only CWEs as report-only and never scored by the static tool.
7. If an enabled rule's CWE does not resolve in the Policy Store at startup, then the Scan Runtime shall fail loud and shall not proceed with the scan.
8. The Ruleset Store shall remap the C/C++ buffer-handling rules that reference the unmapped CWE-120 to CWE-121 (severity 5) so that those rules contribute a nonzero penalty to the project score.
9. The Ruleset Store and the Policy Store shall be loaded with the standard library only and shall require no third-party dependency.
### Requirement 3: Bounded Rule and Policy Adaptation in the Self-Improvement Loop
**Objective:** As an OpenUltraSAST maintainer, I want the HarnessX meta-harness to adapt rules and policy gates only through two bounded levers, so that the loop can suppress, stage, or raise the bar on noisy rules without ever inventing detectors or rewriting the severity authority.
#### Acceptance Criteria
1. The Self-Improvement Loop shall expose exactly two new edit levers: a `rule` lever covering rule status, evidence floor, and threshold, and a `policy` lever covering evidence floor, scope, and the score constants `K` and `MIN_SCORE`.
2. The Rule Edit Validator shall reject any loop edit that modifies detection pattern text, so that pattern authoring remains human-pull-request only.
3. The Policy Edit Validator shall reject any loop edit that modifies the authoritative 0-5 CWE severity, so that the severity authority remains upstream-governed.
4. If the loop proposes moving a rule directly from `enabled` to `disabled`, then the Rule Edit Validator shall reject the edit and require `shadow` as a mandatory intermediate staging state.
5. When validating a proposed edit, the Rule Edit Validator shall assert that the edit parses, that `status` is one of `enabled`/`shadow`/`disabled`, that any threshold or evidence-floor tightening or loosening stays within configured bounds, that an added rule does not duplicate an existing rule identifier, and that every referenced CWE resolves in the Policy Store.
6. If the loop re-proposes an edit that was previously reverted without supplying a retry rationale, then the Self-Improvement Loop shall block the re-proposal via the novelty gate.
7. Before accepting any round, the Self-Improvement Loop shall run a replay smoke gate that proves the edited ruleset and policy still load and boot.
8. When any loop edit is rejected during validation, the Self-Improvement Loop shall raise a rule-change validation error and reject the entire round.
9. The Self-Improvement Loop shall record each rule and policy edit, its lever, and its outcome in the journal so that every change is attributable and reversible.
### Requirement 4: Benchmark-Driven Iteration with a Hard Acceptance Gate
**Objective:** As an OpenUltraSAST maintainer, I want benchmark results to become actionable per-rule signals that drive the self-improvement loop under a hard acceptance gate, so that detection quality improves automatically while regressions are reverted byte-for-byte.
#### Acceptance Criteria
1. The Benchmark Harness shall attribute every recorded miss and every recorded false positive to a `rule_id` and shall aggregate per-rule precision and recall in its metrics.
2. The Benchmark Harness shall emit a per-rule recommendation artifact alongside its records, recommending enable, disable, shadow, threshold, or evidence-floor changes per rule.
3. When the orchestrator converts benchmark output to loop input, it shall write recall-side items as miss signals and precision-side items as verifier-reject or false-positive signals into the loop's signal file.
4. After the meta-agent edits rules or policy, the Self-Improvement Loop shall re-run the benchmark and shall accept the round only if aggregate recall is at least 90% AND aggregate false-positive rate is under 10% AND the project score did not regress on the held set AND no unpredicted regression occurred.
5. If a re-run round fails any acceptance condition, then the Self-Improvement Loop shall revert the round and restore the rule and policy configuration byte-for-byte.
6. If a rule drags aggregate precision below the gate, then the Self-Improvement Loop shall auto-shadow that rule rather than delete it, so that recall is preserved while the false-positive cost is removed.
7. When a coverage gap surfaces as a persistent miss, the Self-Improvement Loop shall nominate a new or loosened rule through the signal file while leaving pattern authoring to a human pull request.
8. The Self-Improvement Loop shall treat maximizing the project score as the optimization objective and the recall/FP gate as a separate hard feasibility constraint, so that the loop cannot improve the score by shadowing rules in a way that harms real security.
### Requirement 5: Project Scores via verycode Policies
**Objective:** As a security reviewer, I want a single 0-100 project score computed from policy-weighted, reachability-adjusted findings and gated in CI, so that a scan produces a bounded AppSec quality signal alongside its findings.
#### Acceptance Criteria
1. The Score Model shall compute a single 0-100 project score from policy-governed severity weights multiplied by a reachability multiplier over the reported findings, using the standard library only.
2. The Score Model shall use reachability multipliers keyed exactly on the three emitted reachability values `reachable`, `inferred-file-surface`, and `unknown`.
3. When a finding maps to a CWE that is dynamic-only or unmapped in the Policy Store, the Score Model shall assign that finding a zero score penalty and shall report it as out-of-scope or unmapped rather than scoring it.
4. The Score Model shall exclude findings whose rule status is `shadow` from the score input before computing the project score.
5. The Score Model shall emit a scan-level score artifact containing the project score, the maximum severity, the total penalty, a by-category breakdown, the out-of-scope dynamic-only count, the unmapped CWE list, and the gate verdict, and shall include the score block in the scan manifest.
6. When confirming a false positive, the Calibration subsystem shall lower the finding's effective reachability multiplier rather than delete the rule, so that recall stays high while the score stops over-penalizing.
7. If any reported finding maps to policy severity 5 AND has `reachability_status` equal to `reachable`, then the CI Score Gate shall fail the build.
8. Where the score gate is configured as blocking, the CI Score Gate shall fail the build when the project score is below `MIN_SCORE`; until the score constants are calibrated against the corpus, the CI Score Gate shall compute and report the score advisory-first without blocking.
9. The Score Model and the CI Score Gate shall run in the zero-dependency default install with no HarnessX present.
### Requirement 6: Detection Gate Non-Regression
**Objective:** As an OpenUltraSAST maintainer, I want the existing detection gate of at least 90% recall and under 10% false positives on the benchmark corpus to hold across every phase and every change, so that no governance, scoring, runtime, or self-improvement change can silently degrade detection quality.
#### Acceptance Criteria
1. The CI Pipeline shall evaluate aggregate recall and aggregate false-positive rate on the benchmark corpus on every merge and shall fail the merge if recall is below 90% or the false-positive rate is at or above 10%.
2. While the default ruleset and an empty loop ledger are in effect, the Benchmark Harness shall produce results byte-identical to the pre-feature baseline for the governance and rules-as-data phases.
3. When the self-improvement loop proposes any rule or policy change, the Self-Improvement Loop shall require the recall/FP gate to pass on the re-benchmarked candidate before the change can be accepted.
4. If a HarnessX-backed stage is enabled, then the CI Pipeline shall confirm recall and false-positive rate stay within the configured tolerance of the equivalent zero-dependency baseline.
5. The CI Pipeline shall treat the recall/FP gate as a hard constraint that is never folded into the project-score reward, so that improving the score can never substitute for meeting the detection gate.
6. If any change would reduce recall below 90% or raise the false-positive rate to 10% or above, then the CI Pipeline shall block the change regardless of any project-score improvement.
### Requirement 7: HarnessX Dependency-Weight Decision
**Objective:** As an OpenUltraSAST maintainer, I want an explicit dependency-weight decision that keeps the core install zero-dependency and chooses between an optional HarnessX extra and a vendored lean subset based on an audited dependency graph, so that a supply-chain auditing tool never silently inherits an unacceptable transitive dependency footprint.
#### Acceptance Criteria
1. The Packaging Configuration shall keep the core `dependencies` list empty and shall expose HarnessX only as an optional extra pinned to a specific commit revision.
2. The Runtime shall guard every HarnessX import behind a capability check and shall import HarnessX lazily so that heavy code paths stay cold in the default install.
3. Before adopting the HarnessX execution stages, the maintainer shall run a dependency dry-run audit and shall record the realized transitive dependency graph.
4. If the audited transitive graph pulls a browser-binary, container-SDK, or web-server stack as a mandatory dependency and the maintainers judge it unacceptable for a supply-chain auditing tool, then the Packaging Configuration shall switch to a vendored lean subset of HarnessX under the same extra name with the heavy subtrees removed.
5. While HarnessX is absent, the CI Pipeline shall run a zero-dependency guard test confirming the governance, scoring, and benchmark planes operate with no HarnessX present.
6. Where the project is developed via a submodule, the Runtime setup shall initialize HarnessX non-recursively so that the large reinforcement-learning fork subtree is not pulled.
7. The Packaging Configuration shall keep the vendored lean subset pre-specified so that switching from the optional extra to the vendored subset is a packaging change and not a redesign.
@@ -1,191 +0,0 @@
# Research & Design Decisions
---
**Purpose**: Capture the discovery findings and architectural investigation behind the `harnessx-self-improving-rulesets` spec. This is the background-notes companion to `design.md` — it records *what was read*, *what was found in the source*, and *why* the three resolved decisions came out the way they did. `design.md` carries the committed architecture; this file carries the evidence and the rejected alternatives.
---
## Summary
- **Feature**: `harnessx-self-improving-rulesets`
- **Discovery Scope**: Complex Integration (adopt an external agent-runtime library + re-architect the in-house rule/policy/scoring planes)
- **Key Findings**:
- HarnessX and OpenUltraSAST (OUS) already both have a thing called a "harness" with "processors" and "events", but they are **incompatible at every load-bearing point**: HarnessX is async-generator / message-window-contract / transcript-state; OUS is sync / declared-`reads`/`writes`-contract / dict-state. There is no drop-in.
- HarnessX's real, reusable value for OUS is concentrated in three places: the **LLM hunter agent loop** (control/tools/observability processors), the **llm-judge verifier** (`build_judge_prompt`/`parse_judge_response`/`_call_judge`), and the **meta-harness self-improvement engine** (journal / novelty / evidence / replay / attribution). Everything else (lab web UI, browser tools, sandboxes, office skills) is unused weight.
- OUS today fuses **detector + severity authority + volume control** into one regex layer (`PATTERN_RULES` in `findings.py`); its self-improvement loop only demotes *ranking* (floored at `0.1`, keyed on `tags[0]`), never the rules; and its benchmark already names the ruleset as the thing to fix (`next_improvement_candidate="rules_or_language_hunter"`) but writes it to an inert JSON. The signals exist; the loop back onto the rules does not.
- The `verycode` policy data (`CWE_Score.tsv`, 167 CWEs, severity 0-5 + static/dynamic scope) is **complete and authoritative**, but the `verycode-score` scoring *algorithm was never implemented* (`// scorer code here` placeholder). The score formula in the design is a labeled, defensible reconstruction.
- OUS today has **zero runtime dependencies** (`dependencies = []`); HarnessX's `dependencies` list is ~30 heavyweight packages (Playwright, Docker SDK, FastAPI/uvicorn, multi-LLM SDKs, office-doc suite) with **no lean extra** to pip-install a subset — a direct hard dependency would invert the project's supply-chain posture for a security tool.
## Research Log
### HarnessX core composition & runtime model
- **Context**: We need to know whether OUS's `harness.HarnessRuntime` can be replaced by, or interoperate with, HarnessX's `Harness`, and how an external project builds and runs a HarnessX harness.
- **Sources Consulted**: `/tmp/HarnessX/harnessx/core/{harness,builder,processor,events,runloop,state,contract_check,config_schema,config_store,model_config,trajectory}.py` (read in full); `/home/mc/Source/OpenUltraSAST/src/openultrasast/harness.py`.
- **Findings**:
- Public composition is three objects + one method: `HarnessBuilder.build() -> HarnessConfig`; `ModelConfig.agentic(HarnessConfig) -> Harness`; `await Harness.run(task) -> HarnessResult`. **`HarnessConfig` carries NO model info** — model is always supplied separately via `ModelConfig` (key→provider map, `main` required, non-`main` keys auto-get sub-harnesses).
- `HarnessBuilder` is an immutable fluent factory: `add(proc)`, `slot(**kw)`, `add_tool`, `plugin(source)`, `__or__`/`merge` (collects conflicts → `HarnessConflictError`), `build()` (topo-sorts processors per hook by `_order` then Kahn within a bucket by `_after`). Declarative alt: `build_from_config(dict)` from a flat `processors:` list of `_target_` dicts.
- The run loop (`run_loop`) is a fixed async agent loop: `task_start` → `while True:` (step: `step_start` → `before_model` → call provider → `after_model` → per-tool `before_tool`/exec/`after_tool` → `step_end`) → `task_end`. `get_procs(key)` runs `"*"` (MultiHook) processors on every hook, then hook-specific lists.
- State is **two message tracks** (`raw_messages` append-only, `messages` effective context) + typed `slots: dict[str, StateSlot]` (the nearest analogue to OUS's flat `state` dict, but typed and `_target_`-serializable). `StatefulTrajectory` is a first-class product (`s_t = (z_t, Δz_t, a_t, o_t, e_t, r_t)`), built inline during `run_loop`, with `backfill_rewards` and `to_training_records`/`to_rl_records`.
- Hook-name alignment vs OUS: 5/8 match exactly (`task_start`, `before_model`, `after_model`, `before_tool`, `after_tool`, `task_end`); the gap is `stage_start`/`stage_end` ↔ `step_start`/`step_end`. Semantically an OUS "stage" is a coarse pipeline phase wrapping a callback (`run_stage(name, callback)`), whereas a HarnessX "step" is one model-call iteration — not the same concept.
- **Implications**: A whole-pipeline swap is high risk. The cheapest correct adoption confines HarnessX's async loop to the one OUS stage that *is* an agent loop (the hunter pool) and keeps the sync driver for deterministic stages. This directly motivates Decision D1 (phased seam, not big-bang).
### The two incompatible processor contracts
- **Context**: OUS's defining feature is a declared `reads`/`writes` per-key state contract. We must know if HarnessX preserves it.
- **Sources Consulted**: `/tmp/HarnessX/harnessx/core/{processor,contract_check}.py`; `/home/mc/Source/OpenUltraSAST/src/openultrasast/harness.py:49-118`.
- **Findings**:
- HarnessX `Processor` is `async def process(event) -> AsyncIterator[Event]` — an async generator that yields transformed events. The recommended base is `MultiHookProcessor` with `on_*` hook methods, an `@on(EventClass)` decorator, and class-attr ordering hints (`_order`, `_after`, `_singleton_group`, `_hook`). There is **no `spec`, no `handles`, no `reads`/`writes`.**
- HarnessX's contract governs **message-window mutation**, not state keys: enforced in `ProcessorChain.process` via `_validate_messages_contract` (system stays at index 0 on `step_start`; `before_model` net length ∈ {0,+1}; appended message must be role `user`; no out-of-window edits) and per-chain `check_post_hook_invariants`. Mode is the env var `HARNESSX_CONTRACT_MODE ∈ {warn(default), strict}` — not a constructor arg.
- OUS `Processor` is **synchronous**, `def handle(event, state: dict) -> dict | None`, with `spec: ProcessorSpec(name, version, handles, reads, writes)`; `_run_processor` enforces *state-key* discipline (reads must pre-exist; writes must be declared; return must be a dict). `ContractMode` is a `"strict"|"warn"` constructor arg.
- **Implications**: The two contracts do not map onto each other. OUS's per-key allow-list (its provenance/audit discipline) has **no home in HarnessX**. To keep that discipline we must reimplement it as a `SlotContractMixin` on our HarnessX processors (`reads_slots`/`writes_slots` class attrs validated in a base `on_*` wrapper), gated by our own `OUS_SLOT_CONTRACT` env. This is Decision D7.
### HarnessX built-in processors — reuse map
- **Context**: Which HarnessX processors can OUS register directly, which are reusable as patterns/helpers, and which are irrelevant?
- **Sources Consulted**: `/tmp/HarnessX/harnessx/processors/` (control, tools, multi_model, observability, evaluation, memory, context); `/home/mc/Source/OpenUltraSAST/src/openultrasast/cli.py` (`_run_scan`).
- **Findings**:
- **Register-direct only inside the hunter loop** (the one real agent loop): `LoopDetectionProcessor`, `CostGuardProcessor`/`TokenBudgetProcessor`, `ToolFailureGuard`, `SelfVerifyProcessor`, `ToolFilterProcessor`/`ToolWhitelistProcessor`, `ModelRouterProcessor`, `EpisodeMetricsProcessor`/`OTelProcessor`/`CheckpointProcessor`.
- **Reuse as helper functions** for `verify`: the `llm_judge` code (`build_judge_prompt`, `parse_judge_response` with the verdict whitelist, the two-attempt repair loop `_call_judge`) plus the strict-JSON verdict schema (`verdict/confidence/cause/missing_capability`/`lesson`/`missing`). The judge is explicitly told "ground truth is intentionally withheld" — matching OUS's no-oracle scanning.
- **Reuse as pattern** for `calibrate`/`record_calibration`: `MemoryRetrievalProcessor` + `MemoryPolicy` (retrieve/add, should_retrieve/should_store) is the clean interface to refactor the FP ledger toward; `MemoryExtractionProcessor` mirrors distilling verifier outcomes into durable learnings. Storage stays a custom JSON ledger.
- **Keep custom** for every SAST-domain stage: `static_mapping`, `preprocess`, `entry_point_mapping`, `quick_findings`, `report`, `sarif`, `manifest`, and the benchmark recall/FP scoring (HarnessX `EvalResult.score` is a single float — too coarse for the recall/FP gate).
- **Implications**: Adoption is bimodal — register HarnessX classes directly only in the hunter sub-harness; everywhere else port logic as helpers or patterns. This is the backbone of the §3 flow-migration table in `design.md`.
### HarnessX self-improvement (meta_harness) and the rule-adaptation seam
- **Context**: Can an external signal (a verifier rejection or a benchmark miss) drive a *detection-rule* change through HarnessX's self-improvement loop, and how does it compare to OUS's current calibration?
- **Sources Consulted**: `/tmp/HarnessX/harnessx/meta_harness/{agent,journal,replay,validate_workflow}.py`, `processors/contract_autocheck.py`, `workers/trajectory_digester.py`, `rl/{builder,config,task}.py`; `/home/mc/Source/OpenUltraSAST/src/openultrasast/{calibration,benchmark,findings,rank,cli}.py`.
- **Findings**:
- HarnessX self-improves a *harness* round by round. The unit of change is a `HarnessConfig` (the meta-agent literally rewrites the inner agent's config/processors/templates as files); the unit of memory is a markdown **journal** with per-`## Round N` YAML frontmatter; the unit of safety is a multi-phase **validation gate**.
- The journal classifies every change by a **lever** from `_VALID_LEVERS = {configuration, control, action, instruction}` and tracks `predicted_affected` task_ids plus a `gating_attribution` (`flipped/still_F/regressed/still_T/absent`). `build_context` renders a per-lever **time-decayed Beta posterior** scoreboard (`_DECAY=0.9`) and an explicit "Reverted hypotheses — do not re-propose" list.
- `EvolveValidator.run` enforces validity (canonicalize → contract → diff → replay smoke gate), policy (novelty + evidence, only when the diff is non-empty), advisory. `check_novelty` hard-blocks re-proposing a reverted hypothesis or reverted `(levers, predicted_affected)` signature unless a `retry_rationale` is present. `_evidence` requires a `candidates.md` `## Candidate C-N` cross-referenced by the journal. `run_synthetic_task_smoke_gate` proves the evolved config boots end-to-end. `ContractAutoCheckProcessor` gives in-session `[AUTO-CHECK]` feedback on every authored `processors/*.py`/`tools/*.py` edit.
- **The concrete seam to drive a rule change**: add a lever value to `journal._VALID_LEVERS`; carry the external signal in via a `rule_signals.json` read-context artifact + `_render_task_brief` deliverable; add a validity-phase check (`EvolveValidator._rules`) that raises `StrictValidationError(kind="rule_change")`; add changeset buckets (`compute_changeset` + `journal._CHANGESET_KEYS_ORDER`); extend `contract_autocheck` to classify rule files. The novelty/evidence/replay/attribution machinery then applies for free.
- **OUS by comparison**: detection rules in `findings.py` are static and never modified. Calibration (`calibration.py`) only does `calibrated = max(0.1, calibrated - 0.5)` on a *file's* ranking priority (keyed on `tags[0]`, not `rule_id`) plus a prompt-constraint string — it never disables/tightens/adds a rule, has no novelty gate, no attribution. Benchmark misses produce a `BenchmarkCalibrationRecord` whose `next_improvement_candidate` is hard-coded to `"rules_or_language_hunter"` and written to an **inert** `calibration_records.json` that nothing consumes. The recall side is a fully open loop.
- **Implications**: OUS has exactly the *signals* HarnessX needs (verifier rejections = precision side; benchmark misses = recall side) but no loop closing back onto the rules. The meta_harness is the engine that closes it. This motivates Decision D3 (which levers, and their hard bounds) and the §5/§6 design.
### The verycode policy & scoring model
- **Context**: The design needs a central CWE→severity authority and a project score. What does `verycode` actually provide?
- **Sources Consulted**: `/tmp/verycode-policies/{README.md,CWE_Score.tsv}`; `gh repo view norandom/verycode-score` (incl. `action.yaml`, `main.go`); `gh repo view norandom/verycode-lib`; `/home/mc/Source/OpenUltraSAST/src/openultrasast/findings.py`.
- **Findings**:
- `CWE_Score.tsv` is the real artifact: 167 data rows, one per CWE, columns `Flaw Category | CWE ID | CWE Name | Flaw Severity (0-5) | Static (X/empty) | Dynamic (X/empty)`. Severity distribution: 12×5, 15×4, 97×3, 31×2, 0×1, 12×0. Scope: 154 Static, 45 Dynamic, 13 Dynamic-only, none neither. (Header column 4 is literally `"Flaw Severity "` with a trailing space.)
- **The scoring algorithm does not exist.** `norandom/verycode-score` is the unmodified `the-gophers/go-action` "Tweeter" template with `// scorer code here` and a `--policy policies/standard.json` flag; `norandom/verycode-lib` is a one-line stub README. The data shape + the "GitHub Advanced Security findings, weighted by CWE severity" descriptions imply a Veracode-style 0-100 pipeline; the design reconstructs the formula and labels it as such.
- **Real coverage gap**: CWE-120 (Classic Buffer Overflow) is **absent from the TSV**, but 3 high-value C/C++ rules use it (`c-unsafe-copy`, `c-unsafe-memory-copy`, `c-unsafe-scanf`). Unmapped → those rules would contribute 0 to the score. CWE-121 (Stack Buffer Overflow) is present at severity 5 and is the precise classic-overflow CWE.
- **Severity mismatch**: OUS rule strings disagree with policy (rules label CWE-78/95/134 "high"; policy rates them 5/critical; rules label CWE-94/502 "high"; policy rates them 3/medium). The policy is the authoritative source.
- The OUS reachability vocabulary is exactly `reachable | inferred-file-surface | unknown` (verified in `findings.py`), which maps cleanly onto a `REACH_MULT = {reachable:1.0, inferred-file-surface:0.6, unknown:0.4}` exploitability multiplier — and that multiplier is the natural FP-calibration knob (lower effective reachability instead of deleting a rule).
- **Implications**: Severity must flow from PolicyStore (`CWE_Score.tsv`), not from the rule. Day-one fixes: remap the 3 CWE-120 rules to CWE-121 (Decision D5); discard rule string-severities (Decision D4). The score is a stdlib-only reconstruction (Decision D8 ships it advisory-first until `K`/`MIN_SCORE` are calibrated).
### Installability & dependency weight
- **Context**: How does an external project depend on HarnessX, and what does it cost a zero-dependency security tool?
- **Sources Consulted**: `/tmp/HarnessX/pyproject.toml`, `harnessx/__init__.py`, `core/builder.py`, `plugins/{base,loader,discovery,convert}.py`, `examples/{coding,research,assistant}/harness.py`, `extensions/plugins/workflow/plugin.py`, `.gitmodules`; `/home/mc/Source/OpenUltraSAST/pyproject.toml`.
- **Findings**:
- HarnessX is **library-first** and **effectively not on PyPI** (`name="harnessx"`, `version="0.1.0"`, Beta; consumed from Git). `requires-python = ">=3.11"` — exact match with OUS, no floor conflict. Depend via a SHA-pinned Git URL (the version string won't move).
- Custom stages map cleanly onto HarnessX's plugin model: each SAST stage is a `MultiHookProcessor`; group stages + tools + skills into a `HarnessPlugin` mounted via `builder.plugin(...)`; load in-process (instance/class) rather than via directory/`plugin.json`.
- **Dependency-weight is the headline risk.** HarnessX's `dependencies` (always-installed, not extras) is ~30 heavyweight packages: `anthropic`/`openai`/`litellm`/`tiktoken`, `fastapi`/`uvicorn`/`websockets`, `playwright` (ships browser binaries), `docker`/`e2b`, `hydra-core`/`omegaconf`/`structlog`/`loguru`, `opentelemetry-*`, `mcp`, `python-pptx`/`python-docx`/`openpyxl`/`reportlab`/`pdfplumber`. **There is no lean extra** — `pip install harnessx` pulls everything. OUS currently has `dependencies = []`.
- **Mitigating fact**: HarnessX uses **pervasive lazy imports** (heavy modules imported inside functions). So the heavy deps are *installed* but only *imported on demand* — which makes both an optional-extra opt-in and a vendored subset genuinely viable.
- **Submodule caveat**: `.gitmodules` declares a nested `recipe/verl_harnessX/verl` → a large verl RL fork OUS does not need; never `--recursive` init.
- **Implications**: A hard dependency is off the table (it inverts the zero-dep stance and pins to an unmovable Beta). Options narrow to (B) optional extra and (C) vendored lean subset. The lazy-import architecture is what makes B keep heavy code cold by default and makes C a surgical packaging switch. This is Decision D2.
### Current OUS rule-system problem analysis
- **Context**: Establish, in code terms, why the user's complaint ("rules are too influential and not centrally managed") is true.
- **Sources Consulted**: `/home/mc/Source/OpenUltraSAST/src/openultrasast/{findings,rank,calibration,benchmark,verification,harness,config,cli}.py`.
- **Findings** (six code-level defects):
1. **Pattern matches dominate output.** In quick mode, every non-comment regex match becomes a finding (`quick_scan_findings`); the only gate is language. Ranking only sorts; verification rejects only on `reachability_status == "unknown"`. A noisy regex sets output volume and FP rate directly — the rule *is* the result.
2. **Severity is rule-local.** `PatternRule.severity` is a string literal copied straight onto the finding (`_finding_from_match`). Two rules for the same CWE can disagree; nothing reconciles them.
3. **No central store.** The ruleset is a Python tuple compiled into the package; `config.py` has no rule/policy section. You cannot enable/disable/tune a rule without editing source and cutting a release.
4. **Calibration demotes ranking, not rules.** `calibrate_ranking` only subtracts `learning.demotion` (default 0.5) from a *file's* `priority`, floored at `MIN_CALIBRATED_PRIORITY = 0.1`, keyed on `tags[0]`. A chronically wrong rule fires at full severity forever; learning targets the wrong object.
5. **No CWE-policy linkage.** `cwe` is free-text used only for rationale text and loose benchmark matching. No table maps CWE → governed severity/enablement/evidence floor.
6. **No project-score weighting.** Severity and emission are global constants; the ranking signal (`composite_priority`) only re-sorts. A low-severity crypto finding in a fuzzable network parser is never promoted; a high-severity finding in dead code is never demoted.
- **Implications**: The regex layer is simultaneously **detector, severity authority, and volume knob**, while ranking/verification/calibration/benchmark can only act *around* the rules, never *on* them. The fix is to split those three roles into three owners (rules detect / policy decides severity & scope / score is the optimization target) with a one-directional authority model (governance drives execution; execution emits signals; the loop edits governance under a gate). This is the organizing principle of `design.md`.
## Architecture Pattern Evaluation
These are the three "architect angles" that were considered for the overall shape. Each is internally coherent; the synthesis in `design.md` borrows from all three and resolves their three disagreements (D1/D2/D3).
| Option | Description | Strengths | Risks / Limitations | Notes |
|--------|-------------|-----------|---------------------|-------|
| **Incremental seam** | Keep the OUS sync driver; run HarnessX only inside the `hunter_pool` callback via `asyncio.run`. Rules→data + policy + score added around the existing pipeline. Dependency = optional extra (B) with vendored (C) as escape hatch. Loop edits a `"rule"` lever (status/threshold). | Smallest blast radius; confines the async surface to one stage; keeps the recall/FP gate green per-merge; preserves zero-dep default. | Two harness contracts coexist for a while; `HarnessRuntime` lingers; deterministic stages don't gain HarnessX trajectories yet. | Chosen as the base trajectory (Phases 0-4). |
| **Native rewrite** | Delete `HarnessRuntime` entirely; the *whole* scan becomes a single-step HarnessX harness with `skip_model=True` + slot dataflow; vendored lean subset (C) as the committed default. Loop edits a `"rule"` lever; patterns stay human-PR'd. | Cleanest end-state; one runtime, one contract; full trajectory/journal coverage of every stage; smallest auditable dep graph. | Touches every stage at once — too much blast radius to keep the gate green per-merge; vendoring is permanent fork-maintenance burden. | Demoted to optional Phase 5+; its end-state (single-step harness, retire `HarnessRuntime`) is the long-term target. |
| **Policy-first** | The whole scan runs as processors on a fixed loop; central CWE policy is the centerpiece; loop edits `"rule"` + `"policy"` levers (policy = evidence floors/scope, *not* the 0-5 CWE severity). Dependency = optional extra (B) default, vendored (C) fallback. | Strongest governance story; two levers give the loop the right surface to "stop rules being too influential" without inventing detectors or rewriting severity. | Same whole-pipeline-at-once risk as native if taken literally; needs careful bounds so the loop can't corrupt the ruleset. | Contributed the two-lever model (D3) and the bounded-authority safety rules. |
## Design Decisions
The three architect angles agreed on the destination (rules→governed data; severity from a central CWE policy; a gated project score; HarnessX hosting the hunter loop and a rule-adapting self-improvement loop). They disagreed on three load-bearing choices. Each is resolved below with the recall/FP gate as the deciding tie-breaker. (D4-D9 are smaller decisions that follow.)
### Decision D1: Scope of the HarnessX migration
- **Context**: How much of the scan moves onto HarnessX, and how fast?
- **Alternatives Considered**:
1. Incremental seam — sync driver stays; HarnessX only in the hunter callback.
2. Native rewrite — delete `HarnessRuntime`; whole scan becomes a single-step harness immediately.
3. Policy-first whole-loop — whole scan as processors on a fixed loop.
- **Selected Approach**: **Phased.** Adopt HarnessX directly *at the hunter loop* first (Phase 3); migrate deterministic stages onto a single-step harness only later and optionally (Phase 5+), retiring `HarnessRuntime` only after the seam is proven and the gate has held.
- **Rationale**: The sync→async + contract mismatch is the single highest-risk item in the whole effort. The native rewrite touches every stage at once — too much blast radius to keep ≥90% recall / <10% FP green per-merge. The incremental seam ships the *actual* HarnessX value (agent loop, journal, `StatefulTrajectory`) at exactly the one stage that is already an agent loop.
- **Trade-offs**: Two harness contracts coexist through Phases 3-4 and `HarnessRuntime` lingers; in exchange every phase is independently shippable, test-gated, and reversible.
- **Follow-up**: Golden-output diff gate at each phase; confine `asyncio.run` to the hunter callback; `SlotContractMixin` to preserve key-level discipline.
### Decision D2: Dependency strategy
- **Context**: A zero-dependency security tool considering a ~30-package agent runtime with no lean extra.
- **Alternatives Considered**:
1. (A) Hard dependency in main deps.
2. (B) Optional extra `openultrasast[harnessx]`, SHA-pinned, lazy-imported.
3. (C) Vendored lean subset under `src/openultrasast/_vendor/harnessx/`.
- **Selected Approach**: **(B) is the shipping default; (C) is a pre-committed, triggered fallback** gated by an explicit `pip install --dry-run` audit before Phase 3. Core `dependencies = []` stays; all HarnessX imports are lazy behind a `_has_harnessx()` check.
- **Rationale**: HarnessX's pervasive lazy imports mean the heavy deps are *installed* but only *imported on demand*, so a clean opt-in extra already keeps heavy code paths cold in the default install — with the least maintenance burden, while tracking upstream. (A) permanently inverts the project's stance and pins to an unmovable Beta `0.1.0`. (C) adds permanent fork-maintenance and should only be paid for when the *installed* graph (not the *imported* graph) is itself unacceptable.
- **Trade-offs**: With B, enabling the extra still inherits the full HarnessX dep set (no lean upstream extra exists); every HarnessX import must be guarded.
- **Follow-up**: Run the dry-run audit and record the realized transitive graph. **Decision rule**: if it pulls Playwright browser binaries / Docker SDK / a FastAPI web stack as *mandatory* transitive deps and maintainers judge that unacceptable, switch to (C) — vendor `harnessx/{core,plugins,processors,bundles,meta_harness,workspace,tools/base}` + the `llm_judge`/`journal`/`validate_workflow`/`agent`/`contract_autocheck` modules, delete `lab/`/browser/sandbox/office subtrees, re-pin a minimal set. The lazy-import design makes the deletion surgical. Never use the recursive submodule path (`recipe/verl_harnessX/verl`).
### Decision D3: What the self-improvement loop is allowed to edit
- **Context**: The user's core complaint is that rules are "too influential." The loop must be able to *govern* rules without becoming a new way for them to run wild.
- **Alternatives Considered**:
1. Single `"rule"` lever (status/threshold).
2. `"rule"` lever; patterns stay human-PR'd (native angle).
3. `"rule"` + `"policy"` levers, where policy = evidence floors/scope, *not* CWE severity (policy-first angle).
- **Selected Approach**: **Two bounded levers.** `"rule"` = `status` (enabled/shadow/disabled) + evidence-floor + threshold, **never pattern text**. `"policy"` = evidence floor + scope + score constants `K`/`MIN_SCORE`, **never the authoritative 0-5 CWE severity**. Patterns and CWE severities are human-PR-only.
- **Rationale**: This is the precise answer to "stop rules being too influential" — the loop may *suppress / stage / raise-the-bar*, never *invent detectors* or *rewrite the severity authority*. Bounds + mandatory `shadow` staging + auto-revert keep loop authority safe; the meta_harness novelty/evidence/replay/attribution gates apply for free.
- **Trade-offs**: The loop cannot fix a bad pattern itself (it can only `shadow` it and nominate a human PR), and cannot re-weight a CWE — both are deliberate guardrails.
- **Follow-up**: Implement `EvolveValidator._rules`/`._policy` raising `StrictValidationError(kind="rule_change")`; enforce the four "may NOT" bounds (no pattern edits, no 0-5 severity edits, no `enabled→disabled` without `shadow`, no reverted re-proposal without `retry_rationale`).
### Decision D4: Severity authority
- **Context**: Two rules for the same CWE can disagree on severity today.
- **Selected Approach**: PolicyStore (`CWE_Score.tsv`) is authoritative; `resolve_severity(policy, rule.cwe, target, ranking)` replaces the literal `rule.severity` read; `PatternRule.severity` is removed. Policy always wins; rule string-severities are discarded.
- **Rationale**: One CWE = one governed severity, reconciled centrally; ends rule disagreement.
- **Trade-offs**: Loses the per-rule severity hint (acceptable — it was an authoring accident, not a governed decision).
### Decision D5: The CWE-120 coverage gap
- **Context**: 3 C/C++ buffer rules use CWE-120, which is absent from `CWE_Score.tsv`.
- **Selected Approach**: Remap `c-unsafe-copy`/`c-unsafe-memory-copy`/`c-unsafe-scanf` to **CWE-121** (Stack Buffer Overflow, severity 5) in the ruleset TOML.
- **Rationale**: CWE-121 is the precise classic-overflow CWE and is already at severity 5; this avoids forking the upstream policy file. Otherwise those high-value rules contribute 0 to the score.
- **Follow-up**: Add a hard CI invariant — every `enabled` rule's `cwe` must resolve in PolicyStore (fail loud at startup); log `unmapped_cwe` in the score artifact; PR genuine gaps upstream to verycode-policies.
### Decision D6: Response to a confirmed false positive
- **Context**: How does the system react to an FP without hurting recall?
- **Selected Approach**: Lower the finding's effective **reachability multiplier** and/or `shadow` the rule — **never delete it**.
- **Rationale**: The score stops over-penalizing while the rule keeps firing (tracked, just not scored/reported), preserving ≥90% recall and pushing FP <10%. This is exactly the recall/FP balance the project gates on.
### Decision D7: Where OUS's `reads`/`writes` contract lives
- **Context**: HarnessX has no per-key state contract; OUS's defining discipline must survive.
- **Selected Approach**: Reimplement as a `SlotContractMixin` on our `MultiHookProcessor`s (`reads_slots`/`writes_slots` class attrs validated in a base `on_*` wrapper), gated by `OUS_SLOT_CONTRACT=strict|warn`.
- **Rationale**: The two contracts are incompatible; the mixin preserves provenance/audit discipline inside HarnessX's runtime without trying to bend the message-window contract.
### Decision D8: Project-score gate rollout
- **Context**: The verycode score formula is a reconstruction (no upstream implementation).
- **Selected Approach**: Ship the score gate **advisory-first** (computed and reported, not blocking); promote to blocking only after `K`/`MIN_SCORE` are calibrated against the benchmark corpus. Score computation is stdlib-only and always on (zero-dep), so CI gating runs in the default install with no HarnessX present.
- **Rationale**: The formula needs corpus calibration before it can safely block a merge; keeping it zero-dep keeps the CI gate independent of the optional harness.
### Decision D9: RL adoption
- **Context**: HarnessX has an `rl/` subsystem (weight-level improvement) alongside the config-level meta_harness.
- **Selected Approach**: Optional, Phase 5+.
- **Rationale**: Config-level adaptation via the meta_harness is sufficient to close the rule/policy loop; weight-level RL over hunter trajectories is purely additive and not on the critical path.
## Risks & Mitigations
- **Sync↔async + contract incompatibility (High)** — confine the async surface to the hunter stage via `asyncio.run` in its callback (Phase 3); `SlotContractMixin` for key-level discipline; deterministic stages run `skip_model`; golden-output diff gate at each phase.
- **Dependency posture inversion (High)** — governance/scoring planes stay zero-dep; HarnessX strictly behind the extra + lazy imports + pinned SHA; CI zero-dep guard test; `pip --dry-run` audit before Phase 3; vendored-subset fallback pre-specified.
- **Benchmark-gate regression from rule auto-tuning (High)** — the recall/FP gate is the per-round acceptance test (aggregate + per-rule, hard constraint); `shadow` before `disabled`; novelty + attribution block thrash and reverted re-proposals; auto-revert byte-for-byte.
- **Score gaming the gate (High)** — keep the gate as a *hard constraint*, never part of the reward; track unpredicted regressions; add a score-regression check to acceptance.
- **Meta-agent corrupts the ruleset, re-introducing "too influential" (Medium)** — bounded levers (D3): no pattern rewrites, no severity edits, mandatory `shadow` staging, no `enabled→disabled` jump; recall gate auto-reverts; every change journaled and novelty-checked.
- **Policy completeness / CWE drift (Medium)** — day-one CWE-120→CWE-121 remap; hard CI invariant that every enabled rule's CWE resolves; `unmapped_cwe` logged; treat verycode-policies as authoritative-upstream and PR gaps there.
- **Frozen Beta upstream (Medium)** — pin SHA; document in `VENDOR.md`; keep the meta_harness patch minimal/isolated; rule lever degrades gracefully (absent patch = no rule lever, loop still does ranking demotion); vendored subset removes upstream-instability exposure for depended modules.
- **Slot serialization loss on resume (Medium)** — all scan slots use plain dicts/dataclasses with `_target_` registration; per-slot-type round-trip serialization test (non-serializable content restores as `None` on `wake()`).
- **Two-store coordination drift (Low-Med)** — a single `resolve_severity` owns the rule-status + policy-gate combination; the changeset records both lever edits per round; the benchmark regression suite catches interaction regressions.
## References
- HarnessX core — `/tmp/HarnessX/harnessx/core/{harness,builder,processor,events,runloop,state,contract_check,model_config,trajectory}.py` — composition/run-loop/contract/state model.
- HarnessX processors — `/tmp/HarnessX/harnessx/processors/{evaluation/llm_judge.py, evaluation/strategies/evaluators/prm.py, observability/{otel_proc,episode_metrics,checkpoint}.py, memory/memory_retrieval.py, control/loop_detection.py}` — reuse map (verify/calibrate/observability).
- HarnessX meta_harness — `/tmp/HarnessX/harnessx/meta_harness/{agent,journal,replay,validate_workflow}.py`, `processors/contract_autocheck.py`, `workers/trajectory_digester.py`; `rl/{builder,config,task}.py` — self-improvement engine + rule-adaptation seam.
- HarnessX packaging — `/tmp/HarnessX/pyproject.toml`, `harnessx/__init__.py`, `plugins/{base,loader,discovery,convert}.py`, `examples/{coding,research,assistant}/harness.py`, `extensions/plugins/workflow/plugin.py`, `.gitmodules` — installability + dependency weight.
- verycode policy — `/tmp/verycode-policies/{CWE_Score.tsv,README.md}`; `gh repo view norandom/verycode-score` (`action.yaml`, `main.go` placeholder); `gh repo view norandom/verycode-lib` (stub) — policy data (complete) vs scoring algorithm (never implemented).
- OUS current system — `/home/mc/Source/OpenUltraSAST/src/openultrasast/{findings,rank,calibration,benchmark,verification,harness,config,cli}.py` — `PATTERN_RULES`, ranking-only calibration (`MIN_CALIBRATED_PRIORITY=0.1`), inert `next_improvement_candidate`, sync `HarnessRuntime`, no rule/policy config section.
- Companion design — `.kiro/specs/harnessx-self-improving-rulesets/design.md` (committed architecture); detection gate per `MEMORY.md`: ≥90% recall / <10% FP on the benchmark corpus.
@@ -1,22 +0,0 @@
{
"feature_name": "harnessx-self-improving-rulesets",
"created_at": "2026-06-22T10:07:18Z",
"updated_at": "2026-06-22T16:45:00Z",
"language": "en",
"phase": "implementation",
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": true
}
},
"ready_for_implementation": true
}
@@ -1,277 +0,0 @@
# Implementation Plan
This plan splits the fused regex layer into three governed owners (rules detect, CWE policy decides severity/scope, project score is the optimization target) and adopts HarnessX behind an optional extra. Tasks are grouped into four phases. Each phase is independently shippable and keeps the ≥90% recall / <10% FP detection gate green. Phases 1-2 (Foundation) and the early Core work stay zero-dependency and byte-identical by default; the HarnessX execution and self-improvement work (Integration) lives behind a lazy, SHA-pinned extra; Validation locks the cross-cutting gates and the dependency-weight decision.
## Phase 1 — Foundation: Governance & Scoring Planes (zero-dep)
- [x] 1. Stand up the central CWE policy authority
- [x] 1.1 Vendor the verycode CWE severity data and build the Policy Store loader
- Vendor the verycode CWE-to-severity dataset into the governance plane and parse it with the standard library only, tolerating the trailing-space severity column header.
- Expose, per CWE, an authoritative 0-5 severity, a static scope flag, and a dynamic scope flag; mark dynamic-only CWEs as report-only.
- Observable completion: loading the policy yields the full set of CWE policy entries with correct static/dynamic flags and zero third-party imports, verified by a unit test.
- _Requirements: 2.6, 2.9_
- _Boundary: PolicyStore_
- [x] 1.2 Expose policy-governed severity resolution
- Resolve a finding's severity exclusively from the Policy Store keyed on the rule's CWE, discarding any severity string carried in legacy rule data.
- Guarantee that two findings whose rules map to the same CWE receive identical severity.
- Observable completion: a resolution call returns the policy severity (never a legacy string) and is identical for two rules sharing a CWE, proven by a unit test.
- _Requirements: 2.2, 2.3_
- _Boundary: PolicyStore_
- [x] 1.3 Enforce the fail-loud startup CWE-resolution invariant
- Assert at startup that every enabled rule's CWE resolves in the Policy Store; abort the scan loudly before any work when an enabled rule's CWE is unmapped.
- Observable completion: an enabled rule with an unmapped CWE aborts the scan with a loud error and no findings are produced, proven by an integration test.
- _Requirements: 2.7_
- _Boundary: PolicyStore_
- _Depends: 1.1_
- [x] 2. Build the project Score Model and CI gates (zero-dep)
- [x] 2.1 Compute the 0-100 policy-weighted, reachability-adjusted project score
- Compute a single 0-100 project score from policy-governed severity weights multiplied by a reachability multiplier over reported findings, using the standard library only.
- Key the reachability multipliers exactly on the three emitted reachability values (`reachable`, `inferred-file-surface`, `unknown`) without redefining the vocabulary.
- Assign a zero score penalty to findings whose CWE is dynamic-only or unmapped, reporting them as out-of-scope or unmapped instead of scoring them.
- Observable completion: a no-penalty corpus scores 100, a single severity-5 reachable finding scores roughly 43, and dynamic-only/unmapped findings contribute zero penalty, proven by boundary unit tests.
- _Requirements: 5.1, 5.2, 5.3, 5.9_
- _Boundary: ScoreModel_
- _Depends: 1.1_
- [x] 2.2 Emit the scan-level score artifact and manifest score block
- Emit a score artifact containing the project score, the maximum severity, the total penalty, a by-category breakdown, the out-of-scope dynamic-only count, the unmapped-CWE list, and the gate verdict; include the score block in the scan manifest.
- Observable completion: a scan writes a score artifact with every required field and merges the score block into the manifest, verified against the documented artifact schema.
- _Requirements: 5.5_
- _Boundary: ScoreModel_
- _Depends: 2.1_
- [x] 2.3 Implement the two-condition CI score gate (advisory-first)
- Fail the build when any reported finding maps to policy severity 5 and has reachability `reachable`.
- When configured as blocking, fail the build when the project score is below the minimum; until the score constants are calibrated against the corpus, compute and report the score advisory-first without blocking.
- Observable completion: the hard gate fails on a severity-5 reachable finding while the score gate reports advisory-only by default, proven by gate tests.
- _Requirements: 5.7, 5.8, 5.9_
- _Boundary: CI Score Gate_
- _Depends: 2.1_
- [x] 2.4 Centralize ruleset/policy/score configuration
- Add configuration entries for the ruleset, policy, and score data locations plus the gate thresholds and emission floors used by later phases.
- Observable completion: the scan reads the score constants and emission thresholds from configuration rather than hardcoded literals, confirmed by a config-driven test.
- _Requirements: 5.8_
- _Boundary: Packaging Configuration, ScoreModel_
## Phase 2 — Core: Rules-as-Data and Benchmark Signals (zero-dep, byte-identical)
- [x] 3. Convert detection rules into versioned, governed data
- [x] 3.1 Externalize detection patterns into a versioned Ruleset Store
- Load detection rules from versioned data files in which each rule declares a pattern, a CWE, tags, and a loop-controlled status, carrying no rule-local severity field.
- Remove the rule-local severity field from the in-memory rule model so severity can only come from policy.
- Observable completion: the existing pattern rules load from versioned data files with no severity attribute present, proven by a unit test asserting the absence of the field.
- _Requirements: 2.1, 2.9_
- _Boundary: RulesetStore_
- [x] 3.2 Implement the three-status volume control
- Support exactly three rule statuses — `enabled`, `shadow`, `disabled` — where enabled fires/scores/reports, shadow fires and records outcomes but is excluded from report and score, and disabled does not fire.
- Exclude shadow-status findings from the reported set and from the score input while still recording their outcome for precision tracking.
- Observable completion: an enabled rule appears in report and score, a shadow rule is excluded from both yet records an outcome, and a disabled rule never fires, proven by a unit test across all three statuses.
- _Requirements: 2.4, 2.5, 5.4_
- _Boundary: RulesetStore, ScoreModel_
- _Depends: 3.1, 2.1_
- [x] 3.3 Remap the unmapped C/C++ buffer rules to a scored CWE
- Remap the C/C++ buffer-handling rules that reference the unmapped CWE-120 to CWE-121 so they resolve in policy and contribute a nonzero penalty.
- Observable completion: the remapped buffer rules resolve to a severity-5 policy entry and produce a nonzero score penalty, verified by a unit test.
- _Requirements: 2.8_
- _Boundary: RulesetStore_
- _Depends: 3.1, 1.1_
- [x] 3.4 Wire findings and severity resolution through the governance plane
- Pass the loaded ruleset into the finding-detection stage instead of reading an implicit module-level rule list, and resolve every finding's severity through the Policy Store.
- Apply emission thresholds so not every regex match becomes a reported finding, replacing the old "rule is its own volume control" behavior.
- Observable completion: a scan produces findings with policy-resolved severity and shadow findings filtered out, while the default ruleset keeps benchmark output byte-identical to the pre-feature baseline.
- _Requirements: 2.2, 2.3, 2.5, 6.2_
- _Boundary: PolicyScoringProcessor, RulesetStore, PolicyStore_
- _Depends: 3.1, 3.2, 1.2_
- [x] 4. Make benchmark output an actionable per-rule signal
- [x] 4.1 Attribute misses and false positives to rules
- Attribute every recorded miss and every recorded false positive to a rule identifier and aggregate per-rule precision and recall in the benchmark metrics.
- Observable completion: a benchmark run reports per-rule precision/recall and every miss and false positive carries a rule identifier, verified by a metrics test.
- _Requirements: 4.1_
- _Boundary: Benchmark Harness_
- _Depends: 3.1_
- [x] 4.2 Emit a per-rule recommendation delta artifact
- Replace the inert improvement-candidate stub with a per-rule recommendation artifact recommending enable, disable, shadow, threshold, or evidence-floor changes per rule.
- Observable completion: a benchmark run writes a recommendation artifact with per-rule suggested actions alongside its records, verified against the documented delta schema.
- _Requirements: 4.2_
- _Boundary: Benchmark Harness_
- _Depends: 4.1_
- [x] 4.3 Introduce the loop-owned ledger and re-key calibration to rules
- Add the loop-owned overlay ledger that records per-rule status, evidence floor, threshold, and precision, applied over the ruleset at load time.
- Re-key calibration from the first-tag key to the rule identifier and write status flips into the ledger rather than only demoting ranking.
- Observable completion: an empty ledger leaves behavior byte-identical to the prior phase while a ledger entry overlays a rule's status at load, proven by an integration test.
- _Requirements: 2.4, 5.6, 6.2_
- _Boundary: RulesetStore, Calibration subsystem_
- _Depends: 3.1, 3.2_
- [x] 4.4 Convert false-positive confirmation into a reachability knob
- When confirming a false positive, lower the finding's effective reachability multiplier rather than delete the rule, so recall stays high while the score stops over-penalizing.
- Observable completion: confirming a false positive lowers the finding's effective reachability multiplier and the rule still fires, proven by a calibration test.
- _Requirements: 5.6_
- _Boundary: Calibration subsystem, ScoreModel_
- _Depends: 4.3, 2.1_
## Phase 3 — Integration: HarnessX Execution & Self-Improvement (optional extra)
- [x] 5. Gate and package the HarnessX dependency-weight decision
- [x] 5.1 Run the dependency dry-run audit and record the transitive graph
- Before adopting the HarnessX execution stages, run a dependency dry-run audit and record the realized transitive dependency graph.
- If the audited graph pulls a browser-binary, container-SDK, or web-server stack as a mandatory dependency and it is judged unacceptable, select the pre-specified vendored lean subset under the same extra name with the heavy subtrees removed.
- Observable completion: the audit produces a recorded transitive-dependency graph and a documented accept-extra-or-vendor-subset decision before any HarnessX execution code is wired in.
- _Requirements: 7.3, 7.4, 7.7_
- _Boundary: Packaging Configuration_
- [x] 5.2 Configure HarnessX as a SHA-pinned optional extra with a capability guard
- Keep the core dependency list empty and expose HarnessX only as an optional extra pinned to a specific commit revision; initialize any submodule non-recursively so the large RL fork subtree is not pulled.
- Guard every HarnessX import behind a capability check and import it lazily so heavy code paths stay cold in the default install.
- Observable completion: the default install resolves with an empty core dependency list and HarnessX symbols are only imported after the capability check passes, verified by an import-cold test.
- _Requirements: 7.1, 7.2, 7.6_
- _Boundary: Packaging Configuration_
- _Depends: 5.1_
- [x] 6. Adopt HarnessX at the hunter loop and verifier
- [x] 6.1 Run the hunter pool stage on a real HarnessX agent loop
- Run the hunter pool stage on a real HarnessX run loop with the control, tool, and observability processors registered in the hunter sub-harness, invoked from the existing hunter-pool stage callback.
- Confine all asynchronous execution to this single stage so the deterministic stages remain synchronous.
- Observable completion: with the extra installed, the hunter pool produces per-target trajectories from a real agent loop while all other stages stay synchronous, verified by an integration test.
- _Requirements: 1.1, 1.4_
- _Boundary: HxScanOrchestrator_
- _Depends: 5.2_
- [x] 6.2 Map stage events and identifiers onto HarnessX hooks
- Map the existing stage-start/stage-end events to the HarnessX step-level hooks and the scan identifier to the HarnessX run identifier without losing trace continuity.
- Observable completion: a HarnessX-backed run preserves trace continuity with stage events mapped to step hooks and the scan id mapped to the run id, verified by inspecting the emitted trace.
- _Requirements: 1.7_
- _Boundary: HxScanOrchestrator_
- _Depends: 6.1_
- [x] 6.3 (P) Run deterministic stages as model-disabled HarnessX processors with slot contracts
- Run each deterministic stage under HarnessX with model invocation disabled so no deterministic stage incurs a model call.
- Preserve each stage's declared read/write slot allow-list and validate slot access on every lifecycle hook.
- Observable completion: deterministic stages execute under HarnessX with no model calls and a slot-contract violation is raised on undeclared slot access, proven by a slot-contract test.
- Implemented zero-dep in `slot_contract.py` (`SlotContractMixin`/`SlotPipeline`, validates reads/writes per lifecycle hook, enforces `skip_model`) + `stage_processors.py` (the deterministic stages as `skip_model` slot processors; `run_quick_pipeline` is byte-identical to the quick path). Per "bridge, do not map", the sync `HarnessRuntime` stays the top-level driver and these slot processors are the model-disabled representation; `host_under_harnessx` is the lazy, capability-gated bridge that disables the model on HarnessX's `BeforeModelEvent.skip_model`. Module names differ from the design's `harness/{slot_contract,processors}.py` because `harness` is a module, not a package. Tests: `tests/test_slot_contract.py`.
- _Requirements: 1.5, 1.6_
- _Boundary: HxScanOrchestrator_
- _Depends: 6.1_
- [x] 6.4 (P) Add the LLM-judge verifier with a structural fallback
- Produce verification verdicts using the HarnessX llm-judge helpers when the extra is present, mapping the strict-JSON verdict to a verification result.
- Retain the existing structural verifier (which rejects on unknown reachability) verbatim as the zero-dependency fallback.
- Observable completion: with the extra present the judge produces verdicts and with it absent the structural verifier produces equivalent verdicts, proven by a paired verifier test.
- _Requirements: 1.2_
- _Boundary: VerifyProcessor_
- _Depends: 5.2_
- [x] 6.5 Fall back gracefully when a HarnessX capability is unavailable
- When a HarnessX-backed stage is enabled but its capability is unavailable at runtime, fall back to the existing equivalent stage and record the degradation in the scan manifest.
- Observable completion: requesting a HarnessX stage whose capability is missing runs the existing equivalent and records a degradation entry in the manifest, proven by a fallback test.
- _Requirements: 1.8_
- _Boundary: HxScanOrchestrator_
- _Depends: 6.1, 6.4_
- [x] 7. Stand up the bounded self-improvement loop
- [x] 7.1 Add the two bounded edit levers
- Expose exactly two new edit levers: a rule lever covering rule status, evidence floor, and threshold, and a policy lever covering evidence floor, scope, and the score constants.
- Observable completion: the loop's valid-lever set includes exactly the rule and policy levers and rejects any edit outside their declared fields, verified by a lever test.
- _Requirements: 3.1_
- _Boundary: EvolveValidator, improve/evolve.py_
- _Depends: 5.2_
- [x] 7.2 Enforce the rule/policy edit safety bounds in the validator
- Reject any loop edit that modifies detection pattern text and any edit that modifies the authoritative 0-5 CWE severity.
- Reject moving a rule directly from enabled to disabled, requiring shadow as a mandatory intermediate staging state.
- Assert that each edit parses, that status is one of the three literals, that threshold/evidence-floor changes stay within configured bounds, that an added rule does not duplicate an existing identifier, and that every referenced CWE resolves in policy; raise a rule-change validation error and reject the whole round on any failure.
- Observable completion: each prohibited edit (pattern change, severity change, enabled-to-disabled jump, duplicate identifier, unmapped CWE, out-of-bounds threshold) is rejected and rejects the round, proven by validator unit tests.
- _Requirements: 3.2, 3.3, 3.4, 3.5, 3.8_
- _Boundary: EvolveValidator_
- _Depends: 7.1, 1.1_
- [x] 7.3 Add the novelty, replay, and journal gates
- Block re-proposing a previously reverted edit that lacks a retry rationale via the novelty gate.
- Run a replay smoke gate before any benchmark re-run proving the edited ruleset and policy still load and boot.
- Record each rule and policy edit, its lever, and its outcome in the journal so every change is attributable and reversible.
- Observable completion: a reverted re-proposal without rationale is blocked, a non-booting edited ruleset is rejected before re-benchmark, and every edit appears in the journal with its lever and outcome, proven by gate tests.
- _Requirements: 3.6, 3.7, 3.9_
- _Boundary: EvolveValidator, improve/evolve.py_
- _Depends: 7.2_
- [x] 7.4 Convert benchmark deltas into loop signals
- Convert the per-rule recommendation delta and false-positive learnings into a loop signal file, writing recall-side items as miss signals and precision-side items as verifier-reject or false-positive signals.
- Observable completion: a benchmark run produces a signal file whose recall-side entries are miss signals and precision-side entries are verifier-reject/false-positive signals, verified against the documented signal schema.
- _Requirements: 4.3_
- _Boundary: improve/evolve.py_
- _Depends: 4.2, 7.1_
- [x] 7.5 Implement the hard acceptance gate with byte-for-byte revert
- After the meta-agent edits rules or policy, re-run the benchmark and accept the round only if aggregate recall is at least 90% AND false-positive rate is under 10% AND the project score did not regress on the held set AND no unpredicted regression occurred.
- On any failed acceptance condition, revert the round and restore the rule and policy configuration byte-for-byte.
- Observable completion: a round that regresses recall is reverted and the configuration is restored byte-for-byte, while a compliant round is accepted and persisted, proven by a self-improvement integration test.
- _Requirements: 4.4, 4.5, 6.3_
- _Boundary: improve/evolve.py_
- _Depends: 7.3, 7.4, 4.1_
- [x] 7.6 Auto-shadow precision-draggers, nominate coverage gaps, and separate the objective
- Auto-shadow a rule that drags aggregate precision below the gate rather than delete it, preserving recall while removing the false-positive cost.
- Nominate a new or loosened rule through the signal file when a coverage gap surfaces as a persistent miss, leaving pattern authoring to a human pull request.
- Treat maximizing the project score as the optimization objective and the recall/FP gate as a separate hard feasibility constraint that is never folded into the score reward.
- Observable completion: a deliberately noisy rule is auto-shadowed (not deleted) when it drags precision, a persistent miss emits a rule nomination, and the loop cannot raise the score by shadowing rules that breach the gate, proven by integration tests.
- _Requirements: 4.6, 4.7, 4.8_
- _Boundary: improve/evolve.py_
- _Depends: 7.5_
## Phase 4 — Validation: Cross-Cutting Gates & Non-Regression
- [x] 8. Lock the detection-gate non-regression checks into CI
- [x] 8.1 Enforce the per-merge recall/FP detection gate
- Evaluate aggregate recall and aggregate false-positive rate on the benchmark corpus on every merge and fail the merge if recall is below 90% or the false-positive rate is at or above 10%.
- Treat the recall/FP gate as a hard constraint that is never folded into the project-score reward, blocking any change that breaches the gate regardless of score improvement.
- Observable completion: a CI run fails the merge when recall drops below 90% or false-positive rate reaches 10%, and a score improvement cannot unblock a gate-breaching change, proven by pipeline tests.
- _Requirements: 6.1, 6.5, 6.6_
- _Boundary: CI Pipeline_
- _Depends: 4.1_
- [x] 8.2 (P) Assert byte-identical benchmark baseline for the zero-dep phases
- With the default ruleset and an empty loop ledger, assert the benchmark output is byte-identical to the pre-feature baseline for the governance and rules-as-data phases.
- Observable completion: a golden-diff test confirms byte-identical benchmark output with the default ruleset and empty ledger.
- _Requirements: 6.2_
- _Boundary: Benchmark Harness, CI Pipeline_
- _Depends: 3.4, 4.3_
- [x] 8.3 (P) Confirm HarnessX-backed stages stay within detection tolerance
- When a HarnessX-backed stage is enabled, confirm recall and false-positive rate stay within the configured tolerance of the equivalent zero-dependency baseline.
- Observable completion: a CI run with a HarnessX-backed stage enabled keeps recall/FP within the configured tolerance of the baseline, proven by a tolerance test.
- _Requirements: 6.4_
- _Boundary: CI Pipeline, HxScanOrchestrator_
- _Depends: 6.1, 8.1_
- [x] 8.4 (P) Add the zero-dependency guard test
- Run a guard test confirming that with HarnessX absent the governance, scoring, and benchmark planes operate and CI gating runs in the default install.
- Observable completion: the guard test passes with HarnessX uninstalled and fails if any governance/scoring/benchmark path imports HarnessX.
- _Requirements: 7.5_
- _Boundary: CI Pipeline_
- _Depends: 8.1_
- [x] 9. Validate scan-pipeline integrity and serialization
- [x] 9.1 (P) Verify capability fallback and unchanged sync-driver behavior
- Confirm that with the extra absent, quick and standard scans execute through the existing sync driver with behavior unchanged from the pre-adoption baseline, and that a requested-but-unavailable HarnessX stage records a degradation.
- Observable completion: an extra-absent scan matches the pre-adoption baseline and a missing-capability request records a manifest degradation, proven by an end-to-end fallback test.
- _Requirements: 1.3, 1.8_
- _Boundary: HxScanOrchestrator, CI Pipeline_
- _Depends: 6.5_
- [x] 9.2 (P) Verify slot serialization round-trips on resume
- Confirm each scan slot type serializes and restores without loss after a resume, guarding against non-serializable slots restoring as empty.
- Observable completion: a per-slot-type round-trip test passes for every scan slot after a resume cycle.
- Done for the current artifact-persistence model (findings/ranking/file-target/verification/score round-trip + the real findings.json reload path in `tests/test_scan_pipeline_integrity.py`). The HarnessX processor slot-contract serialization variant remains deferred with task 6.3.
- _Requirements: 1.5_
- _Boundary: HxScanOrchestrator_
- _Depends: 6.3_
+218
View File
@@ -0,0 +1,218 @@
# Design: hunter-learning-loop
> **Amendment 2026-10-14.** The feature table below is the shape of the first round and is superseded as the
> model's input by the ordered trace of `access-control-prover` (design sections "trace: the ordered
> serialisation" and "cfg and CfgDominance"). The feature table remains the screen's instrument and the
> ablation's bookkeeping; the decision strategy is the one stated in requirements.md, "Decision strategy,
> corrected 2026-10-14": a rule over the trace, a zero-shot judge for the residual question, the pool for
> measurement only.
## Overview
**Purpose**: turn the LLM hunter from a hand-written prompt into a measured search stage whose inputs are an
engineered feature table and whose prompt is optimised by a program runtime, judged on held-out folds.
**Users**: the maintainer runs the loop locally on the research host; the pre-push safety net consumes its output
later (not in this spec).
**Impact**: adds a research package `src/openultrasast/hunt/` and one research dependency (DSPy); reuses the
learn harness for labels, folds, leak audit and paired analysis; reuses `benchmarks/valid/hunter_arm.py` as the
runner and `benchmarks/unseen/score.py` plus `benchmarks/valid/metrics.py` as the only scorekeepers. The hook,
its urllib client and the core install are untouched.
### Goals
- A feature table with named, switchable columns is the contract between producers, ablation and the program (3, 4).
- The hunter is a DSPy program over that table plus the changed hunks; its instruction and demonstrations are
proposed by DSPy optimisers and accepted only by our rule on held-out folds (5, 6).
- Every annotation's worth is a paired, pre-registered ablation with intervals; the importance table is the output
the maintainer reads (4).
- Search recall and suspicions per push improve on the held-out pilot, reported with the pilot's own scorer (7, 8).
### Non-Goals
- Thresholds or operating points (4.7); the hook's LLM tier (pre-push-safety-net); the proof arm (search-with-proof);
retraining the classifier (7.5); replacing the quick-rule engine with Semgrep (separate decision, brief pending);
migrating `learn/labels.py` to pandas (statistics item, separate).
## Boundary Commitments
### This spec owns
- `src/openultrasast/hunt/`: the feature table schema, the producer list, the hunt program, the ablation runner,
the acceptance rule, the run artifact.
- The training-set recipe (which advisories, folds, digests) and its records under `benchmarks/measurements/`.
- The importance table and the before/after report on the pilot.
### Out of boundary
- Label semantics (`benchmarks/valid/catalog.py`, `verify.py`, `learn/labels.py`): consumed as they are.
- The scorer and metrics (`benchmarks/unseen/score.py`, `benchmarks/valid/metrics.py`): never edited to fit.
- The hook, `tool_hunter.py`'s shipped prompt, the classifier program, Semgrep adoption, lizard adoption (each a
producer candidate that enters only through the producer contract after its own evaluation).
### Allowed dependencies
- `openultrasast.source`, `semantic` facts, `mapping` entry points, `learn.folds`, `learn.leaks`,
`learn.experiments` (paired bootstrap, McNemar), `learn.labels` readers, `benchmarks.valid.hunter_arm` (runner),
`benchmarks.unseen.score`; `dspy` (research extra `[hunt]`), `pandas` (table), `scipy`/`statsmodels` once the
statistics item lands (until then `learn.experiments`).
- Dependency direction: data (manifests, labels) → producers → feature table → program → runner → scorer → report.
Nothing to the right imports to the left; producers never import `hunt.program`; nothing in `hunt/` imports
`push/` or `cli.py`.
### Revalidation triggers
- The feature table schema version changes; a producer's output shape changes; the scorer's tolerance changes;
DSPy minor version changes; the pilot manifest digest changes.
## Architecture
### Existing architecture analysis
The hunter (`tool_hunter.run_tool_hunter`) is a tool loop with a four-sentence prompt and no diff
(01-flowcharts/U6-hunter-client.md). The runner `hunter_arm.run` already walks a frozen manifest, checks out
each head, proves bytes read, runs the hunter, classifies each finding with the decision-engine classifier and
writes one record per change; `combine` joins with the replay. The harness has grouped folds, a leak audit and
paired analysis. The missing pieces are the table, the switchable inputs and the optimisable program.
### Pattern and boundary map
```mermaid
flowchart LR
M[(frozen manifests<br/>training set, pilot held out)] --> P
L[(labels<br/>fix sites, ordinary pushes)] --> T
subgraph producers
P1[source.facts_for]
P2[semantic facts<br/>roles, def-use, obligations]
P3[mapping entry points]
P4[diff hunks]
P5[candidates: lizard, Semgrep<br/>after evaluation]
end
M --> P[hunt.producers.compute]
P --> P1 & P2 & P3 & P4 & P5
P1 & P2 & P3 & P4 & P5 --> T[(feature table<br/>hunt.table)]
T --> A[hunt.ablation<br/>column mask per arm]
A --> G[hunt.program<br/>dspy.ReAct + HuntSignature]
G --> R[hunter_arm.run<br/>records per change]
R --> S[unseen.score + valid.metrics]
S --> I[importance table,<br/>acceptance decision, report]
O[dspy optimisers<br/>GEPA, MIPROv2] --> G
I --> O
```
- Selected pattern: a table-centred pipeline; the program is a replaceable consumer of the table.
- Existing patterns preserved: the runner's record shape and resumability; the scorer; folds by repository; the
instrument proof (bytes read, base and head verified).
### Technology stack (changed layers only)
| Layer | Tool | Role |
|---|---|---|
| Program runtime | `dspy==3.4.*` (pulls litellm, openai, gepa, diskcache) as extra `[hunt]` | signature, ReAct tool loop, optimisers, cache, usage |
| Model access | `dspy.LM("openai/deepseek-flash", api_base=DEEPSEEK_BASE_URL, api_key=env)` | one client for the research side; prices registered from `model/endpoint.py:55` |
| Table | `pandas` as extra `[hunt]` | the feature table, masks, joins with labels and scores |
| Statistics | `learn.experiments` now; `scipy`/`statsmodels` after the statistics item | paired bootstrap, McNemar |
## File structure plan
```
src/openultrasast/hunt/
__init__.py
table.py FeatureTable: schema v1, columns, masks, read/write (jsonl + parquet), digest
producers.py PRODUCERS: fixed list of (name, fn, columns, needs); compute(root, change) -> rows
program.py HuntSignature, HuntProgram(dspy.Module: ReAct + tools), render_row, parse_output
metric.py search_metric(example, prediction) -> score, feedback text (GEPA), per-change outcome
ablation.py Manifest (pre-registered), arms as masks, run(arm) -> records, analyse -> importance table
accept.py the acceptance rule (6.1-6.8) over compile folds and held-out folds
artifact.py RunArtifact: program id, schema version, masks, data digests, usage, versions
benchmarks/valid/hunter_arm.py --program <artifact> and --mask <arm> options; everything else unchanged
benchmarks/valid/training_set.py sourcing recipe apart from the pilot (catalog + verify + assemble, pilot excluded)
tests/test_hunt_*.py table, producers (planted-advisory test), program with a scripted LM, metric,
ablation masks, acceptance rule, artifact
```
### Rule: a pattern engine's miss is never evidence
Maintainer, 2026-10-13: "what does not work is that we dismiss things just because semgrep cannot find the bug.
if semgrep was enough we'd use it." A Semgrep (or quick-rule, or engine) match is positive evidence and may enter
the table as a feature ("matched here, rule X"). The absence of a match is not recorded as a feature, is never a
filter, a demotion, a down-weight or a gate on a hunter suspicion, and no acceptance rule may count it. The
Semgrep arm on the pilot is a comparison arm only.
## Components and interfaces
| Component | Intent | Requirements | Depends on | Contract |
|---|---|---|---|---|
| FeatureTable | one row per candidate location with named columns | 3.1, 3.5, 4.2 | pandas | `columns: feature.<producer>.<name>`, `label`, `provenance.*`, `fold`, `schema_version` |
| producers | compute facts from the repository only | 3.2, 3.3, 3.4, 3.7 | source, semantic, mapping | `compute(root, change) -> list[dict]`; absent feature = `None` + `provenance.<name>.reason` |
| HuntProgram | the search | 3.5, 3.6, 5.1 | dspy, producers (rendered row) | input: rendered row + hunks; output: list of `{path, line, family ∈ FAMILY_IDS, confidence, rationale, cited_lines}` |
| search_metric | per-change score and feedback | 4.3, 7.1 | unseen.score tolerance | hit on fix site with right family = 1; ordinary push suspicions counted; feedback text names misses |
| ablation | arms as masks, paired runs | 4.1 to 4.6, 4.8 | table, program, runner, experiments | manifest: table digest, masks, folds, seed, budget, rule |
| accept | train and holdout rule | 6.1 to 6.8 | ablation results | returns accepted|rejected with both fold results and the regression budget check |
| RunArtifact | the record of a run | 5.1, 5.2, 5.5, 9.5 | dspy save | content-addressed JSON + the saved program + the DSPy cache directory copied |
### FeatureTable schema v1 (one row per candidate location)
| Column group | Columns | Producer |
|---|---|---|
| key | `change_id`, `repo`, `head`, `path`, `line`, `kind` (vulnerability or ordinary), `fold` | manifest |
| label | `label` (1 fix site, 0 ordinary, null unknown), `family`, `fix_span` | labels |
| diff | `feature.diff.hunks` (list of (start, end) at head), `feature.diff.on_changed_line`, `feature.diff.distance` | producers.diff |
| source | `feature.source.function`, `.span`, `.callers` (list of (path, line)), `.entry_points`, `.language` | source.facts_for |
| semantic | `feature.semantic.roles` (source/sink/sanitizer per identifier), `.def_use` (binds and calls in the function), `.obligations` | semantic facts |
| taxonomy | `feature.taxonomy.families` (closed list with one-line definitions) | ruleset/families.toml |
| provenance | `provenance.<producer>.{version, ok, reason, bytes_read}` | each producer |
A candidate location is every function the change touched at head (ordinary pushes) or the labelled fix site plus
every other touched function (positives; the non-site rows carry `label` null, 1.3).
### HuntSignature
Inputs (each an `InputField`, present only when its column is unmasked): `change_summary`, `hunks`,
`function_text`, `callers`, `entry_points`, `roles`, `def_use`, `obligations`, `families`. Output: `findings`
(JSON list, the classifier's answer structure). Tools (ReAct, `max_iters` bounded by the budget): `read_file`,
`grep_repo`, `find_refs` from `hunter_tools`, path-clamped. An answer with a family outside `FAMILY_IDS` or a
cited line outside the shown text is recorded `unknown` and scores against the program (3.6, 3.7).
### Optimisation and acceptance (one change per round, 6.8)
1. Compile folds and held-out folds come from `learn.folds` by repository (2.5); the pilot is refused by digest.
2. A round changes one thing: a producer mask (ablation), an instruction (GEPA or MIPROv2 proposal), or a
demonstration set (BootstrapFewShot). DSPy proposes; `search_metric` scores on compile folds.
3. `accept` scores the candidate on held-out folds, per family, with intervals from the paired bootstrap; keeps
it only if compile and held-out are both non-negative, one strictly positive, no family below its regression
budget, instruction under the length cap; ties rejected; rejected candidates kept visible to the next round;
held-out never shown to the proposer (6.2 to 6.7).
4. The artifact records both fold results and the rule (6.7).
## System flows
```mermaid
sequenceDiagram
participant M as maintainer
participant A as hunt.ablation
participant T as FeatureTable
participant G as HuntProgram (dspy)
participant R as hunter_arm.run
participant S as score + metrics
M->>A: register manifest (table digest, masks, folds, budget, rule)
A->>T: load table, apply mask for arm
A->>G: build program (artifact or baseline)
A->>R: run arm over compile folds (resumable records)
R->>S: score per change
A->>A: paired analysis vs comparison arm (bootstrap by repo, McNemar)
A->>M: importance table, decision (accept.py), artifact id
```
## Data flow and storage
- Tables, records and DSPy caches live outside the checkout under `$OUSAST_RESULTS/hunt/<run>/` (9.4); the run
artifact and the importance table are copied into `benchmarks/measurements/<date>-hunt-<run>/`.
- Budgets: money and calls per run through `dspy.track_usage()` and litellm prices (9.1, 9.5); errors recorded by
type (9.2); records resumable by `(change_id, input_digest, program_id, mask)` (9.3).
## Error handling
- A producer that cannot compute writes `provenance.<name>.ok=false` with a reason; the row stays (3.4).
- A change with nothing readable is an instrument failure, never a clean result (8.4); more than 5% invalidates
the run (8.5).
- A parse rejection in `source.ranges` is recorded as a provenance reason, not swallowed.
## Testing strategy
- Unit: table masks and digests; each producer on a fixture repository; the planted-advisory test (3.7); the metric
on scorer fixtures; the acceptance rule on synthetic fold results (every clause of 6.1 to 6.8).
- Program: a scripted `dspy.LM` (DSPy's dummy LM) drives ReAct through the three tools; no network in tests.
- Integration: one ablation round on a five-change fixture manifest with the scripted LM; the artifact round-trips.
- Live, budgeted: the pilot run once per accepted program (8.6), recorded under `benchmarks/measurements/`.
## Requirements traceability
| Requirement | Components |
|---|---|
| 1.1, 1.2, 1.3, 1.4, 1.5, 1.6 | FeatureTable label columns, search_metric, training_set.py (verification record next to the label) |
| 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7 | training_set.py, learn.folds, learn.leaks audit over table columns, RunArtifact digests, pilot refusal in ablation |
| 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8 | FeatureTable schema v1, producers (planted-advisory test for 3.7), HuntSignature, program output check |
| 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8 | ablation (manifest, masks, paired runs, importance table, curve), search_metric |
| 5.1, 5.2, 5.3, 5.4, 5.5, 5.6 | HuntProgram, RunArtifact, DSPy cache per run, scorer and labels outside the editable surface, length cap in accept |
| 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8 | accept |
| 7.1, 7.2, 7.3, 7.4, 7.5 | search_metric (search stage), hunter_arm classification (second stage, unchanged), report |
| 8.1, 8.2, 8.3, 8.4, 8.5, 8.6 | hunter_arm `--program`, measurements record, instrument proof in records |
| 9.1, 9.2, 9.3, 9.4, 9.5 | RunArtifact, dspy.track_usage, error types, results directory, resumable records |
## First increment (what task group 1 builds)
`hunt/table.py`, `hunt/producers.py` with the four existing producers, `hunt/program.py` with the baseline
signature equal to today's hunter prompt plus the hunks, `hunt/metric.py`, `--program` in `hunter_arm.py`, the
scripted-LM tests, and one baseline run over the 12 pilot positives compared with the pilot's hunter records to
prove the plumbing before any optimisation.
@@ -0,0 +1,300 @@
# Requirements Document
## Project Description (Input)
Maintainer, 2026-10-13: "the hunter would then need a dspy process now, so that we iteratively refine our
llm integration. we can distinct positives from false positives. and dspy can mutate prompts with its
statistical tools. the key is to produce enough information for the llm to use it correctly." Then:
"threshold based assessments come later. right now the search is too bad. if I cut a threshold at 50:50
that is coinflip optimization. what we need rather than that is an annotate model for the llm, with
statistical ablation." And: "the answer to our problem is feature engineering + math." Before that:
"proof should be a resort to face with certainty. before our search problem needs to be solved to reduce
what we have to proof."
Who has the problem: the pre-push safety net. The valid benchmark pilot
(`benchmarks/measurements/2026-10-12-valid-benchmark-pilot/NOTES.md`) measured the LLM tool hunter at
5 of 12 verified positives found and 54 false alarms per 100 ordinary pushes after the decision-engine
classifier. The hook as shipped found 0 of 12.
Current situation: the hunter's search prompt is a hand-written string, its family answer is free text,
and it never sees the diff. Of 343 ordinary-push suspicions the classifier called `vulnerable`, 250 sit
more than 10 lines from any changed line: the hunter re-reports the repo's old code on every push. Every
precision gain so far was a hand rule or a threshold picked after looking at the misses, in-sample. A
threshold on today's output is a coin flip dressed as a decision. The learning harness that compiles the
decision-engine classifier (bootstrapped demonstrations, instruction proposals, grouped folds, a leak
audit, paired A/B experiments with cluster bootstrap, a cached and replayable model caller) exists, but
the hunter does not run on it, and the compile step accepts the best score on one validation split with
no train-against-holdout check, no per-family regression check, and no prompt length cap.
What should change: the search gets better before anything is thresholded. The hunter receives an
annotation model: a named, versioned set of facts about the push computed from the repository (changed
hunks with line numbers, the enclosing function and its callers, entry-point evidence, source, sink and
sanitizer roles, framework facts, def-use inside the function, guards and obligations, closed taxonomy
families with the classifier's answer format). Which facts the model needs is decided by statistical
ablation: each annotation is switched on and off in pre-registered, paired experiments over the same
changes, and its effect on search recall over fix sites and on suspicions per ordinary push is reported
per family with intervals. An annotation or a prompt change is kept only when it helps on held-out folds.
Labels come from the benchmark's rule: a suspicion is a true positive when it lands on a verified fix
site, a false positive when it sits on an ordinary push. Training data is sourced separately (about 100
advisories through the fix-site catalog and verifier, about 40 accepted positives) with repository-level
folds and the leak audit; the 252-change pilot stays held out and is only used to report the gain.
Thresholds and operating points come after the search is good; this spec reports the whole recall
against false-alarm curve and sets none. The proof arm runs only on what survives.
## Introduction
This spec turns the LLM hunter from a hand-written prompt into a measured search stage. It owns four
things: the labelled data the loop learns from, the annotation model the hunter is given about a push,
the ablation method that decides which annotations stay, and the rule by which a changed prompt or
annotation set is kept or thrown away. It reports in the benchmark's own terms: recall, false alarms per
100 pushes, precision, per family, with intervals. It sets no threshold.
## Boundary Context
- **In scope**: labelling hunter suspicions from fix-site positives and ordinary pushes; sourcing a
training set separate from the held-out pilot; the annotation model and its producers; statistical
ablation of annotations; the hunter as a versioned, replayable compiled program; the acceptance rule
for a prompt, demonstration or annotation-set change; re-measuring the pilot and reporting the gain.
- **Out of scope**: choosing confidence or scope thresholds and operating points (later, once the search
is measured good); wiring the compiled hunter into the pre-push hook (`pre-push-safety-net`); the proof
arm itself (`search-with-proof`, `proof-arm-opencode`); building new deterministic analyses such as a
whole-repo call graph or taint engine (`engine-change-scoped-analysis` owns those; this spec consumes
what exists); growing the benchmark beyond what this loop needs (`valid-detection-benchmark`).
- **Adjacent expectations**: the fix-site catalog and verifier produce the labels; the benchmark scorer
and metrics are the only scorekeepers; the decision-engine classifier stays as the second stage and is
not retrained here; the earlier freeze on hunter changes ("no hunter prompt, tool or configuration may
change before that measurement") is lifted by the pilot measurement, which is the baseline.
## Decision strategy, corrected 2026-10-14
The maintainer's finding after the second access-control screen: the annotations this loop selected and
screened were software features, not security features. Counts of callers, entry points, calls, binds,
function length, diff shape, language and framework describe the code around a change; none of them
determines that the code is unsafe, and a decision model fitted on them learns the pool (the deletion count
separates reversed fixes at 0.90) and generalises to nothing. "If you build a safety net and only send
information unrelated to security, the expected result is nothing."
What decides safety for the access-control family is a property of a path, in order: an entry an attacker
can reach, request-derived data reaching a protected operation along that path, and no required check
dominating the operation by control flow. That object, the ordered trace with argument provenance and
control-flow dominance, is specified and built in `access-control-prover` (requirements 9 and 10). From
this date:
1. The hunter's input is the trace, not the feature table. Shape annotations may be rendered for context but
are not decision features and the ablation shall not credit them as such.
2. The pool measures; it never trains. No model is fitted on the pool's labels, no prompt is compiled on the
pool, no threshold is tuned on it. The verdict is a rule over the trace, the same on every repository;
the facts naming entries, operations and checks are closed data with a stated source.
3. Where a judgement remains (is this entry meant to be public; is this operation protected somewhere the
facts do not model), the judge reads the trace zero-shot with general security knowledge. The pool
reports how often it is right; the unseen pilot says whether that holds elsewhere.
4. The ablation's remaining use is selection, not fitting: which trace components the judge needs to see,
with intervals, read with the same suspicion as any in-sample number.
5. Requirements 3.3 and 4 below stand as written for the record of the first round; where they name shape
annotations as the model's input, this section overrides them.
## Requirements
### Requirement 1: Labels from fix sites and ordinary pushes
**Objective:** As the maintainer, I want every hunter suspicion labelled true or false by the benchmark's
own rule, so that the loop learns from facts and not from my reading of the misses.
#### Acceptance Criteria
1. When a hunter suspicion names a path and line inside a verified fix-site function of a positive change,
or within the benchmark scorer's location tolerance of it, and its family equals the change's family,
the Learning Loop shall label it a true positive.
2. When a hunter suspicion is raised on an ordinary push, the Learning Loop shall label it a false positive.
3. When a hunter suspicion is raised on a positive change but outside every verified fix site of that
change, the Learning Loop shall label it unknown and shall exclude it from training and scoring.
4. The Learning Loop shall label at the level of the suspicion (path, line, family), and shall also derive
the push-level outcome (caught, alarmed) with the benchmark scorer, so that both the search stage and
the push-level metric are measurable from the same records.
5. When a positive change is used for training, the Learning Loop shall record its fix-site verification
record (model checks and human decision) next to the label, so that a label can be traced to its
evidence.
6. If a label's source cannot be traced to a verified fix site or a frozen ordinary draw, the Learning
Loop shall reject it.
### Requirement 2: A training set apart from the held-out pilot
**Objective:** As the maintainer, I want the loop to learn on data the benchmark never reports on, so that
the pilot number stays a measurement.
#### Acceptance Criteria
1. The Learning Loop shall source its training positives through the fix-site catalog and verifier from
advisories and repositories that are not in the held-out pilot, and shall record the sourcing run.
2. The Learning Loop shall draw its training negatives as ordinary pushes from the training repositories
only, at the benchmark's ratio of at least 20 ordinary pushes per positive change.
3. The training set shall contain at least 40 accepted positive changes across at least two languages
before the first ablation result is reported; a run on fewer is marked a dry run.
4. While an ablation, compile or evaluation runs, the Learning Loop shall never read the held-out pilot's
changes, labels or records, and every artifact shall record which data digests it did read.
5. The Learning Loop shall split the training set by repository group into folds, so that no repository
appears on both sides of a split.
6. Before the first ablation, the Learning Loop shall run the leak audit over every annotation and shall
report any annotation that separates positives from negatives on its own above the audit's threshold,
so that an annotation that encodes the label is found before it is credited.
7. If a training repository is later found in the held-out pilot or in any benchmark report, the Learning
Loop shall mark every run that used it as contaminated and shall exclude those runs from any reported
gain.
### Requirement 3: The annotation model
**Objective:** As the maintainer, I want the facts the hunter sees about a push to be a named, versioned
set that each can be switched on or off, so that "enough information for the model" is something we can
measure rather than guess.
#### Acceptance Criteria
1. The Learning Loop shall define the annotation model as a versioned schema of named annotations, each
with a producer, a bound on its size, and a digest, and shall record the schema version on every
hunter run.
2. The Learning Loop shall compute every annotation from the repository at the push's revisions and from
the tool's own framework knowledge only; no annotation shall read a label, an advisory, a CVE or GHSA
identifier, a fix commit, or security-worded commit text.
3. The annotation model shall include at least: the changed hunks with head-side line numbers; the
enclosing function's full text at head; the names and locations of that function's callers; entry-point
evidence (an in-repo route, handler, command or public API that reaches the function); source, sink and
sanitizer roles for the identifiers in the function; the framework facts that apply to the file;
def-use evidence inside the function; the guards and obligations on the changed path; and the closed
list of taxonomy families with a one-line definition each.
4. When an annotation's producer cannot compute it for a push, the Learning Loop shall record the
annotation as absent with the reason, and shall not substitute a default that could be read as a fact.
5. The Learning Loop shall render the enabled annotations into the hunter's input in a fixed order with
fixed labels, so that two runs with the same annotation set and the same push produce the same input.
6. The Learning Loop shall require the hunter's family answer to come from the closed list; an answer
outside the list is recorded as `unknown` and counts against the program.
7. The Learning Loop shall require the hunter's answer in the same fixed structure the classifier uses
(verdict, family, confidence, rationale, cited lines), and shall reject an answer whose cited lines
fall outside the text it was shown.
8. When the hunter reports a suspicion, the Learning Loop shall record whether it lies on a changed line,
within 10 lines of one, or farther away, so that scope is a measured property of every suspicion and
not a filter.
### Requirement 4: Statistical ablation decides what the model needs
**Objective:** As the maintainer, I want each annotation's worth measured by switching it off, so that the
input the hunter gets is built from effects with intervals, not from intuition.
#### Acceptance Criteria
1. Before an ablation runs, the Learning Loop shall register it: the annotation set under test, the
annotations switched off, the folds, the metric, the sample size, the budget and the decision rule.
2. The Learning Loop shall run every ablation arm over the same changes as its comparison arm, paired by
change, with the same program, model and sampling, so that the only difference between arms is the
annotation set.
3. For each ablation, the Learning Loop shall report the paired effect on search recall over fix sites
and on suspicions per ordinary push, per family and pooled, with a 95% interval from a bootstrap
clustered by repository and a paired test on the per-change outcomes.
4. The Learning Loop shall report, after a full ablation round, an importance table: for every
annotation, its effect on recall and on suspicions per ordinary push when removed from the full set and
when added to the baseline, with intervals and sample counts.
5. The Learning Loop shall keep an annotation in the hunter's input only when its removal lowers search
recall or raises suspicions per ordinary push on held-out folds with an interval that excludes zero,
or when its removal has no measurable effect and it is needed by an annotation that does.
6. If an annotation raises suspicions per ordinary push without raising recall, the Learning Loop shall
drop it and shall record the result where the next round can see it.
7. The Learning Loop shall not use a confidence cut, a scope cut or any other threshold on the hunter's
output as the quantity being optimised; confidence is reported, not tuned, in this spec.
8. The Learning Loop shall report the full recall against suspicions-per-push curve of each annotation
set, so that a later operating-point decision has the whole curve and not one point of it.
### Requirement 5: The hunter as a compiled, replayable program
**Objective:** As the maintainer, I want every version of the hunter's prompt, demonstrations and
annotation set to be an artifact I can name, diff, replay and roll back, so that an improvement is a fact
and not a memory.
#### Acceptance Criteria
1. The Learning Loop shall store each compiled hunter as a single content-addressed artifact holding the
instruction, the demonstrations, the annotation schema version and enabled set, the model, the data
digests, every scored configuration and the spend.
2. When the hunter runs, the Learning Loop shall record the program id and the annotation set on every
suspicion it emits.
3. The Learning Loop shall route every hunter model call through a cached, metered caller, so that a
rerun of the same program on the same inputs reproduces the same answers at no cost.
4. The Learning Loop shall keep the scorer, the labels and the held-out data outside every surface the
compile step may edit, so that a proposal cannot read its own answer key.
5. If a run exhausts its budget before finishing, the Learning Loop shall write no program artifact and
shall report the run as unfinished with the spend so far.
6. The Learning Loop shall cap the instruction length per family, and shall reject a proposal that
exceeds the cap regardless of its score.
### Requirement 6: Accepting or rejecting a change
**Objective:** As the maintainer, I want a prompt, demonstration or annotation-set change kept only when
it helps on data it was not tuned on, so that the loop cannot overfit its way to a better number.
#### Acceptance Criteria
1. When a candidate program is scored, the Learning Loop shall score it on the compile folds and on the
held-out folds separately, per family, and shall report both.
2. The Learning Loop shall accept a candidate only when its compile-fold and held-out-fold results are
both non-negative against the current program and at least one is strictly positive.
3. If any family's held-out result falls below its regression budget, the Learning Loop shall reject the
candidate, whatever the aggregate shows.
4. If a candidate ties the current program within the pre-registered tolerance, the Learning Loop shall
reject it.
5. The Learning Loop shall report every result with its sample count and a 95% interval, and shall not
accept a candidate on a point estimate alone.
6. The Learning Loop shall keep rejected candidates and their scores where the next proposal round can
see them, and shall never show the held-out results to the proposer.
7. When a candidate is accepted, the Learning Loop shall record the comparison (both programs' ids, both
fold results, the decision rule) in the new program's artifact.
8. The Learning Loop shall change one thing per round (one annotation, or one instruction, or one
demonstration set), so that an accepted gain is attributable.
### Requirement 7: Two stages, reported for what each owns
**Objective:** As the maintainer, I want the search stage and the classifier stage measured separately,
so that we fix the search problem before paying for proof.
#### Acceptance Criteria
1. The Learning Loop shall report the search stage on recall over verified fix sites (did a suspicion land
on the fix site with the right family) and on suspicions per ordinary push, per family.
2. The Learning Loop shall report the classifier stage on precision over the suspicions the search stage
emitted, and on the push-level false alarms per 100 ordinary pushes after it, per family.
3. The Learning Loop shall report the combined push-level result as recall, false alarms per 100 pushes
and precision with 95% intervals, in the benchmark's names and no coined ones.
4. The Learning Loop shall report the gate distance on every run: the recall reached at 10 false alarms
per 100 pushes and the false alarms at 90% recall, read off the curve, as a report and not as a tuned
operating point.
5. The Learning Loop shall not retrain or recompile the classifier stage within this spec; a classifier
change is a separate experiment.
### Requirement 8: Measuring the gain on the pilot
**Objective:** As the maintainer, I want one number that says what the loop bought, measured on the data
it never saw, so that the decision to put the hunter on pre-push rests on evidence.
#### Acceptance Criteria
1. When a compiled hunter is accepted, the Learning Loop shall run it once over the held-out pilot's 252
changes with the same scorer and metrics as the pilot, and shall write the result next to the pilot's
measurement.
2. The Learning Loop shall report the gain as the before-and-after of search recall, suspicions per
ordinary push, push-level recall, false alarms per 100 pushes and precision, per family, with
intervals, against the pilot's recorded hunter numbers.
3. The Learning Loop shall report, for the same run, the number of suspicions the proof arm would have to
handle per 100 pushes, so that proof's workload is a measured count.
4. The Learning Loop shall prove its instrument in every pilot run: for each change it shall record bytes
read per changed file and the verified base and head, and shall mark a change with nothing readable as
an instrument failure rather than a clean result.
5. If a pilot run's instrument failures exceed 5% of changes, the Learning Loop shall report the run as
invalid and shall not quote its numbers.
6. The Learning Loop shall run the pilot at most once per accepted program, and shall record each run, so
that the pilot is not used as a tuning surface.
### Requirement 9: Cost, secrets and unattended runs
**Objective:** As the operator, I want ablation and compile runs to stay inside a budget, keep the model
key out of every record, and finish without me, so that the loop can run overnight on the research host.
#### Acceptance Criteria
1. The Learning Loop shall take a pre-registered budget in money and calls per run and shall stop at the
first exceeded bound.
2. The Learning Loop shall never write a model key, or an exception message that could carry one, into a
record, a log or an artifact; an error is recorded by its type.
3. When a run is interrupted, the Learning Loop shall resume from its recorded progress without repeating
finished model calls.
4. The Learning Loop shall write its outputs outside the repository checkout and shall keep each record
once, so that an overnight run cannot fill the host's disk with duplicates.
5. The Learning Loop shall report, at the end of every run, the spend, the calls, the replay hits and the
wall time.
@@ -0,0 +1,143 @@
# Research and design decisions: hunter-learning-loop
## Summary
- **Feature**: `hunter-learning-loop`
- **Discovery scope**: Extension of the learn harness and the valid benchmark; one new runtime dependency (DSPy).
- **Maintainer direction, 2026-10-13**: "the core we need to evolve is feature engineering ... that is the deciding
factor here"; "labels for me are a pandas task"; DSPy for the hunter program first, statistics libraries second,
no OpenAI SDK on its own ("unless dspy pulls it in"; it does, through litellm); lizard against its alternatives is worth
pursuing; labels and folds are a pandas and scikit-learn task; Semgrep OSS is a good idea. Each of those is a
feature producer or a feature consumer, and the feature table decides between them.
- **Key findings**:
- The pilot (benchmarks/measurements/2026-10-12-valid-benchmark-pilot) measured the hunter at 5 to 6 of 12 and 54
false alarms per 100 pushes after the classifier; 250 of 343 "vulnerable" suspicions on ordinary pushes sit more
than 10 lines from the diff. The hunter never sees the diff (01-flowcharts/U6-hunter-client.md).
- The harness already has the pieces the loop needs: labels (`learn/labels.py`), repository folds and leak audit
(`learn/folds.py`, `learn/leaks.py`), paired experiments with cluster bootstrap and McNemar
(`learn/experiments.py`), the cached caller (`learn/program.py`), and the runner that joins hunter output to
frozen changes (`benchmarks/valid/hunter_arm.py`). What it lacks is a feature table and a program runtime that
can be optimised rather than hand-edited.
- `source.facts_for` (P1, commit 062a7b7) is the first feature producer: language, enclosing function, span,
callers, entry points, from the repository only.
## Research log
### DSPy as the program runtime
- **Context**: requirements R3 to R6 need a signature with switchable inputs, demonstrations, instruction
proposals, a scorer-driven compile, and metered, replayable model calls.
- **Sources**: local install, `dspy==3.4.1` into the research venv (`uv pip install --dry-run dspy`: 42 packages,
among them `litellm 1.105`, `openai 2.54`, `gepa 0.1.4`, `diskcache`, `json_repair`, `tenacity`, `pydantic`);
API introspection of the installed package (`dspy.LM(model, cache=True, num_retries=3, ...)`,
`dspy.ReAct(signature, tools, max_iters=20)`, `dspy.Predict`, `dspy.Signature` with `InputField`/`OutputField`,
`dspy.Evaluate(devset, metric, num_threads)`, `dspy.BootstrapFewShot`, `dspy.MIPROv2(metric, auto=...)`,
`dspy.GEPA(metric, auto, reflection_lm, candidate_selection_strategy="pareto")`, `dspy.track_usage()`,
`dspy.Module.save(path)`).
- **Findings**: `dspy.LM` reaches DeepSeek and OpenRouter through litellm with an OpenAI-compatible base URL and the
key from the environment; the project's model id `deepseek-flash` is not in litellm's price table, so prices are
registered from our table (`model/endpoint.py:55`). DSPy caches responses on disk (`diskcache`) keyed by the
request; a run's cache directory is settable, so a run can be replayed from its own directory. `dspy.ReAct` is
the tool-calling loop the hunter already is (read_file, grep_repo, find_refs). GEPA is the optimiser the
learning-harness brief described (per-instance Pareto archive, reflective edits); MIPROv2 is the instruction and
demo search. Neither applies our acceptance rule; that stays outside DSPy (R6).
- **Implications**: the hunter program is written as a DSPy module from the start; the classifier
(`learn/program.py`) stays as is for this spec (R7.5). The hook's urllib client is untouched: the hook has no LLM
tier yet, and the core install stays PyYAML-only; DSPy is a research extra.
### The feature table (labels and features as a pandas task)
- **Context**: R1, R3, R4 need one row per candidate location with named feature columns, a label, provenance, and
the ability to switch columns off per ablation arm.
- **Findings**: `learn/features.py` builds feature records as nested dicts and parts; `learn/labels.py` (979 lines)
joins sources by hand; `benchmarks/unseen/score.py` joins records by id. All of it is group-by, merge and
dedup work. pandas and scikit-learn (grouped folds, calibration, metrics) are what the maintainer expects here and are pulled
by none of the current extras.
- **Implications**: the loop's contract is a DataFrame (persisted as JSONL or parquet) with a fixed column schema;
producers add columns, ablation arms drop columns, the program renders a row into its prompt. Existing dict-based
builders are consumed, not rewritten, in this spec; migrating labels to pandas is part of the statistics item,
not this one.
### Feature producers and the tools under evaluation
- **Context**: the annotation model (R3) lists the facts; the maintainer wants the producers chosen by what gives
the best features.
- **Candidates**: `source.facts_for` (ours; P1), the semantic overlay facts (roles, def-use, obligations;
`semantic/`), `mapping.analyze_entry_points`, lizard (function spans, nesting, cyclomatic complexity; pure
Python), universal-ctags (spans across many languages; a binary), Semgrep OSS or Opengrep (pattern
and taint findings as features; also a candidate to replace the quick-rule engine, a separate decision).
- **Result, lizard (benchmarks/measurements/2026-10-13-span-engines/REPORT.md)**: on 610 pair files and 570 labelled
functions, lizard matches our parser engine line for line on C, Python and Java (end-line difference 0.00,
name-set Jaccard 0.97 to 1.00) and survives the 56 excerpts the cpp grammar rejects, but loses 6 of 44 labelled
JavaScript functions to naming (`$` identifiers, `function anonymous`, nested named functions, assigned function
expressions), gets TypeScript end lines wrong on 22% of labels (one by 81 lines), has no Perl reader, and is the
slowest of the four engines. Verdict: keep ours; lizard's per-function cyclomatic complexity and nesting are a
candidate feature column, not a span engine (its nesting correlates with `measure_nesting` at Spearman 0.6, so
it is not a drop-in there either).
- **Result, tree-sitter-language-pack**: the same tree-sitter, 503 of 503 spans identical with our parser engine,
1.4x faster (it caches parsers), one 6 MB MIT wheel in place of six grammar wheels, and it adds Go, Ruby, PHP
and Perl grammars that ours handles by regex today. Adopt it as the grammar packaging in `semantic/extra.py`;
map `.c` to the c grammar first (20 of the 56 rejections clear); cache the parser. `tree-sitter-languages` is
ABI-broken under tree-sitter 0.26; universal-ctags has no pip build.
- **Result, Semgrep CE and Opengrep (benchmarks/measurements/2026-10-13-semgrep-brief/BRIEF.md)**: the engine is
LGPL-2.1 and shelling out carries no obligation, but every Semgrep-maintained registry rule has been under the
Semgrep Rules License since 2024-12-13 (internal use only, no distribution, not in competing products), so
nothing but our own converted rules could ship. Measured here: 4.7 s startup floor and about 5 to 10 ms per
file after it (the regex tier runs in-process under a second); converted regex and taint rules fire with
traces for Python, PHP and JavaScript; C propagation through `strcpy` does not (propagators are Pro); Groovy has
no parser; a Semgrep sanitizer silenced the `ORDER BY` shape our `quoted_only` flag refuses on purpose; guards,
dispatch facts, obligations, shadow status and emit floors have no equivalent. A swap would retire about 1,250
lines. Recommendation taken from the brief: option C first, a Semgrep arm on the valid pilot
(`benchmarks/valid/semgrep_arm.py`, our rules converted, scored by the same scorer) before any decision on the
quick tier; adopting it later means amending the "no second engine" rule in safety-net.md with that measurement.
For this spec, Semgrep findings are a candidate feature column like any other producer.
- **Research needed**: Semgrep registry rule licence terms for an open-source tool.
### Training data apart from the pilot
- **Context**: R2. The pilot (252 changes, 12 positives) is held out.
- **Findings**: `benchmarks/valid/catalog.py` and `verify.py` produce fix-site positives from OSV advisories in
about 12 accepted per 32 advisories; the used-set guard (`benchmarks/unseen/*`) excludes training-only sources.
`learn/folds.py` groups by repository; `learn/leaks.py` audits per-signal separation.
- **Implications**: about 100 advisories give about 40 positives; negatives at 20 per positive from the same
repositories; folds by repository; the pilot is never read by the loop.
## Architecture pattern evaluation
| Option | Description | Strengths | Risks | Notes |
|---|---|---|---|---|
| A. Extend `learn/program.py` with a hunter signature | one harness, our caller and cache | no new dependency | the compile loop has no train-vs-holdout rule, no tool loop, no optimiser beyond demos+instructions; everything the maintainer asked for (statistical mutation of prompts) is to build | rejected |
| B. New `hunt/` package on DSPy, feature table as the contract, harness reused for labels, folds, leaks, paired analysis | the optimisers, the tool loop, caching and usage come with the library; our code is the feature producers, the table, the acceptance rule, the scoring | one heavy research dependency; two caches (DSPy's and `Caller`'s) until the classifier migrates | selected |
| C. Rewrite the classifier onto DSPy too | one runtime | large rewrite of a parked line | later, if B earns it |
## Design decisions
### Decision: DSPy runs the hunter program; the acceptance rule stays ours
- Alternatives: A above; a hand-written optimiser.
- Selected: `dspy.ReAct` over a `HuntSignature` whose inputs are the rendered feature row plus the changed hunks;
`dspy.GEPA` or `MIPROv2` propose candidates; `dspy.Evaluate` scores them on compile folds with our metric; the
candidate is accepted only by our rule on held-out folds (R6), implemented outside DSPy.
- Trade-offs: DSPy's cache is request-keyed, not content-addressed; a run pins its cache directory and copies it
with the run record. Cost comes from `dspy.track_usage()` with our prices registered in litellm.
### Decision: the feature table is the contract between producers, ablation and the program
- Selected: one row per (change, path, line) candidate with columns `feature.<producer>.<name>`, `label`,
`provenance.*`, `fold`; producers are functions `(root, change, path, line) -> dict`; an ablation arm is a
column mask recorded in the manifest; the program sees only the unmasked columns, rendered in a fixed order.
- Trade-offs: a table per run is larger than nested records; it is also what pandas, scipy and the maintainer read.
### Decision: producers are a fixed list, switched by name, no registry
- `source.facts_for`, `semantic` facts (roles, def-use, obligations), `mapping` entry points, `diff` hunks; lizard and
Semgrep/Opengrep join the list only after their evaluation reports.
### Decision: objective and metric
- Search stage: recall over verified fix sites (a suspicion within the scorer's tolerance of the fix site with the
right family) and suspicions per ordinary push; the DSPy metric returns the per-change score and, for GEPA, a
text feedback naming the missed site or the off-diff suspicions. No threshold is tuned (R4.7); the curve is
reported (R4.8).
## Risks and mitigations
- DSPy or litellm upgrades change behaviour: pin `dspy==3.4.*`, record versions in every run artifact.
- A producer reads a label by accident: producers take the repository and a location only; a test feeds a
repository with a planted advisory id and asserts no feature carries it (R3.7).
- Train-on-test: the pilot path is denied to the loop by construction (the runner refuses the pilot manifest
digest), and every artifact lists the data digests it read (R2.4).
- Cost: budgets per run (R9.1); DSPy cache on; `--replay-only` reruns at no cost.
## References
- DSPy 3.4.1 (installed; https://dspy.ai), GEPA (https://github.com/gepa-ai/gepa), litellm pricing registration.
- PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md, U7-learn-memory-tasks.md; 02-duplication-report.md D1, D4.
- benchmarks/measurements/2026-10-12-valid-benchmark-pilot/NOTES.md (numbers and the filter sweep).
@@ -0,0 +1,23 @@
{
"feature_name": "hunter-learning-loop",
"created_at": "2026-10-13T09:05:00+02:00",
"updated_at": "2026-10-13T12:20:00+02:00",
"language": "en",
"phase": "tasks-generated",
"approvals": {
"requirements": {
"generated": true,
"approved": true,
"note": "maintainer 2026-10-13: '4 first' (DSPy for the hunter program)"
},
"design": {
"generated": true,
"approved": false
},
"tasks": {
"generated": true,
"approved": false
}
},
"ready_for_implementation": false
}
+94
View File
@@ -0,0 +1,94 @@
# Implementation Plan
- [x] 1. Foundation: the feature table and its producers
- [x] 1.1 Create the feature table with a versioned column schema, masks and digests
- One row per candidate location (change, path, line) with key, label, feature, provenance and fold columns
- Rows read and write as JSONL and parquet; a table digest covers rows and schema version
- A mask drops named feature columns and is recorded with the table it was applied to
- Done when a round-trip test shows the same digest after write and read, and a masked table lists its mask
- _Requirements: 3.1, 3.5, 4.2, 5.1_
- [x] 1.2 Produce the four existing annotations from the repository only (P)
- Diff hunks at head with on-changed-line and distance; enclosing function, span, callers, entry points from the source facts; roles, def-use and obligations from the semantic facts; the closed family list with one-line definitions
- An annotation a producer cannot compute is recorded as absent with a reason, never defaulted
- Done when a fixture repository yields every column, and a repository with a planted advisory id and a security-worded commit message yields no feature carrying either
- _Requirements: 3.2, 3.3, 3.4, 3.7, 3.8_
- _Boundary: producers_
- [x] 1.3 Label the rows by the benchmark's rule (P)
- Fix-site rows of positives label 1 with the change's family and fix span; ordinary-push rows label 0; other rows on a positive label null and are excluded from scoring
- Each positive row carries the verification record reference next to the label; a row whose source cannot be traced is rejected
- Done when a fixture manifest with one positive and two ordinary pushes produces the expected labels and the null rows are excluded
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
- _Boundary: FeatureTable, training_set.py_
- [x] 1.4 Install the research extra and pin the program runtime
- Optional dependency group for the loop with the program runtime and the table library, pinned to a minor version; the core install stays unchanged
- Done when the extra installs into the research environment and the core test suite collects without it
- _Requirements: 5.1, 9.5_
- [x] 2. Core: the hunter as a program
- [x] 2.1 Define the hunt signature and the program with the three repository tools
- Inputs are the rendered unmasked feature columns plus the hunks; the output is the classifier's answer structure with a family from the closed list and cited lines inside the shown text
- The tool loop is bounded by the run budget; an answer outside the vocabulary or citing unseen lines is recorded unknown
- Done when a scripted model drives the program through the tools on a fixture and the parsed findings match the fixture's expectation
- _Requirements: 3.5, 3.6, 5.1, 5.2, 5.4_
- [x] 2.2 Score a change: the search metric with feedback (P)
- A suspicion within the scorer's tolerance of the fix site with the right family counts as a hit; ordinary-push suspicions are counted per push; the feedback text names the missed site or the off-diff suspicions
- Done when the metric reproduces the pilot's hit list for the 12 positives from their recorded suspicions
- _Requirements: 1.4, 4.3, 7.1, 7.2_
- _Boundary: search_metric_
- [x] 2.3 Record a run as a content-addressed artifact (P)
- Program id, schema version, mask, data digests, every scored configuration, spend, calls, replay hits, wall time, library versions; the saved program and its cache directory travel with it
- A run that exhausts its budget writes no artifact and reports the spend so far
- Done when two identical runs produce the same artifact id and a budget-exhausted run leaves only the partial report
- _Requirements: 5.1, 5.2, 5.3, 5.5, 9.1, 9.2, 9.5_
- _Boundary: RunArtifact_
- [x] 3. Integration: the runner and the baseline
- [x] 3.1 Run the program through the existing hunter-arm runner
- The runner takes a program artifact and a mask, computes the table rows for each change at head, renders the inputs, runs the program and writes the same record shape as today, resumable by change id, input digest, program id and mask
- The pilot manifest is refused by digest unless the run is explicitly a pilot report run
- Done when a run over a five-change fixture manifest with the scripted model writes records the existing scorer accepts
- _Requirements: 2.4, 8.4, 9.3, 9.4_
- [x] 3.2 Prove the plumbing: baseline program on the 12 pilot positives
- The baseline instruction equals today's hunter prompt plus the hunks; one live, budgeted run over the 12 positives only, as a pilot report run
- Done when the search metric over the new records is compared with the pilot's recorded hunter hits and the difference is explained in the measurement record
- _Requirements: 8.1, 8.4, 8.5, 8.6_
- _Depends: 2.2, 2.3_
- [ ] 4. Core: the training set, folds and ablation
- [ ] 4.1 Source the training set apart from the pilot
- About one hundred advisories through the fix-site catalog and verifier, pilot repositories excluded, at least 40 accepted positives across two languages, 20 ordinary pushes per positive from the same repositories; the sourcing run is recorded
- Done when the frozen training manifest lists its positives, negatives, languages and the pilot exclusion check
- _Requirements: 2.1, 2.2, 2.3_
- [x] 4.2 Fold by repository and audit every annotation for leaks
- Compile folds and held-out folds by repository group; the leak audit runs over every feature column and reports any that separates positives from negatives on its own
- Done when the folds are disjoint by repository and the audit report lists each column with its separation score
- _Requirements: 2.5, 2.6_
- [x] 4.3 Register and run a paired ablation
- A manifest fixes the table digest, the masks, the folds, the metric, the sample size, the budget and the decision rule before anything runs; each arm runs over the same changes with the same program and sampling
- Results per arm carry the paired effect on search recall and suspicions per ordinary push, per family and pooled, with intervals from the repository-clustered bootstrap and a paired test
- Done when an ablation over a fixture manifest with the scripted model yields two arms, the paired statistics, and the full recall-against-suspicions curve
- _Requirements: 4.1, 4.2, 4.3, 4.7, 4.8_
- [x] 4.4 Build the importance table and apply the keep/drop rule
- For every annotation, its effect when removed from the full set and when added to the baseline, with intervals and counts; an annotation stays only on a held-out effect whose interval excludes zero or when a kept annotation needs it; one that raises suspicions without raising recall is dropped and recorded
- Done when the table is written for a fixture ablation and the rule's decisions match the fixture's expectations
- _Requirements: 4.4, 4.5, 4.6_
- [x] 4.5 Contamination check
- A training repository later found in the pilot or in a benchmark report marks every run that used it as contaminated and excludes those runs from any gain
- Done when a fixture run is marked contaminated after its repository is added to a report list
- _Requirements: 2.7_
- [x] 5. Core: proposals and acceptance
- [x] 5.1 Propose candidates with the program runtime's optimisers
- One change per round: an instruction proposal, a demonstration set, or a mask; proposals score on compile folds with the search metric; the proposer never sees held-out results; rejected candidates stay visible to the next round
- Done when a round on a fixture produces a candidate with its compile-fold score and the proposer's inputs are shown to exclude held-out data
- _Requirements: 5.4, 6.6, 6.8_
- [x] 5.2 Accept or reject by the rule
- Compile-fold and held-out results per family; accepted only when both are non-negative, one strictly positive, no family below its regression budget, instruction under the length cap; ties rejected; the comparison is recorded in the new artifact
- Done when every clause of the rule has a synthetic fold-result test that flips the decision
- _Requirements: 5.6, 6.1, 6.2, 6.3, 6.4, 6.5, 6.7_
- [ ] 6. Validation: the pilot gain
- [x] 6.1 Run an accepted program once over the held-out pilot and report the gain
- Same scorer and metrics as the pilot; before-and-after of search recall, suspicions per push, push-level recall, false alarms per 100 and precision, per family, with intervals; the proof arm's workload per 100 pushes; the gate distance read off the curve; the run recorded next to the pilot's measurement, at most once per accepted program
- Done when the measurement record exists with the before-and-after table and the instrument proof for every change
- _Requirements: 7.3, 7.4, 7.5, 8.1, 8.2, 8.3, 8.6_
+16 -16
View File
@@ -1,4 +1,4 @@
# Brief: ir-consolidation — stop maintaining an IR we did not need, and adopt one we do
# Brief: ir-consolidation, stop maintaining an IR we did not need, and adopt one we do
> **Status 2026-10-02:** dormant. Brief only and unapproved. The subtraction it asks for still needs a measured recall comparison before the flat IR goes.
@@ -12,16 +12,16 @@
Two different questions live inside that, and they have opposite answers. Keeping them apart is most of the
value of this brief.
## Question 1 — the source IR we already replaced and never retired
## Question 1, the source IR we already replaced and never retired
`semantic/` is **3,218 lines, 16% of the codebase**: a hand-written IR (`ir.py`, `cst.py`) and a flat taint
engine (`taint.py`), plus the overlay that runs them. `model-grounded-detection` measured that IR against
Joern's CPG and the result was decisive — **2.2% entailment against 41.3%** — which is why the CPG was
Joern's CPG and the result was decisive, **2.2% entailment against 41.3%**, which is why the CPG was
adopted for the model layer. The flat IR was never removed, and still runs as a proposer in every scan.
So the outgrowth is not something we are about to add. It is something already carried.
**Vine and P-Code do not help here.** Both are *binary* IRs — Vine lifts executables (BitBlaze, UC Berkeley,
**Vine and P-Code do not help here.** Both are *binary* IRs. Vine lifts executables (BitBlaze, UC Berkeley,
GPL-2.0, last significant activity 2009), and P-Code lifts machine code. Our source languages are Python,
PHP, JavaScript and Java, and the source IR we should adopt is the one we already have. The work is
subtraction, not substitution:
@@ -32,16 +32,16 @@ subtraction, not substitution:
A candidate ~1,000-line deletion with a number attached, which is the only kind worth doing.
## Question 2 — the abstraction we are missing, where an existing IR is exactly right
## Question 2, the abstraction we are missing, where an existing IR is exactly right
`c/memory` has produced **zero true positives** and three distinct classes of false one. The reason is
structural and now stated in the report (Req 5.6): it models unbounded string CALLS, while real C overflows
are *arithmetic* — an index, a pointer offset, a `memcpy` whose size is computed. Every one of libpng's is,
are *arithmetic* (an index, a pointer offset, a `memcpy` whose size is computed). Every one of libpng's is,
and its library code never calls a modelled string API at all.
"Can this expression exceed this allocation" is a value question, not a reachability one. **Rolf Rolles'
GhidraPAL carries a three-valued-logic abstract interpreter over Ghidra P-Code** — each bit definite 0,
definite 1, or unknown, with abstract transformers for `INT_ADD`, `INT_MULT`, `INT_AND`, the comparisons —
GhidraPAL carries a three-valued-logic abstract interpreter over Ghidra P-Code**, with each bit definite 0,
definite 1, or unknown, and with abstract transformers for `INT_ADD`, `INT_MULT`, `INT_AND`, the comparisons,
validated against concrete execution in its own tests. That is precisely the shape of analysis the C family
lacks, and it exists.
@@ -55,13 +55,13 @@ lacks, and it exists.
**angr is the strongest candidate and Vine is not one.** Three things decide it:
* **It is Python.** This codebase is Python with `dependencies = []` in the core, and every heavyweight
* It is Python. This codebase is Python with `dependencies = []` in the core, and every heavyweight
capability so far has been reached across a subprocess boundary rather than imported. angr fits that
pattern as an extra, the way `harnessx` and the tree-sitter grammars already do.
* **Value-set analysis is the abstraction we are missing**, by name. "Can this index exceed this allocation"
* Value-set analysis is the abstraction we are missing, by name. "Can this index exceed this allocation"
is a VSA question, and VSA is abstract interpretation rather than SMT -- which is the choice
`model-grounded-detection` already made and gave its reasons for.
* **BSD-2-Clause**, against Vine's GPL-2.0. This project has a standing rule about never vendoring code
* BSD-2-Clause, against Vine's GPL-2.0. This project has a standing rule about never vendoring code
without a stated licence, and a GPL dependency is a decision about the whole tool rather than about one
analysis.
@@ -71,19 +71,19 @@ thing to read before writing any abstract domain here, whether the platform unde
It is also the honest answer to a decision this project already took. `model-grounded-detection` chose
abstract interpretation over SMT, and chose to build the model from code because execution cannot be
assumed. A bit-level abstract interpreter over P-Code is *still abstract interpretation* and still static —
what changes is the artifact: it needs a **built** library rather than its source.
assumed. A bit-level abstract interpreter over P-Code is *still abstract interpretation* and still static.
What changes is the artifact: it needs a **built** library rather than its source.
## What this brief must decide
1. Whether the source-IR subtraction is safe, measured rather than assumed.
2. Whether the C arithmetic class is worth a second pipeline at all, or whether `c/memory` stays scoped to
string misuse and says so — which is where task 2.16 left it, deliberately.
string misuse and says so, which is where task 2.16 left it, deliberately.
3. If it is worth it: **angr** first, on language, licence and the fact that value-set analysis is the
named abstraction -- with GhidraPAL read as the worked example of a bit-precise domain rather than as a
platform. Vine is dormant since 2009 and is not a candidate.
4. What a second pipeline costs in the one currency this project has been strict about — **not lines, but
the number of things that can silently return nothing.** This session found three separate quiet failures
4. What a second pipeline costs in the one currency this project has been strict about,
**the number of things that can silently return nothing rather than lines.** This session found three separate quiet failures
in one PHP scan. A binary pipeline adds a build step, a lifter, and a domain, each of which can produce
an empty result that looks like a clean one.
+46 -46
View File
@@ -14,13 +14,13 @@ experiments.
Six facts from the repository and the recorded results shape the design:
- **The deployment target is a repository nobody trained on.** Maintainer: "the tool will never run against the
- The deployment target is a repository nobody trained on. Maintainer: "the tool will never run against the
test cases it's being trained with. it's purpose is a repo safety net." (Req 7). The primary unit is a candidate
in changed code (`ousast pre-push`, base..head), and whole-repository `scan` is the secondary mode. Every
reported number therefore comes from folds grouped by repository across all corpora. Features are
repository-agnostic by an allow-list. Fit to a user's own repository is an opt-in layer outside the reported
numbers.
- **Labels are scarce, and full plane signals exist on 43 rows.** The only recorded plane run with
- Labels are scarce, and full plane signals exist on 43 rows. The only recorded plane run with
verify/agree/tie-break is `validation-46`: 15 cases, 43 candidates after triage, 19 of them with
`site_match` true, all from population v2
(`~/ousast-results/plane/validation-46/*-final/agreed.json`). The pair corpus is larger (427 pairs and 452
@@ -30,19 +30,19 @@ Six facts from the repository and the recorded results shape the design:
classifier over verify signals cannot be evaluated today. The design starts from the signals every labelled row
can have, harvests the rest at a stated cost, and accepts that data will stay small: the engine learns by
memory and compilation, not by fitting parameters.
- **The core install is PyYAML only** (`pyproject.toml:11`). numpy, scipy and scikit-learn are not installed in
- The core install is PyYAML only (`pyproject.toml:11`). numpy, scipy and scikit-learn are not installed in
`.venv`. Retrieval (a Gower distance and a cosine over at most 40 vectors) and calibration (two-parameter Platt)
stay pure Python; the model calls go through the plane's metered client (section 4).
- **`pre-push` already has the decision seam.** `admit_candidates` (`push/policy.py:879`) applies one evidence
- `pre-push` already has the decision seam. `admit_candidates` (`push/policy.py:879`) applies one evidence
bar to `CandidateDelta`s with `novelty` (`push/policy.py:61-74`). Its capability registry is empty by design
(`push/policy.py:802-823`: "No shipped declaration is enabled"), so `pre-push` blocks nothing today. The
engine's operating point replaces the capability-eligibility reasons, and the structural reasons stay.
- **The memory store is the system of record and has the needed shape.** Rows are keyed by repository and pin,
- The memory store is the system of record and has the needed shape. Rows are keyed by repository and pin,
with a closed `KINDS` tuple (`plane/memory.py:52`). There are two backends (FileStore, S3Store) behind one
interface (`plane/memory.py:176-291`), content-addressed blobs (`put_facts`, `plane/memory.py:249-255`), and a
train-on-test `Guard` (`improve/memory.py:95-133`). Features, labels, decisions, model artifacts and experiment
outcomes become new row kinds and blob prefixes. No second store is added.
- **Hand-written framework knowledge is embedded in the detectors** (Req 8). Examples are WordPress `$wpdb`,
- Hand-written framework knowledge is embedded in the detectors (Req 8). Examples are WordPress `$wpdb`,
hooks and `maybe_unserialize` in `ruleset/semantic/php.toml` and `ruleset/php/rules.toml`, and Flask and
Django names in `ruleset/semantic/python.toml`. Section 2 tags each entry. Priors become optional features
that are off by default. Framework roles are inferred per repository instead.
@@ -548,7 +548,7 @@ If the pool cannot satisfy a minimum, take what exists and record `balance: "sho
**The evaluation boundary (Req 3.4, 7.1).** `learn/retrieve.py: eligible(example, target, fold)` is the only way
an example enters a prompt, for retrieved examples and for the compiled demonstrations alike:
- `example.group` is not in the fold's evaluation groups (all of them, not just the target's), and not the
- `example.group` is not in the fold's evaluation groups (every evaluation group, including the target's), and not the
target's group after the CVE/alias merge (`Groups`, `learn/labels.py:182`);
- leave-one-source-out: `example.source` is not the held-out source;
- leave-one-framework-out on F: `F not in example.frameworks`; a compiled demonstration tagged F is swapped for its
@@ -669,23 +669,23 @@ million. Token sizes are estimates to be replaced by the smoke run's measured `u
| candidate (80-line excerpt ~1,100, signals and roles ~300) | ~1,400 | no |
| output (JSON, rationale <= 3 sentences) | ~200 | -- |
- **Per candidate, `k = 1`**: 2,750 x 0.014 + 5,950 x 0.44 + 200 x 1.32 per million = **$0.0029**.
- **Each extra sample** (same prompt, fully cached): 8,700 x 0.014 + 200 x 1.32 per million = **$0.0004**. So
- Per candidate, `k = 1`: 2,750 x 0.014 + 5,950 x 0.44 + 200 x 1.32 per million = **$0.0029**.
- Each extra sample (same prompt, fully cached): 8,700 x 0.014 + 200 x 1.32 per million = **$0.0004**. So
`k = 5` is **$0.0045** with cache hits, and $0.0146 if the cache does not hit (the smoke run decides the default).
- **Embedding**: $0.000024 per candidate; the whole store about $0.10 once.
- **Per compile**: bootstrap 150 x $0.0029 = $0.44; proposals ~$0.01; scoring 600 x $0.0029 = $1.74; total
- Embedding: $0.000024 per candidate; the whole store about $0.10 once.
- Per compile: bootstrap 150 x $0.0029 = $0.44; proposals ~$0.01; scoring 600 x $0.0029 = $1.74; total
**~$2.2, ceiling $3.00**.
- **Per full evaluation** of one program, evaluated families only (475 verified rows outside memory,
- Per full evaluation of one program, evaluated families only (475 verified rows outside memory,
config_secrets and unknown) plus a 200-unit benign-delta sample for false BLOCKs:
- outer folds: 675 candidates, **$2.0 at k = 1, $3.0 at k = 5** (cached);
- leave-one-source-out: 475 more, **$1.4 / $2.1**;
- leave-one-framework-out: $0 today (no framework at the floor);
- total **~$3.4 (k = 1) to $5.2 (k = 5)**, ceiling $7.
- **Learning curves** (section 7): 6 memory-size points + 3 source-count points on a fixed 200-candidate subset at
- Learning curves (section 7): 6 memory-size points + 3 source-count points on a fixed 200-candidate subset at
`k = 1`: 9 x 200 x $0.0029 = **~$5.2**, ceiling $7.
- **A program-variant A/B** on 300 paired candidates: arm A replays cached responses from the evaluation ($0);
- A program-variant A/B on 300 paired candidates: arm A replays cached responses from the evaluation ($0);
arm B costs **$0.9 (k = 1) to $1.35 (k = 5)**.
- **Deployment**: `pre-push` with ~5 candidates is ~$0.02 per push at `k = 5`; a plane Run case with ~30 candidates
- Deployment: `pre-push` with ~5 candidates is ~$0.02 per push at `k = 5`; a plane Run case with ~30 candidates
~$0.14.
**Against the balance.** DeepSeek was ~$19.6 before the harvest; the harvest's verify passes are ~$6 expected and
@@ -807,10 +807,10 @@ flagged in any pass as 17 for the plane vs 19 for the reference, of 20. The plan
(`plane/tasks/verify.py:1-9, 164`, `CALLERS_CAP = 8` at :34), and the comparison is confounded (other changes,
stochastic passes, v2 is where it was seen).
- **Arms.** A is `verify` as today. B is `verify` with `OUSAST_VERIFY_CALLERS=off`, a new switch (about 3 lines)
- Arms. A is `verify` as today. B is `verify` with `OUSAST_VERIFY_CALLERS=off`, a new switch (about 3 lines)
that makes `callers_line` return empty. Same model (`deepseek-flash`), `PER_HUNT = 6`, `MAX_STEPS = 6`, same
candidates, same `facts.json`, same runner image digest, two passes (a, b) per arm.
- **Units: the development corpus, not v2** (v2 generated the hypothesis):
- Units: the development corpus, not v2 (v2 generated the hypothesis):
- population v1, 11 cases, whose engine scans are recorded under `~/ousast-results/independent-v1/scans/` (44
scans);
- pmpro, MediaWiki, WP Statistics (dev-php);
@@ -821,15 +821,15 @@ stochastic passes, v2 is where it was seen).
model-free and frozen in the units file: per pin, the fix-changed functions, plus the top engine- and
quick-alert functions, up to 8 candidates per pin, on the vulnerable and the fixed pin. Only units where arm A's
callers line is non-empty are *treated*. The primary analysis is restricted to them, and the rest are reported.
- **Metric.** Primary: the per-positive-unit rate of "flagged in any pass" (the 17-vs-19 metric), with positives
- Metric. Primary: the per-positive-unit rate of "flagged in any pass" (the 17-vs-19 metric), with positives
from the label builder (fix-range functions on the vulnerable pin). Secondary: the flag rate on fixed-pin
negatives (false flags), and usd per candidate.
- **Sample size and MDE.** About 55 positive units across 41 repositories (about 28 dev + 27 pointer pairs). With
- Sample size and MDE. About 55 positive units across 41 repositories (about 28 dev + 27 pointer pairs). With
a discordance rate of about 0.15, alpha 0.05 and power 0.8, the paired MDE is `(1.96 + 0.84) * sqrt(0.15) /
sqrt(55)`, about **0.15**. The observed gap of 0.10 needs about 115 positive units, so an inconclusive result is
the likely and honest outcome. It is registered as such in advance, and a follow-up needs new units, not a
continuation.
- **Budget.** Hunts per pass per arm: 14 dev repositories x 2 pins x up to 3 files (84) + 27 pairs x 2 sides (54)
- Budget. Hunts per pass per arm: 14 dev repositories x 2 pins x up to 3 files (84) + 27 pairs x 2 sides (54)
= 138. Four arm-passes give 552 hunts x $0.014, **about $7.7 expected**. The ceiling is **$15 total, $7.5 per
arm**, enforced by the task budgets (`plane/budget.py`). Candidate generation reuses the recorded v1 engine
scans. For dev-php it runs the engine once per pin on the host, one container at a time, from a frozen source
@@ -1111,30 +1111,30 @@ Each commit is gated on the full suite's own exit code, `ruff` and `mypy`. None
commit 9 adopts a program, and without an adopted program every path is today's. Steps that spend model money
start only after the maintainer confirms their budget against the balance read just before.
1. **Schema and features** (1.1-1.3, 7.2) -- done.
2. **Framework tags** (8.2, 8.4) -- done.
3. **Roles** (8.1) -- done.
4. **Labels** (2.1-2.3, 7.1) -- done.
5. **Harvest** (1.1, data) -- in progress: verify a/b on the verify-family pairs and the development cases (about
1. Schema and features (1.1-1.3, 7.2) -- done.
2. Framework tags (8.2, 8.4) -- done.
3. Roles (8.1) -- done.
4. Labels (2.1-2.3, 7.1) -- done.
5. Harvest (1.1, data) -- in progress: verify a/b on the verify-family pairs and the development cases (about
$6, ceiling $10), model roles on the candidate files (ceiling $10), engine features on the host.
6. **Program, memory, compile, first evaluation** (3.1-3.6, 5.1-5.2, 7.3-7.4, 8.3): excerpts and the example
6. Program, memory, compile, first evaluation (3.1-3.6, 5.1-5.2, 7.3-7.4, 8.3): excerpts and the example
store, metered embeddings and the cache, retrieval with the boundary test, the program and response cache, folds,
compile, cross-fitted calibration; then the paid slice (smoke, embed, compile, outer evaluation, ~$5, ceiling $7)
and, after a second go-ahead, leave-one-source-out and the memory-size curves (~$7, ceiling $10), committed under
`benchmarks/measurements/<date>-decision-engine-v1/`.
7. **Experiments** (4.1-4.4): `learn/experiments.py`, `register`, `experiment-outcome`, the Run annotation checks,
7. Experiments (4.1-4.4): `learn/experiments.py`, `register`, `experiment-outcome`, the Run annotation checks,
`OUSAST_VERIFY_CALLERS`, exp-001 registered, run and recorded, and the program-variant experiment machinery.
8. **Integration** (6.1-6.3, 7.3, 7.5): the `embed` and `decide` tasks with the seeded memory, the `scan` stage,
8. Integration (6.1-6.3, 7.3, 7.5): the `embed` and `decide` tasks with the seeded memory, the `scan` stage,
`pre-push` admission with the engine, report fields, the fallback line, and the opt-in adaptation layer.
9. **Adopt** (3.3, 8.2): `ousast learn adopt`, the packaged program with the licence filter, the priors-on/off and
9. Adopt (3.3, 8.2): `ousast learn adopt`, the packaged program with the licence filter, the priors-on/off and
`k` experiments with recorded decisions.
10. **v3 check** (5.3): `prediction-v3.json` is committed first, then v3 is run by its owner under protocol v3, and
10. v3 check (5.3): `prediction-v3.json` is committed first, then v3 is run by its owner under protocol v3, and
`result-v3-engine.json` is committed. Nothing between the two commits may touch the program, memory snapshot,
features, priors or thresholds.
## Risks
- **Label scarcity decides what can be evaluated.** The status of each family today:
- Label scarcity decides what can be evaluated. The status of each family today:
- memory (5 repository groups, 173/173 rows, almost all openssl and curl), config_secrets (5) and prototype are
**insufficient data** (`labels.json` `verified_pin_labels`). Users get today's behaviour there, labelled as
such, and no money is spent evaluating them.
@@ -1143,16 +1143,16 @@ start only after the maintainer confirms their budget against the balance read j
- The plane profile has 43 rows from one population until the harvest Run.
- BLOCK at 95% precision is likely **not reachable** for most families on this data. The design reports "BLOCK
not offered" instead of lowering the target.
- **Weak signals, strong code.** The pair baseline has quick-rule labelled recall 0.24 and Youden -0.013 overall
- Weak signals, strong code. The pair baseline has quick-rule labelled recall 0.24 and Youden -0.013 overall
(`pairs.json` `overall`); the v2 engine recall is 0/17 (`results-v2.json`). The program reads the code, so it
can beat the signals, but the verify history says the model over-flags (v1 1/12, v2 0/17 adjudicated true). A
calibrated classifier that mostly says "advisory" is honest; recall at ADVISORY 0.90 may need a threshold with
poor precision. That is reported, not tuned away on outer folds.
- **LLM nondeterminism.** The same prompt can answer differently, and temperature 0 is not a provider guarantee.
- LLM nondeterminism. The same prompt can answer differently, and temperature 0 is not a provider guarantee.
Guards: every response cached and replayable (numbers reproduce at $0); sample 1 at temperature 0; the vote mean
over `k` samples as the score; the A/B unit is paired and both arms' caches are recorded; a re-ask agreement rate
on a fixed 30-candidate canary set is reported with each evaluation.
- **Label leakage into prompts.**
- Label leakage into prompts.
- Through rationales: memory examples carry no rationale; demonstration rationales are produced without the label
and scanned for identities and advisory ids; the candidate's prompt never holds `EXCLUDED_FIELDS` (test).
- Through code: fixed-side excerpts can carry fix comments ("prevent XSS", a CVE id). Advisory ids and security
@@ -1160,11 +1160,11 @@ start only after the maintainer confirms their budget against the balance read j
the change under test, never the later fix.
- Through memorisation: the chat model may have seen public CVE fixes in pre-training. Not removable; the v3
check (unpublished fixes are rarer there) and per-source numbers expose it, and the report names it.
- **Retrieval leakage.** Same repository under another name or corpus: group normalisation, CVE merge, the
- Retrieval leakage. Same repository under another name or corpus: group normalisation, CVE merge, the
near-duplicate drop at cosine >= 0.98 in evaluation, and `test_boundary`. Demonstrations from the compile split
are, by construction, from repositories never evaluated. The seed for plane Runs filters on the host, where the
labels are; the sandbox never receives an example of its own case's group.
- **Label-tracking signals (maintainer decision 2026-10-01: "Neutralise both for v1").** The harvest found two
- Label-tracking signals (maintainer decision 2026-10-01: "Neutralise both for v1"). The harvest found two
signals that follow the pair label instead of the code: verify candidates on fixed sides named functions the side
no longer declares (now never produced: a side's candidates are only the functions it declares, the rest counted
`absent_on_side`), and the engine's "read the file, asked no question" state, which a fixed side reaches by losing
@@ -1173,40 +1173,40 @@ start only after the maintainer confirms their budget against the balance read j
the stored record keeps it. The engine returns only through a pre-registered A/B evaluated on assumed-benign and
benign-control negatives, where it cannot track a fix. `ousast learn audit-leaks` reports how well every signal
alone separates the sides of a pair and flags any above 0.20 (counts under `benchmarks/measurements/`).
- **Earlier leakage paths still apply.** Hand rules and framework priors written after seeing a training repository
- Earlier leakage paths still apply. Hand rules and framework priors written after seeing a training repository
(the `$wpdb` rule from PMPro, the hook source from WP Statistics, `taint.sc:911-949`): priors off by default,
leave-one-framework-out, best-effort `taught_by = [repo]`. The mechanism exporter's train-on-test leak
(`closed-loop-train-on-test-leak.md`): no exporter output is a feature. Verify and tie-break fitted on v2: the v2
fold is design-informed. Thresholds and instructions chosen on reported folds: cross-fitting and the compile
split. v3: fail-closed sources, never opened. User dismissals: refused by both builders.
- **Cost.** Model spend is now per candidate and per evaluation, not one-off. Measured (section 4.6) the first
- Cost. Model spend is now per candidate and per evaluation, not one-off. Measured (section 4.6) the first
slice fits today's balance; everything after it needs a top-up. Guards: `MeteredClient` ceilings on every task
and command, a smoke check that measures real tokens and cache hits before any paid run, cached replay for
re-analysis, unevaluable families skipped, `k = 1` if the prefix cache does not hit. Deployment cost (~$0.02 per
push) is a user-facing change: `pre-push` without a key falls back, and the report states the spend.
- **Model and version drift.** `deepseek-flash` is an alias whose weights can change, and so can OpenRouter's
- Model and version drift. `deepseek-flash` is an alias whose weights can change, and so can OpenRouter's
embedding route. The artifact records model ids, the Model parameters digest and the response `model` field;
before adoption and before the v3 prediction the canary set is re-asked, and agreement below 0.9 with the
cached answers, or an embedding dimension change, invalidates the evaluation (re-embed, re-evaluate) rather than
being averaged in. Prices are data in the Model manifest; a price change changes only cost reports.
- **Latency.** A model call per candidate is slower than a dot product. The 30 s `pre-push` deadline is met by
- Latency. A model call per candidate is slower than a dot product. The 30 s `pre-push` deadline is met by
concurrency and the `undecided: deadline` state; a push with many candidates gets partial decisions, stated.
- **Packaging third-party code.** Demonstrations and memory hold excerpts of corpus repositories. Only
- Packaging third-party code. Demonstrations and memory hold excerpts of corpus repositories. Only
permissively licensed excerpts may ship in the package; the store is never shipped. A user without a store gets
the demonstrations-only program, whose own curve point (memory 0%) and calibration are what they are told.
- **Framework inference is itself a model.** Model roles cost money and can be wrong. `scan` without `[models]`
- Framework inference is itself a model. Model roles cost money and can be wrong. `scan` without `[models]`
gets only wrapper inference, whose reach on framework-heavy code without dependency source is poor. The report
says which role source ran. Both are measured by the roles-on/off experiment. Nothing assumes they help.
- **Host time on the 7 GB host.** Engine features remain the time cost (7-14 hours serial for the pair corpus,
- Host time on the 7 GB host. Engine features remain the time cost (7-14 hours serial for the pair corpus,
dev-php scans of 200-2,000 s each), run once per engine image from a frozen source export, one container at
`--memory 3g` at a time, never beside the kind cluster's verify pods, cached by (content sha, image id). Model
calls add wall time but no memory pressure; excerpts and embeddings are small (~8 KB per example with a 1,536-float
vector, ~45 MB for 5,600 examples).
- **Experiments will often be inconclusive.** With 41 development repositories the MDE is about 15 points.
- Experiments will often be inconclusive. With 41 development repositories the MDE is about 15 points.
Pre-registering that and recording "inconclusive" is the intended behaviour. The failure mode to avoid is
extending an experiment after looking.
- **First paid slice, 2026-10-01 (tasks 6.2, 6.6; injection only).** Four instrument defects surfaced before any
- First paid slice, 2026-10-01 (tasks 6.2, 6.6; injection only). Four instrument defects surfaced before any
number was believed, each fixed with a test:
- *The memory build joined no pair.* The harvest stores a pair side's record under its unit pin (the blob sha1 of
the side's excerpt), not under `label_pin`; `ousast learn memory build --units <harvest units.json>` joins them.
@@ -1227,7 +1227,7 @@ start only after the maintainer confirms their budget against the balance read j
0.90 has precision 0.54 (prevalence 0.53); the majority `vulnerable` verdict alone has precision 0.86, recall 0.43.
Canary re-ask agreement at temperature 0 was 0.83, below the 0.9 the adoption check requires.
- **Slice 2, 2026-10-02 (six families, after the advisory-fix harvest).** Two instrument defects stopped runs before any
- Slice 2, 2026-10-02 (six families, after the advisory-fix harvest). Two instrument defects stopped runs before any
number: the memory build's side reader knew only the first pair-kind source (all 194 advisory sides unread), and
compiled demonstrations skipped the near-duplicate rule that retrieval and the pre-call assertion apply (the
assertion stopped the untrusted_destination evaluation). Both fixed with tests. A confound to read with the new
@@ -25,29 +25,29 @@ money.
## Requirements (proposed)
1. **Mining.** From each fix pair, extract what the fixed function adds on the vulnerable flow: new calls,
1. Mining. From each fix pair, extract what the fixed function adds on the vulnerable flow: new calls,
new conditions guarding the sink, new or changed sink arguments. Record each as a candidate fact with its
language, family, form (sanitizer call, guard predicate, safe argument, sink restriction) and the pairs
that propose it. Mining is deterministic (tree-sitter diff of the two function bodies). No model calls.
2. **Generalisation is explicit.** A candidate is a pattern, not a repository name: a callee name, an
2. Generalisation is explicit. A candidate is a pattern, not a repository name: a callee name, an
argument key, a predicate shape. Candidates naming project-specific identifiers are rejected (Req 8: no
application-specific trees).
3. **Admission by held-out separation.** Folds are by repository, as in the decision engine. A candidate is
3. Admission by held-out separation. Folds are by repository, as in the decision engine. A candidate is
admitted for a fold only if, on that fold's held-out pairs, adding it to the fact tables:
- increases pairs where the vulnerable side keeps a path and the fixed side loses it or becomes
sanitized;
- and does not remove any held-out vulnerable-side path (no recall loss).
A candidate proposed by fewer than 2 distinct repositories in the training folds is not tried.
4. **No train-on-test.** Mining and admission never read held-out pairs; the closed-loop leak recorded in
4. No train-on-test. Mining and admission never read held-out pairs; the closed-loop leak recorded in
memory (`closed-loop-train-on-test-leak`) is the failure this rule exists to prevent. Population v3 is
never read.
5. **Measurement by the trace runner.** Each admission round re-runs the engine on the affected pins only
5. Measurement by the trace runner. Each admission round re-runs the engine on the affected pins only
(the runner's resume and selection already support this; on k3s the worker Deployment runs them in
parallel). The outcome is the pair-asymmetry table per family, before and after, on held-out folds.
6. **Gate before production.** An admitted fact enters `src/openultrasast/ruleset/semantic/*.toml` only after
6. Gate before production. An admitted fact enters `src/openultrasast/ruleset/semantic/*.toml` only after
the detection gates (`gate`, `map_gate`, `pair_gate`) show no regression, and only as a registered
experiment whose decision rule was fixed before the run.
7. **Coverage second.** After discharge facts, the same loop proposes source and sink additions from pairs
7. Coverage second. After discharge facts, the same loop proposes source and sink additions from pairs
where neither side has a path (91 of 105 today), admitted under the same held-out rule.
## How it relates to what exists
@@ -9,13 +9,13 @@ Maintainer direction and context: `brief.md`.
## Boundary Context
- **In scope**: a feature record per candidate built from every instrument and the memory store; a labelled
- In scope: a feature record per candidate built from every instrument and the memory store; a labelled
dataset with provenance; training, calibration and versioning of the decision engine; A/B experiments as plane
Runs; learning curves and leave-one-population-out estimates; the engine's use in `ousast scan`, `ousast
pre-push` and plane Runs; a final one-time check on frozen population v3.
- **Out of scope**: new instruments (they plug in as signal producers later); deep or large models; any change to
- Out of scope: new instruments (they plug in as signal producers later); deep or large models; any change to
population v3 or its protocol; publishing results (licences still gate that).
- **Adjacent expectations**: instruments keep working standalone; the memory store (harnessx-removal Req 6) is
- Adjacent expectations: instruments keep working standalone; the memory store (harnessx-removal Req 6) is
the system of record; the plane attributes every experiment's token spend per task.
## Requirements
@@ -121,7 +121,7 @@ rule hits.
reported unless the engine's decision changes.
4. *(Amended 2026-10-03, maintainer decision; refines the BLOCK rule of 7.3 and 7.4. Tested by exp-004.)* BLOCK
precision is demonstrated pooled across families, not per family:
- **Normalisation**, declared before any run: each family's raw score is put on one scale by a per-family Platt
- Normalisation, declared before any run: each family's raw score is put on one scale by a per-family Platt
map (`learn.calibrate.fit_map` with its floors) fitted on training folds only. A family the training folds
cannot map does not block on that fold.
- **t_block**: one threshold on the normalised score for all families, chosen on training folds as the lowest
@@ -132,6 +132,13 @@ rule hits.
- **t_quiet**: per family, the highest threshold keeping >= 90% of the paired positives at or above it (chosen
on training folds, its held-out recall reported); findings below it are silent.
- No held-out label moves a threshold or a normalisation map; candidates whose verdict is `unsure` never block.
5. *(Amended 2026-10-05, maintainer decision, with `search-with-proof`.)* Where a family has an owned demonstration
oracle, the engine is **triage, not the gate**: BLOCK comes only from a verified demonstration (`search-with-proof`
Req 3, 6.3), and the engine's job is to put the true vulnerable function among the top K candidates of a push.
For those families the engine is judged by (a) top-K recall per push (K = 3 to start) on the unseen-repo pool and
(b) its advisory false-alarm rate there; the pooled block gate of 6.4 then governs only families without an
oracle (access control, deserialisation, configuration and secrets), where the engine remains the only signal.
Every arm reports top-K recall next to its catch and false-alarm rates.
### Requirement 7: Built for unseen repositories -- a repo safety net
@@ -254,3 +254,13 @@ rate per push. Within-pair AUC stays a development signal. Tasks 11-14 below fol
- Search input fields, retrieval parameters, k, instruction and demonstrations together (DSPy MIPRO-style) on
training folds; no hand-set input profile. Budget go-ahead before paid search.
- _Requirements: 3, 6, 7_
- [ ] 17. Triage objective (Req 6.5, amended 2026-10-05 with `search-with-proof`)
- [ ] 17.1 Metric: top-K recall per push (K = 1, 3, 5) of the true vulnerable function among changed functions,
on held-out folds and on the unseen-repo pool; added to the pool scorer next to catch and false alarm.
- [ ] 17.2 Score the existing arms on it ($0 replay): trained logistic (cheapest per candidate, likely default),
LLM programs (exp-005 A), engine-evidence ordering, and an averaged ensemble.
- [ ] 17.3 Re-scope 14 (pooled block gate) to families without a demonstration oracle; 15.4 and 16 report top-K
recall as their primary unseen-pool number for families with an oracle.
- _Requirements: 6.5, 7_
-113
View File
@@ -1,113 +0,0 @@
# Brief: learning-harness
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Problem
The tool is a hand-authored static analyzer with a learning loop bolted on the side, and the loop cannot learn. Three deliberate choices cap it: closed vocabularies (sinks, sources, obligation kinds) that grow only by writing a new spec; a corpus that is the only teacher and a shape that is the only lesson; and a ban on retaining anything the model discovers. Results have ceilings by construction, the loop trains on its own test set, and there is no failure analysis feeding the next round. Detection logic is ~16% of the source and the other 84% cannot say whether the 16% works.
Measured 2026-09-06 on the vibe-py holdout (17 vendored pairs), the existing tool hunter (Sonnet 5 through OpenRouter, 4 steps, tools `read_file`, `grep_repo`, `find_refs`, prompted from file-level hotspots, no learning of any kind) against the overlay:
| scorer | published number | class-aware, identical twins excluded (n = 14) |
|---|---|---|
| tool hunter | 0/17 correct, Youden −0.588 | **12/14 correct, Youden +0.857, 0 leaks** |
| overlay (facts + shapes) | 5/17 correct, Youden +0.294 | 5/14 correct, Youden +0.357 |
The published hunter number is a scoring artifact: the pair scorer counts a hunter finding only when the finding text contains the literal CWE id and the prompt never asks for one, so 24 of 25 vulnerable-side findings that sit inside the labeled function and name the labeled bug were thrown away. The 24 "leaks" split into 3 findings on byte-identical twins and 21 findings outside the labeled function on the trap twin, most of them real defects in a deliberately vulnerable repository. Raw runs: `benchmarks/measurements/2026-09-06-vibe-py-holdout-hunter-*.json`.
Three consequences follow.
1. **The model with tools is already the best detector we own, without any learning.** The roadmap thesis ("recall from the model with tools plus proof; precision and silence from static checks") is confirmed, and the harness has been optimizing the wrong component.
2. **Detection, silence and leak are meaningless without a class.** Every scorer, lever and gate uses text tokens (CWE strings, sink names) as a proxy for class; the corpus's own labels are 32 free-text strings across 7 slices, and 130 of 176 vfc rows say `other`.
3. **Improvements without a class dimension oscillate.** A change that helps one class and hurts another reads as no change on an aggregate Youden; the loop keeps or reverts it for the wrong reason and the system degrades in ways nobody predicted. This is the maintainer's observation and it is what the corpus-seeded lever does today.
Repairs already measured and required regardless: the mechanism lever seeds candidates from holdout pairs (holdout Youden +0.059 with every pair teaching, −0.059 with the train split only); five vibe-py twins are byte-identical; TypeScript has no grammar; absence excerpts carry no registration context; unit fixtures are easier than the corpus rows they stand in for.
## What others learned
Full sourced notes in `research.md` (four threads, 2026-09-06). The findings that change this design:
**Cyber reasoning systems (AIxCC finals, Big Sleep, XBOW, Glasswing).** The routing key that worked in every finalist was mode × language × sanitizer or CWE family, aligned to what the oracle can confirm; nobody routed on fine CWE. Only proof survives: every top system gates a report on an executed oracle, the accuracy multiplier decided the ranking (Atlantis 0.9999 versus 0.9044), and XBOW keys a non-AI validator per vulnerability class that alone decides success. RoboDuck's cheap logprob classifier (~$0.001) sending the top 20% to a $0.50 agent is the closest public analogue of an auto-classifier front end. Ensembles beat selection when the winner is unpredictable, with attribution done per agent after the fact. Validated is not correct: 38–46% of automatically accepted patches were semantically wrong. Hallucination controls are structural (forced tool calls, curated tools instead of bash, symbol resolution). Cross-run learning is nearly absent in the field, and the one trained component reported catastrophic forgetting across projects. Stability and cost decided more than cleverness.
**Self-improving harnesses (GEPA, DSPy, AlphaEvolve, DGM, Self-Harness, SkillOpt, AHE, Meta-Harness).** Track wins per instance or class, never the aggregate: GEPA's per-instance Pareto archive doubled the gain of greedy selection. Accept a change only when train and holdout are both non-negative and one is strictly positive; reject ties. Meta-agents predict the class they help at 33.7% precision and the class they hurt at 11.8% ("regression blindness"), so a cross-class sweep cannot be optional. Two-stage evaluation is where sample efficiency comes from. Prompt bloat is the overfit signature; cap length. Structural edits (tools, checklists, memory) transfer across model families, prose edits regress. Self-diagnosis confabulates (0 of 121 reflections named the real cause; structured trajectory facts reached 86%). Hide the checker and the gold labels from the editable surface. One change, one class per round. Harness-edit quality is flat across model tiers while detector quality is not: a cheap meta-agent, spend on the detector.
**Datasets and taxonomies.** Deduplicate by normalized hash before splitting and split by repository, chronologically. Report pair-wise outcomes (pair-correct, both-vulnerable, both-benign, reversed) and a directional-bias index, not F1; models that flag both sides win F1 while losing pair accuracy. Match on location and class with hierarchical partial credit and the maximum penalty for a hallucinated class. Fine CWE classification from code is unsolved (top-1 ≤ 14.7%, most errors lateral siblings); coarse families are workable (88 CWEs collapsed to 12 gave per-class experts +12.8% F1, with a router right 63.8% of the time and F1 0.18 when wrong). Specialists collapse without hard negatives. Verdicts flip on renames and whitespace; single-shot evaluation missed 42% of real changes, so evaluate with K ≥ 3 runs and a reliable-change test. Function-level pairs lose context, the same defect as our context-free absence excerpts.
**Routing and regression control.** Learned routers plateau 10–20 points below an oracle and can lose to random; measure router accuracy on its own against random and oracle. Route to several experts with an explicit `unknown`, never argmax. Slice everything; aggregate gains of +2 points have masked 28–39% of items getting worse. Freeze per-class configurations to prevent interference; skill loops driven by self-feedback alone drift, external feedback does not. Production gates are conjunctive per slice with relative thresholds against the last blessed baseline; no public numeric negative-flip budget exists, so the defensible budget is the measured seed-noise floor per class.
## Desired Outcome
A harness that improves a **set of classified detectors** round by round, where every change is attributed to the class it was meant to move, every unpredicted regression in another class counts against it, only what a verifier can confirm is reported above suspicion, and nothing is published without its unscorable denominator and its noise floor.
- **Auto-classifier as routing key.** A closed, versioned taxonomy of roughly 8–12 families derived from the CWE hierarchy and aligned to what our verifiers can confirm (sandbox recipe, static corroboration, entry-point and flow tools). Multi-label with an explicit `unknown`. Three tiers: deterministic static signals first, a cheap model second with logprob averaging, `unknown` third, each answer recording its tier. Measured on its own: agreement with maintainer labels on reviewed pairs with hierarchical partial credit, accuracy against a random router and an oracle, confusion matrix per round. Disagreements are the review queue.
- **Per-class detectors, frozen between rounds.** One hunter configuration per family: length-capped prompt, skills, curated tools with symbol resolution (never bash), step and cost budget, evidence checklist, hard negatives, known counterexamples. A region routes to every family the classifier admits plus a generalist. Round zero clones the current hunter into every family so the baseline is controlled; K ≥ 3 runs per class establish the seed-noise floor that becomes that class's regression budget.
- **Class-aware, pair-wise scorer.** Detection = a finding in the labeled function whose class matches the label, with hierarchical credit and the maximum penalty for a hallucinated class. Silence = no finding of the labeled class in the labeled function on the fixed side. Outcomes as pair-correct, both-vulnerable, both-benign, reversed, plus a directional-bias index, negative flips per item against the last blessed round, and a fixed-FPR view. Unscorable rows (identical twin, unsupported language, unresolved label, missing context) listed by reason and removed from the denominator, never deleted. Dedup by normalized hash before splitting; split by repository, chronologically.
- **Only proof is reported above suspicion.** Class-keyed verifiers decide: sandbox recipes, static corroboration, entry-point, flow and obligation tools. A hunter claim without a verifier stays `suspicion` and is scored as such. Verifier code and gold labels sit outside every editable surface, and anything reported as proven gets a second independent judge.
- **Learning unit = structural change, journaled.** A round's output is a tool, a checklist, a counterexample, a memory entry or a bounded prompt edit for one class, drafted from structured failure facts (which sink, which line, which check fired), never from free-form self-critique. Accepted only when train and holdout are both non-negative with one strictly positive and no other class falls below its budget; ties rejected; rejected edits kept in a buffer the proposer sees; the proposer never sees the holdout. HarnessX's `MetaAgent.evolve` and journal (`hypothesis_id`, `levers`, `predicted_affected`, precision discounted by `regressed_unpredicted`) is the round primitive; the tool supplies tasks keyed by class, trajectories, the evaluator and the routing.
- **Honest fitness.** Split enforced by construction; two-stage evaluation (minibatch gate, the class's full holdout, the cross-class sweep); a per-class archive of which candidate wins which instance rather than one champion; the roadmap rewritten from one command with the unscorable denominator and the noise floor beside every number; a per-round cost cap where an over-budget round is reverted and recorded.
Principle for the roadmap: **classify first, verify before reporting, improve one class at a time under a fitness that punishes what it did not predict.**
## Approach
1. **Class-aware pair scorer and unscorable rows.** Pair-wise outcome vocabulary, location-and-class matching with hierarchical credit, directional-bias index, per-item negative flips, K-run reliable-change test, unscorable reasons out of the denominator, normalized-hash dedup, repository-level split. Re-publish every slice from one command. Smallest change, biggest number.
2. **Taxonomy and auto-classifier.** Family taxonomy versioned in one file with its CWE mapping and its verifier per family; three-tier classifier; its own measurement report; the vfc slice's 130 `other` rows and the 32 free-text classes are its first job, disagreements queued for review.
3. **Per-class hunter configurations.** Clone the current hunter into every family; curated tools with symbol resolution; length-capped prompts; per-family evidence checklist and hard negatives; the classifier routes a region to every admitted family plus the generalist; findings carry their class, detector and verifier.
4. **Verifiers keyed by class.** Sandbox recipes, static corroboration, entry-point, flow and obligation tools exposed to the hunter and to the scorer; a claim rises above `suspicion` only through a verifier; a second independent judge for anything proven.
5. **Split enforcement and lever refusal.** Every exporter and lever takes the train split by construction and refuses candidates touching a holdout pair; re-measure `corpus-seeded-mechanisms` and rewrite its roadmap row.
6. **Evolve loop on HarnessX.** Tasks = classified pairs (`RLTask.task_type` = family); trajectories from journaled hunter runs; evaluator = the class-aware scorer; one structural change for one class per round with `predicted_affected` set; the acceptance rule and cross-class sweep above; per-class Pareto archive; rejected-edit buffer; reflector blind to the holdout; per-round cost cap; the journal's attribution table is the round report and the daily benchmark.
7. **Corpus repairs the loop needs.** TypeScript grammar in the semantic extra; absence rows re-harvested with `handler_context`; identical twins marked `known_limit:identical_twin`; a fixture-difficulty check that fails when a unit fixture is strictly easier than the corpus rows its test claims to cover; a post-cutoff slice for contamination.
8. **Operational floor.** Persisted logs and trajectories, budget caps tested on the failure path, redaction on every prompt and journaled trajectory, cost per point published per round.
## Scope
- **In**: items 1–8; roadmap rewrite from one command; a closure decision on `authorization-obligations` 4.3, 5 and 6.
- **Out**: new static shape families; hand-written facts beyond what a round proposes and validates; changing the evidence ladder or dispositions; any merge gate on non-local slices; vendoring new pairs beyond re-harvesting existing recipes; fine-tuning model weights.
## Boundary Candidates
- Taxonomy and classifier (new, owns class assignment and its measurement) vs pair labels (maintainer-owned reference)
- Per-class detector configurations (new, evolved, frozen between rounds) vs static analyzers (unchanged internally, exposed as tools and verifiers)
- Class-aware scorer (replaces the text-token rule) vs catalog schema (one additive `unscorable` reason, one `class` field)
- Evolve loop (new, HarnessX) vs existing bounded levers (rules, policy, mechanisms; kept, subordinated to split, class and budget)
## Out of Boundary
- Taint semantics, walker, dominance internals (`guard-dominance-regime`, `overlay-ir-completeness`)
- Call graph and path records (`reachability-flow-model`), except as a future hunter tool
- Obligation checker internals (`authorization-obligations`); its corpus tasks move here, its lever waits for the split
## Upstream / Downstream
- **Upstream**: `pair-corpus-honesty` (labels, tiers, splits, `known_limit`), `corpus-seeded-mechanisms` (exporter, LOO, lever), `tool_hunter` and `provider/openrouter`, `harness_ext`, HarnessX 0.1.0 (`meta_harness`, `rl`, `tracing.journal`), `tree-sitter-overlay-extra`, the sandbox and regress recipes.
- **Downstream**: every roadmap number; `harnessx-self-improving-rulesets` (its evolve step is this spec); `authorization-obligations` 4.3/5/6; scan reports (findings carry class, detector and verifier).
## Existing Spec Touchpoints
- **Extends**: `pair-corpus-honesty` Req 1 (scorer) and Req 9/10 (row classification); `corpus-seeded-mechanisms` Req 4/5 (split by construction); `three-stage-scan` MAP (per-class hunters as proposers); `propose-adjudicate-prove` (verifiers keyed by class); `harnessx-self-improving-rulesets` (the evolve loop lands here).
- **Folds in**: the retired `closed-loop-integrity` brief.
- **Re-homes**: `authorization-obligations` 4.3 and the corpus half of 6.1; holds 5.
## Constraints
Core `dependencies = []`; HarnessX and grammars stay optional extras; every loop step degrades to a recorded reason without them. No row deleted; unscorable rows keep a reason. Labels stay maintainer-owned: the classifier proposes, disagreements queue for review, and a class change on a reviewed pair is a reviewed edit. Model-authored skills, tools and prompt edits are retained only after held-out validation under the acceptance rule and are journaled with attribution; nothing a round proposes reaches a scan without its class holdout and the cross-class sweep. Split enforced by construction. Verifier code and gold labels outside every editable surface. Prompt length capped per class. Gates (`gate`, `map_gate`, `pair_gate`) stay byte-identical. Redaction on every prompt and trajectory. Per-round cost cap with revert.
## What this project is for (maintainer, 2026-09-06)
The target is to catch the taxonomy families below **reliably** on real-world application code, including absence bugs in vibe-coded services (an authorization check that was never written), with an emphasis on dangerous, low-hanging fruit. It is not a bug-finder for JavaScript interpreters, WebGL renderers or browser engines, and it is not a memory-safety fuzzer: memory bugs in C and C++ are a different game played by specialized systems (AIxCC-class CRSs, Big Sleep) that this project learns from and does not replicate or compete with. The balance to hold is between two failure modes, being useless on actual code and being blind to forgotten checks in generated code; both are in scope, engine and renderer bugs are not. The vfc C slice therefore stays a measured, non-gating slice with one coarse memory-safety family and no crash oracle of our own.
## Decisions (maintainer, 2026-09-06)
1. **Taxonomy spine.** Ten families keyed to danger and to a verifier we can run, with CWE and the mechanism vocabulary as attributes, never the routing key: injection into a query or command (SQL, command, code, template); file path from user input; deserialization of untrusted data; access control (IDOR, missing guard, identity from request body; the obligations checker is its verifier); output encoding (XSS, template to HTML); untrusted destination (server-side request forgery and open redirect, which share the shape of untrusted input choosing where a request goes and account for 34 corpus rows); prototype pollution and unsafe object merge; permissive configuration and secrets (CORS, debug, cookie flags, weak crypto, hardcoded credentials); memory safety as one coarse family for the vfc slice only; unknown. Multi-label routing.
2. **Regression budget.** Measured, not chosen: round zero runs the cloned hunter K = 5 times per family at temperature 0 over the vendored corpus; the per-family negative-flip rate between seeds is the budget. A round that exceeds it in any family is rejected regardless of its target gain.
3. **Models.** Detectors and meta-agent on `deepseek-v4-flash` through DeepSeek's own OpenAI-compatible endpoint (`https://api.deepseek.com`); `deepseek-v4-pro` only as the second independent judge above suspicion and for classifier disagreements queued for review; embeddings stay on OpenRouter. The chat endpoint and the embedding endpoint are configured separately (today one base-URL variable serves both). The 1M context lets the detector read whole files instead of hotspot excerpts.
4. **Round-zero verifiers.** Snippet templates with an oracle per family, on the existing per-language sandbox recipes: canary row in an in-memory database for injection, canary file for command execution and traversal, canary object for deserialization. Access control gets the obligations checker as static corroboration. Output encoding, SSRF, prototype pollution and configuration report at `suspicion` only until they have an oracle. A family without a verifier never reports above suspicion in a scan but is still scored class-aware on the corpus: the corpus is the verifier for learning, the sandbox is the verifier for reporting.
5. **Obligations lever.** `authorization-obligations` task 5 is retired as a spec task and becomes the access-control family's first evolve round; obligation shapes are that family's memory entries under the split rule.
6. **Order.** Round zero on the web families over vibe-py and agent-vfc, where real-world code, dangerous fruit and a working hunter coincide; the vfc C slice later, as a measured slice only.
## Evidence
- `benchmarks/measurements/2026-09-06-vibe-py-holdout-hunter-sonnet5.json`: `ousast pairs --slice vibe-py --split holdout --hunter --hunter-model anthropic/claude-sonnet-5 --json`.
- `benchmarks/measurements/2026-09-06-vibe-py-holdout-hunter-findings.json`: every hunter finding per side with `in_labeled_function` and the scorer's verdict; the class-aware re-score used a keyword classifier written for the measurement only, which is why approach item 2 exists.
- `research.md`: the four research threads with sources.
-651
View File
@@ -1,651 +0,0 @@
# Design Document: learning-harness
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Overview
**Purpose**: Turn OpenUltraSAST from a hand-authored analyzer with a leaking improvement loop into a learning harness: an auto-classifier routes every pair, finding and code region to one of ten verifier-aligned families; one detector configuration per family is evolved one change per round from structured failure facts; a class-aware pair-wise scorer with unscorable denominators and a measured per-family noise floor decides acceptance; only verifier-confirmed claims are reported above suspicion; the split is enforced by construction.
**Users**: the maintainer runs round zero, evolve rounds and the publish command; operators read scan findings that now carry a family, a detector version and a verifier outcome; reviewers work the classifier disagreement queue.
**Impact**: the tool hunter becomes the primary detector and the static machinery becomes tools and verifiers. The pair scorer's hunter path, the exporter, leave-one-out, the mechanism lever and the improve journal change behavior; findings, catalogs and the manifest gain additive fields. Gates stay byte-identical.
### Goals
- One trustworthy number per family per slice, with its denominator, taxonomy version, noise floor and cost.
- A round can only be accepted when it helps its target family on held-out pairs and hurts no other family beyond its measured budget.
- Nothing above `suspicion` without a verifier; verifier code and gold labels outside every editable surface.
- Round zero on the web families over vibe-py and agent-vfc before any learning.
### Non-Goals
- Browser-engine, interpreter or renderer bugs; crash oracles for C and C++ (the memory-safety family is measured, never gating).
- New static shape families, new hand-written facts, evidence-ladder or disposition changes, merge gates on non-local slices, model fine-tuning.
- A general router framework or a generic agent platform; the classifier is a function and a detector configuration is a directory.
## Boundary Commitments
### This Spec Owns
- The family taxonomy file, its loader and its version (1.x).
- Classification of pairs, findings and regions, and the classifier's measurement report and review queue (2.x).
- Class-aware pair scoring: outcomes, directional bias, fixed-FPR view, K-run reliable-change test, negative flips (3.x).
- Unscorable accounting and its reasons; the additive `unscorable` and `family` fields on catalog rows (4.x).
- The single teacher-selection rule every learning path calls, and holdout refusal (5.x).
- Per-family detector configurations, their loader, the family detector runner and finding tags (6.1–6.5); the chat-endpoint resolution independent of embeddings (6.6).
- The verifier registry keyed by family, canary oracles, the second-judge step (7.x).
- Round zero, the noise floor, evolve rounds, acceptance, attribution, the archive, the round journal, cost caps (8.x, 9.x).
- The corpus repairs listed in 10.x and the publish command with committed artifacts (11.x).
### Out of Boundary
- Pair labels, tiers, splits and `known_limit` semantics beyond the two additive fields (`pair-corpus-honesty`).
- Facts, walker, dominance, obligations checker internals; the mechanism store schema; the rule and policy levers' validators.
- The sandbox runner, snippet safety rules and Docker images; this spec adds an oracle to the verdict, not a runner.
- HarnessX internals; this spec composes `MetaAgent.evolve` and the journal behind `harness_ext`.
- Harvesting new pairs; only re-harvesting existing absence recipes.
### Allowed Dependencies
- `pairs`, `benchmark`, `semantic.loo`, `semantic.seed`, `semantic.mechanisms`, `improve.evolve`, `improve.validator`, `tool_hunter`, `hunter_tools`, `provider.openrouter`, `regress.*`, `sandbox.*`, `redaction`, `harness_ext`, `config`.
- Optional extras only through `harness_ext` (HarnessX) and `semantic.extra` (tree-sitter, now including TypeScript); every step degrades to a recorded reason without them.
- Direction: `learning.families` → `learning.classify` → `learning.scoring` → `learning.split` → `learning.detectors` and `learning.verifiers` → `learning.rounds` → `learning.publish` and `cli`. `learning.*` imports the packages above; nothing outside `learning` imports `learning.rounds` or `learning.publish`. `pairs` and `benchmark` never import `learning`.
### Revalidation Triggers
- A change to the family list, a family's CWE set or its verifier (taxonomy version bump).
- A change to the pair outcome vocabulary or the reliable-change test.
- A change to the acceptance rule, the regression-budget definition or the journal schema.
- A change to the chat-endpoint resolution order or the tool-hunter finding shape.
- A change to the sandbox verdict contract (oracle callback) or snippet safety rules.
## Architecture
### Existing Architecture Analysis
- `tool_hunter.run_tool_hunter` is a 4-step loop over three curated tools with findings parsed into `StaticFinding` at `suspicion` (`finding_id = tool-hunter:<path>:<line>`, tag `tool-hunter`). It refuses to emit findings when no tool was called. It has no temperature, budget or context knobs beyond `max_steps`.
- `pairs._hunter_matches_expected` keys detection on text tokens and `_evaluate_hunter_pair` counts every fixed-side finding as a leak under `fix_policy = silent`; both are replaced on the hunter path only. The overlay and inventory paths and `pair_gate` are untouched.
- `evaluate_profiles`, `evaluate_mechanism_profiles`, `propose_mechanism_edits`, `evaluate_loo` and `build_pair_signals` filter by tier and vendored status but not by split; `export_mechanisms` seeds from every case it is given.
- `OpenRouterChatClient` and `OpenRouterEmbeddingClient` read the same `OPENROUTER_BASE_URL`; `complete_chat` always sends `temperature: 0` and drops `reasoning_content` from replies. DeepSeek requires thinking disabled for temperature to apply, `reasoning_content` replayed on tool-call turns, no `/v1` suffix, `json_object` only, no seed.
- `regress.candidate` derives a verdict from the exit code; snippets are compile-only templates or the model's `proposed_snippet`; `check_snippet_safety` bans sockets, curl and workspace writes, which a canary file in `/scratch` respects.
- `MechanismStore` is an append-only JSONL with tombstones and deterministic ids, the pattern reused for the archive and the journal.
- HarnessX 0.1.0 provides `MetaAgent.evolve(current_config, trajectories_dir, output_dir)` with post-flight checks, `JournalEntrySpec(round, label, hypothesis_id, levers, predicted_affected, prose)`, `build_context` with precision discounted by unpredicted regressions, and `RLTask.task_type` routing.
### Architecture Pattern & Boundary Map
```mermaid
graph TB
subgraph Corpus
Catalog[pair catalogs]
Loader[pairs loader with unscorable and family fields]
end
subgraph Learning
Families[families taxonomy]
Classify[classifier three tiers]
Scoring[class aware pair scorer]
Split[teachers and holdout refusal]
Detectors[family detector configs and runner]
Verifiers[verifier registry canary static none]
Rounds[round zero and evolve rounds]
Journal[round journal and archive]
Publish[publish command]
end
subgraph Existing
Hunter[tool hunter loop]
Chat[chat endpoint client]
Regress[regress verdicts and sandbox]
Levers[exporter loo mechanism lever]
Meta[HarnessX MetaAgent optional]
end
Catalog --> Loader --> Scoring
Families --> Classify --> Scoring
Families --> Detectors
Families --> Verifiers
Classify --> Detectors
Split --> Levers
Split --> Rounds
Detectors --> Hunter --> Chat
Verifiers --> Regress
Scoring --> Rounds
Detectors --> Rounds
Verifiers --> Rounds
Rounds --> Journal
Rounds --> Meta
Journal --> Publish
Scoring --> Publish
```
**Architecture Integration**:
- Selected pattern: a `learning` package layered left to right (families → classify → scoring → split → detectors/verifiers → rounds → publish), composing existing seams rather than replacing them.
- Domain boundaries: the corpus owns labels and rows; `learning` owns class assignment, scoring, teaching rules, detector configurations, verifier keys and rounds; `regress` owns execution; `harness_ext` owns the HarnessX seam.
- Existing patterns preserved: append-only JSONL with tombstones for the archive and journal; `HarnessRuntime.run_stage` and degradations for every step; `ScriptedChatClient` for offline tests; `FakeSandboxRunner` for verifier tests.
- New components rationale: each is one requirement area with one owner; the classifier and scorer are functions, the detector configuration is a directory, the journal is a file.
- Steering compliance: core `dependencies = []`; recall first under a false-positive ceiling, now per family; gates byte-identical; no model-authored artifact retained without held-out validation.
### Technology Stack
| Layer | Choice / Version | Role in Feature | Notes |
|-------|------------------|-----------------|-------|
| Chat endpoint | DeepSeek platform API, OpenAI-compatible, `https://api.deepseek.com` (no `/v1`); models `deepseek-v4-flash` (default), `deepseek-v4-pro` (second judge, review queue) | Detectors, classifier model tier, proposer, judge | Thinking on by default: the client sends `thinking: {"type": "disabled"}` for detectors and the classifier so `temperature` applies; `reasoning_content` replayed on tool-call turns; `response_format json_object` with empty-content retry; `logprobs` requested by the classifier tier only. No seed: determinism is best-effort and absorbed by the noise floor. Pricing (peak, USD/1M): flash 0.44 in / 1.32 out, cache hit 0.014; pro 1.32 / 3.96. Concurrency 2500 (flash). |
| Embedding endpoint | OpenRouter, `openai/text-embedding-3-small`, unchanged | Prove-order retrieval | Reads `OPENROUTER_*` only; DeepSeek has no embeddings endpoint. |
| Evolve loop | HarnessX 0.1.0 (`meta_harness.agent.MetaAgent`, `meta_harness.journal`), optional extra | Proposal generation and attribution context | Behind `harness_ext.require_harnessx`; a scripted proposer stands in without it. |
| Parsing | tree-sitter extra + `tree-sitter-typescript>=0.23` | TypeScript rows become scorable | Optional extra only. |
| Sandbox | existing Docker CLI runner, images `python:3.12-alpine`, `node:22-alpine` | Canary verifiers | Oracle callback added to the verdict; safety rules unchanged. |
| Storage | JSONL under `.openultrasast/learning/` and `benchmarks/measurements/` | Journal, archive, noise floors, published artifacts | Append-only, committed when published. |
## File Structure Plan
### Directory Structure
```
src/openultrasast/learning/
├── __init__.py # public surface: load_families, classify_*, score_pair_family, teachers, run_baseline, run_learning_round, publish
├── families.py # Family, FamilyTaxonomy, load_families(ruleset/families.toml), version, verifier kind per family
├── classify.py # Classification, classify_pair/finding/region (tiers), measure_classifier, review queue entries
├── scoring.py # FamilyOutcome, score_pair_family, FamilyMetrics, directional_bias, negative_flips, reliable_change, fixed_fpr_view, unscorable_reason
├── split.py # teachers(cases), refuse_if_holdout(candidate_pairs, cases) -> Refusal | None
├── detectors.py # FamilyConfig, load_family_configs, run_family_detector (whole-file context, curated tools), finding tags
├── endpoint.py # ChatEndpoint, resolve_chat_endpoint (DeepSeek first, OpenRouter fallback, scripted), DeepSeekChatClient adapter
├── verifiers.py # Verifier protocol, CanaryVerifier, StaticVerifier, NoVerifier, verifier_for(family), second_judge
├── rounds.py # run_baseline (round zero, noise floor), run_learning_round (propose, minibatch, holdout, sweep, accept/revert), Attribution
├── proposer.py # Proposer protocol, ScriptedProposer, HarnessXProposer (MetaAgent.evolve behind harness_ext), failure facts, rejected buffer
├── journal.py # RoundRecord, LearningJournal (append-only JSONL), Archive (family -> winning config per pair)
└── publish.py # publish(): regenerate numbers from one command, write roadmap table and measurement artifacts
src/openultrasast/ruleset/families.toml # closed, versioned taxonomy: id, cwes, mechanisms, verifier, description
src/openultrasast/learning/configs/<family>/ # round-zero detector configuration per family (prompt.md, checklist.md, tools.toml, budget.toml, hard_negatives.jsonl, counterexamples.jsonl)
tests/test_learning_families.py, test_learning_classify.py, test_learning_scoring.py, test_learning_split.py, test_learning_detectors.py, test_learning_endpoint.py, test_learning_verifiers.py, test_learning_rounds.py, test_learning_journal.py, test_learning_publish.py, test_fixture_difficulty.py
```
### Modified Files
- `src/openultrasast/pairs.py` — `PairCase.unscorable: str | None` computed at load (identical twin via normalized body hash, `known_limit` passthrough); duplicate report across pairs before split; `split_by_repository` helper honoring existing `split` fields and reporting straddles; `_evaluate_hunter_pair` delegates matching and leaks to `learning.scoring` through an injected callable so `pairs` does not import `learning`; `build_pair_signals` gains a `split` filter.
- `src/openultrasast/benchmark.py` — `ExpectedFinding.family: str | None = None` (additive, stored as an opaque string; the Classifier validates loaded labels against the taxonomy so `pairs` and `benchmark` never import `learning`).
- `src/openultrasast/tool_hunter.py` — `run_tool_hunter` gains `system_prompt`, `context_files`, `tools`, `max_chars` and `tags` parameters with today's values as defaults; finding parsing accepts an optional `family` field; the reply loop replays `reasoning_content`.
- `src/openultrasast/hunter_tools.py` — four new tools beside `read_file`, `grep_repo`, `find_refs`: `read_definition(symbol)`, `entry_points(path)`, `flows(path, function)`, `obligations(path, function)`, each clamped to the repository and returning identifiers and line spans only; `HUNTER_TOOLS` schemas extended.
- `src/openultrasast/provider/openrouter.py` — `OpenRouterChatClient` gains `extra_body` (thinking, response_format, logprobs) and retains the `reasoning_content` field on assistant messages; replaying it across tool turns is the hunter loop's job (`tool_hunter`), not the client's. No behavior change for existing callers.
- `src/openultrasast/config.py` — `[models]` gains `chat_base_url`, `chat_api_key_env`, `judge`; `[learning]` section (`families_path`, `configs_dir`, `k_runs = 5`, `round_cost_cap_usd`, `minibatch`, `cutoff_date`); `load_dotenv` unchanged, tests inject endpoints explicitly.
- `src/openultrasast/semantic/seed.py`, `semantic/loo.py`, `improve/evolve.py` — teacher selection through `learning.split.teachers`; refusals recorded; `evaluate_loo` teaches from train only.
- `src/openultrasast/regress/candidate.py`, `regress/verdict.py` — `run_regression` accepts an optional `oracle: Callable[[SandboxResult], bool]`; `verdict_from_result` uses the oracle when given, exit code otherwise.
- `src/openultrasast/cli.py` — `ousast learning classify|score|baseline|round|publish`; `pairs --hunter` uses the family scorer and `--k-runs`; `[models].hunter` resolution through `learning.endpoint`.
- `pyproject.toml` — `tree-sitter-typescript>=0.23` in the semantic extra; `src/openultrasast/semantic/extra.py` maps `typescript`/`tsx`.
- `benchmarks/pairs/vibe-py/catalog.toml` and `recipes.toml` — the five identical twins gain `known_limit = "identical_twin"`; absence rows gain `family` and `mode = "handler_context"` (re-harvest is a maintainer step).
- **Cross-file registration context (amended during task 4.1).** Some frameworks keep the route table out of the handler module: connexion registers by `operationId` in an OpenAPI document, an Express application registers in a router file. A single-file excerpt of such a handler can never contain its registration, so Req 10.2 is unreachable for those rows without a second file. A recipe may therefore name `context = ["<repo-relative path>"]`; `benchmarks/pairs/harvest.py` fetches each document at the same two commits, extracts only the stanza that names the handler (`extract_registration_context`), and writes it under `<name>/context/<side>/<relpath>` with its own provenance header and the same redaction. The catalog carries `[[pair.context]]` rows, `PairCase.context_files` holds `(relpath, vuln, fixed)`, and `_materialize_side` copies them beside the excerpt at their repository-relative paths so the entry-point mapper sees the same layout the project has. `src/openultrasast/mapping.py` gains `_route_manifest_entry_points`, which reads `operationId` and the operation's `security` block out of a YAML or JSON document and attributes the record to the module the operation id points at. Trap pairs whose fixed side is a different, correctly guarded handler carry `fix_function` so the fixed side gets *its* registration rather than the vulnerable side's.
- `benchmarks/pairs/mechanisms.toml` — unchanged; families reference mechanism ids.
- `.kiro/steering/roadmap.md` — rewritten by `publish` from artifacts.
## System Flows
### Class-aware scoring of one pair (hunter path)
```mermaid
sequenceDiagram
participant S as Scorer
participant C as Classifier
participant D as FamilyDetectors
participant V as Verifier
S->>C: classify_pair(case)
C-->>S: families or unknown, tier
loop K runs
S->>D: run on vuln side and fixed side
D-->>S: findings with family tags
end
S->>S: detection = family match inside labeled function
S->>S: silence = no labeled family inside function on fixed side
S->>V: verify claims that a verifier exists for
V-->>S: suspicion, static corroboration, or proven
S-->>S: FamilyOutcome, negative flips, reliable change
```
Decisions: unscorable pairs short-circuit before any run; K runs share one classification; verifier outcomes never change detection or silence, they change the reported rung.
### One evolve round
```mermaid
flowchart TB
Start[failure facts for target family] --> Propose[proposer drafts one structural change]
Propose --> Refuse{touches holdout or exceeds length cap}
Refuse -->|yes| Reject[reject and record]
Refuse -->|no| Mini[train minibatch of target family]
Mini --> MiniOk{no decrease}
MiniOk -->|no| Reject
MiniOk -->|yes| Hold[target family full holdout]
Hold --> HoldOk{train and holdout non negative and one strictly positive}
HoldOk -->|no| Reject
HoldOk -->|yes| Sweep[every other family holdout]
Sweep --> SweepOk{negative flips within each budget}
SweepOk -->|no| Reject
SweepOk -->|yes| Accept[accept freeze config bump version]
Accept --> Journal[journal hypothesis predicted attribution cost]
Reject --> Journal
Journal --> Buffer[rejected edit buffer updated]
```
Decisions: the proposer never sees holdout inputs, outputs or scores; cost is metered per stage and the round is reverted when the cap is crossed at any stage; ties reject.
## Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|---|---|---|---|---|
| 1.1–1.4 | closed taxonomy, verifier per family, version, attributes not keys | Families | `load_families`, `FamilyTaxonomy` | — |
| 2.1–2.5 | three-tier multi-label classifier, measured, review queue, whole corpus | Classifier | `classify_*`, `measure_classifier` | scoring |
| 3.1–3.6 | family-in-function detection and silence, outcomes, bias, FPR, K runs, no text tokens | Scorer | `score_pair_family`, `FamilyMetrics` | scoring |
| 4.1–4.5 | unscorable reasons, never delete, dedup, repository split, denominators shown | Loader (pairs), Scorer, Publish | `PairCase.unscorable`, `unscorable_reason`, `split_by_repository` | — |
| 5.1–5.4 | train-only teachers, refusal, re-measure lever, proposer blind | Split, Levers, Proposer | `teachers`, `refuse_if_holdout` | round |
| 6.1–6.6 | per-family config, frozen, routing plus generalist, whole file, curated tools, independent chat endpoint | Detectors, Endpoint | `FamilyConfig`, `run_family_detector`, `resolve_chat_endpoint` | scoring |
| 7.1–7.6 | suspicion unless verified, canary oracles, static corroboration, none, second judge, sealed surface | Verifiers, Regress | `Verifier`, `verifier_for`, `second_judge`, `run_regression(oracle=)` | scoring |
| 8.1–8.4 | clone, K = 5 noise floor, publish round zero, web families first | Rounds | `run_baseline`, `NoiseFloor` | — |
| 9.1–9.9 | failure facts, one change, journal, staged evaluation, acceptance, attribution, archive, cost cap, retention | Rounds, Proposer, Journal | `run_learning_round`, `Proposer`, `RoundRecord`, `Archive` | round |
| 10.1–10.5 | TypeScript, re-harvest, identical twins, fixture difficulty, post-cutoff slice | Extra, Corpus, Tests | `semantic.extra`, catalog fields, `test_fixture_difficulty` | — |
| 11.1–11.5 | one command, artifacts committed, trajectories persisted, degrade with reason, gates identical | Publish, Journal, Runtime | `publish`, `LearningJournal` | — |
## Components and Interfaces
| Component | Layer | Intent | Req | Key Dependencies | Contracts |
|---|---|---|---|---|---|
| Families | data | closed taxonomy with verifier per family | 1.x | ruleset file (P0) | Service |
| Classifier | logic | assign families with tier, measure itself | 2.x | Families (P0), Endpoint (P1) | Service, State |
| Scorer | logic | class-aware pair outcomes and statistics | 3.x, 4.x | Families (P0); findings from Detectors and rungs from Verifiers are inputs, not imports (P1) | Service |
| Split | logic | the one teacher rule and holdout refusal | 5.x | pairs (P0) | Service |
| Detectors | logic | per-family configuration and runner | 6.1–6.5 | tool_hunter (P0), Endpoint (P0), Classifier (P0) | Service, State |
| Endpoint | integration | independent chat endpoint resolution and DeepSeek adapter | 6.6 | provider.openrouter (P0), config (P0) | Service |
| Verifiers | integration | family-keyed oracles and the second judge | 7.x | regress, sandbox (P0), obligations (P1) | Service |
| Rounds | orchestration | round zero, noise floor, evolve rounds, acceptance | 8.x, 9.x | Scorer, Detectors, Proposer, Journal (P0) | Batch, State |
| Proposer | integration | structural change proposals from failure facts | 9.1–9.2 | harness_ext (P1), Endpoint (P1) | Service |
| Journal | data | round records, attribution, archive | 9.3, 9.6, 9.7, 11.3 | JSONL (P0) | State |
| Publish | orchestration | numbers from one command, artifacts, roadmap | 11.x | Scorer, Journal (P0) | Batch |
### Data layer
#### Families
| Field | Detail |
|---|---|
| Intent | Load and version the closed family taxonomy with its CWE set, mechanism set and verifier kind |
| Requirements | 1.1, 1.2, 1.3, 1.4 |
**Responsibilities & Constraints**
- Exactly ten families as decided; ids are stable identifiers; the file carries `version`.
- Verifier kind per family is one of `canary`, `static`, `none`; memory safety is `none`.
- No component may route or score on a CWE or mechanism id; they are looked up only through a family.
**Dependencies**: Inbound: Classifier, Scorer, Detectors, Verifiers (P0). External: `ruleset/families.toml` (P0).
**Contracts**: Service [x]
```python
@dataclass(frozen=True)
class Family:
id: str # e.g. "injection", "path", "deserialization", "access_control", "output_encoding", "untrusted_destination", "prototype", "config_secrets", "memory", "unknown"
cwes: frozenset[str]
mechanisms: frozenset[str]
verifier: Literal["canary", "static", "none"]
description: str
@dataclass(frozen=True)
class FamilyTaxonomy:
version: str
families: tuple[Family, ...]
def by_id(self, family_id: str) -> Family: ...
def family_of_cwe(self, cwe: str) -> Family | None: ...
def family_of_mechanism(self, mechanism: str) -> Family | None: ...
def related(self, a: str, b: str) -> Literal["same", "parent_child", "lateral", "fabricated"]: ...
def load_families(path: Path | None = None) -> FamilyTaxonomy # raises FamiliesError naming the field on any deviation
```
- Invariant: `unknown` is always present and never has a verifier.
#### Journal
| Field | Detail |
|---|---|
| Intent | Append-only record of rounds with attribution, and the archive of which configuration wins which pair |
| Requirements | 9.3, 9.6, 9.7, 11.3 |
**Contracts**: State [x]
```python
@dataclass(frozen=True)
class RoundRecord:
round: int
family: str
hypothesis: str
levers: tuple[str, ...] # "tool" | "checklist" | "counterexample" | "memory" | "prompt"
predicted_affected: tuple[str, ...]
predicted_at_risk: tuple[str, ...]
outcome: Literal["accepted", "rejected", "reverted_cost", "refused_holdout"]
reason: str
target_train_delta: float
target_holdout_delta: float
sweep: dict[str, int] # family -> negative flips
attribution: Attribution # flipped_predicted, flipped_unpredicted, precision
cost_usd: float
taxonomy_version: str
config_version: str
class LearningJournal: # .openultrasast/learning/journal.jsonl
def append(self, record: RoundRecord) -> None: ...
def rounds(self) -> tuple[RoundRecord, ...]: ...
def rejected_buffer(self, family: str) -> tuple[str, ...]: ... # hypotheses rejected for this family
class Archive: # .openultrasast/learning/archive.jsonl
def record(self, family: str, config_version: str, pair: str, outcome: FamilyOutcome) -> None: ...
def winners(self, family: str) -> dict[str, str]: ... # pair -> config version that scores it best
```
- Persistence: JSONL, one record per line, never rewritten; the HarnessX journal is mirrored when the extra is present, never the source of truth.
### Logic layer
#### Classifier
| Field | Detail |
|---|---|
| Intent | Assign zero or more families with the deciding tier; measure itself; queue disagreements |
| Requirements | 2.1, 2.2, 2.3, 2.4, 2.5 |
**Responsibilities & Constraints**
- Tier 1 (deterministic): rule id, sink family from facts, obligation kind, mechanism id, labeled CWE; any hit decides without a model.
- Tier 2 (model): `json_object` question with the taxonomy definitions, `thinking` disabled, `logprobs` requested; averaged log-probability under a threshold yields `unknown`.
- Tier 3: `unknown`.
- Never writes a label; disagreements go to `.openultrasast/learning/review-queue.jsonl`.
- Validates every loaded catalog `family` label against the taxonomy at classification time and rejects a fabricated id by name; the corpus loader stores the label as an opaque string.
**Dependencies**: Inbound: Scorer, Detectors (P0). Outbound: Families (P0), Endpoint (P1, degrades to tier 1 + unknown without a client).
**Contracts**: Service [x] State [x]
```python
@dataclass(frozen=True)
class Classification:
families: tuple[str, ...] # () means unknown
tier: Literal["static", "model", "none"]
confidence: float | None
def classify_pair(case: PairCase, taxonomy: FamilyTaxonomy, *, client: ChatClient | None) -> Classification
def classify_finding(finding: StaticFinding, taxonomy: FamilyTaxonomy, *, client: ChatClient | None) -> Classification
def classify_region(path: str, text: str, function: str | None, taxonomy: FamilyTaxonomy, *, facts, client: ChatClient | None) -> Classification
@dataclass(frozen=True)
class ClassifierReport:
taxonomy_version: str
agreement: float # hierarchical credit vs maintainer labels on reviewed pairs
confusion: dict[str, dict[str, int]]
random_router: float
oracle_router: float
per_tier: dict[str, int]
unknown: int
queued: int
def measure_classifier(cases: Sequence[PairCase], taxonomy: FamilyTaxonomy, *, client: ChatClient | None) -> ClassifierReport
```
- Postcondition: every reviewed pair whose label disagrees with the classification appears once in the review queue with both answers and the tier.
#### Scorer
| Field | Detail |
|---|---|
| Intent | Score one pair per family from detector findings, aggregate per family and slice, decide reliable change |
| Requirements | 3.1–3.6, 4.1, 4.5 |
**Responsibilities & Constraints**
- Detection: a finding whose `family:` tag relates `same` or `parent_child` to the labeled family and whose line lies in the labeled function; `fabricated` scores the maximum penalty; text is never inspected.
- Silence: no `same`/`parent_child` finding of the labeled family inside the labeled function on the fixed side; other findings are counted in `other_findings`, never as leaks.
- K runs per pair at every evaluation (baseline K = 5, rounds K ≥ 3 as 3.5 requires); reliable change against the last blessed round via a paired test on per-pair outcomes; negative flips per family.
**Contracts**: Service [x]
```python
FamilyOutcome = Literal["pair_correct", "both_flagged", "both_silent", "reversed", "unscorable"]
@dataclass(frozen=True)
class PairFamilyScore:
pair: str
family: str
runs: tuple[FamilyOutcome, ...] # length K
outcome: FamilyOutcome # majority of runs
other_findings_vuln: int
other_findings_fixed: int
rung: Literal["suspicion", "static_corroboration", "proven"]
unscorable_reason: str | None
@dataclass(frozen=True)
class FamilyMetrics:
family: str
scorable: int
unscorable: dict[str, int] # reason -> count
outcomes: dict[FamilyOutcome, int]
recall: float
silence: float
youden: float
directional_bias: float # (both_flagged - both_silent) / scorable
fixed_fpr_recall: float # recall at the family's FPR ceiling
negative_flips: int # vs last blessed round
reliable_change: bool
taxonomy_version: str
def score_pair_family(case: PairCase, family: str, vuln_runs: Sequence[Sequence[StaticFinding]], fixed_runs: Sequence[Sequence[StaticFinding]], *, ranges, taxonomy) -> PairFamilyScore
def aggregate(scores: Sequence[PairFamilyScore], *, baseline: Sequence[PairFamilyScore] | None) -> dict[str, FamilyMetrics]
def unscorable_reason(case: PairCase, *, parse_ok: bool, ranges) -> str | None # identical_twin | unsupported_language | unresolved_label | missing_context | known_limit:<x>
```
#### Split
| Field | Detail |
|---|---|
| Intent | The one rule that selects teachers, and the refusal for anything touching holdout |
| Requirements | 5.1, 5.2, 5.3, 5.4 |
**Contracts**: Service [x]
```python
def teachers(cases: Sequence[PairCase]) -> tuple[PairCase, ...] # vendored, gating tier, split == "train", scorable
def refuse_if_holdout(taught_by: Iterable[str], cases: Sequence[PairCase]) -> Refusal | None # Refusal(pairs=..., reason="holdout_pair")
```
- `export_mechanisms`, `evaluate_loo`, `evaluate_profiles`, `evaluate_mechanism_profiles`, `propose_mechanism_edits` and `build_pair_signals` obtain teachers only through `teachers`; refusals are recorded as degradations `{"stage": "learning", "reason": "holdout_pair_refused", "pairs": [...]}`.
#### Detectors
| Field | Detail |
|---|---|
| Intent | Load frozen per-family configurations and run the family detector with whole-file context and curated tools |
| Requirements | 6.1, 6.2, 6.3, 6.4, 6.5 |
**Responsibilities & Constraints**
- A configuration is a directory `learning/configs/<family>/` with `prompt.md` (length cap enforced at load), `checklist.md`, `tools.toml` (subset of `read_file`, `grep_repo`, `find_refs`, `read_definition`, `entry_points`, `flows`, `obligations`), `budget.toml` (`max_steps`, `max_cost_usd`, `max_chars`), `hard_negatives.jsonl`, `counterexamples.jsonl`, `version`.
- Round zero writes every family's directory from the current hunter prompt; a round rewrites only its target family and bumps that `version`.
- The runner builds the user message from the whole file plus resolved definitions, runs `run_tool_hunter` with the family's tools and prompt, and tags findings `family:<id>`, `detector:<id>@<version>`; the generalist is the `unknown` family's configuration.
**Contracts**: Service [x] State [x]
```python
@dataclass(frozen=True)
class FamilyConfig:
family: str
version: str
prompt: str
checklist: str
tools: tuple[str, ...]
max_steps: int
max_cost_usd: float
max_chars: int
hard_negatives: tuple[dict[str, object], ...]
counterexamples: tuple[dict[str, object], ...]
def load_family_configs(configs_dir: Path, taxonomy: FamilyTaxonomy) -> dict[str, FamilyConfig] # raises ConfigError on cap or tool violations
def run_family_detector(root: Path, region: Region, config: FamilyConfig, *, client: ChatClient, model: str) -> list[StaticFinding]
```
- Precondition: `region.families` from the classifier; postcondition: every finding carries exactly one `family:` tag and one `detector:` tag.
#### Endpoint
| Field | Detail |
|---|---|
| Intent | Resolve the chat endpoint independently of the embedding endpoint and adapt DeepSeek quirks |
| Requirements | 6.6 |
**Responsibilities & Constraints**
- Resolution order: explicit injection (tests) → `OPENULTRASAST_HUNTER_CLIENT` scripted modes → `DEEPSEEK_API_KEY` with `DEEPSEEK_BASE_URL` (default `https://api.deepseek.com`) → `OPENROUTER_API_KEY` with `OPENROUTER_BASE_URL`. Embeddings keep reading `OPENROUTER_*` only.
- `[models]`: `hunter` (default `deepseek-v4.1-flash`), `judge` (default `deepseek-v4.1-flash`), `chat_base_url`, `chat_api_key_env`.
V4 Pro was discontinued on 2026-09-10 and its requests route to V4.1 Flash from 2026-09-14, billed at Flash's
price. Detector and judge are now the SAME model, so any claim that the judge is a stronger second opinion is
a claim about the prompt and the evidence put to it, not about model class.
- Adapter: `thinking` disabled and `temperature 0` for detectors and classifier; `reasoning_content` replayed; `response_format json_object` with one retry on empty content; `logprobs` on request; usage fields normalized (`prompt_cache_hit_tokens`) into cost accounting.
**Contracts**: Service [x]
```python
@dataclass(frozen=True)
class ChatEndpoint:
base_url: str
api_key: str
provider: Literal["deepseek", "openrouter", "scripted"]
thinking: bool
price_in_per_m: float
price_out_per_m: float
def resolve_chat_endpoint(config: ResolvedConfig, *, override: ChatClient | None = None) -> tuple[ChatClient, ChatEndpoint] | None
class DeepSeekChatClient: # implements ChatClient; wraps OpenRouterChatClient with extra_body and reasoning_content replay
def complete(self, *, model, messages, tools, timeout_seconds=60, json_object=False, logprobs=False) -> ChatResponse
```
#### Verifiers
| Field | Detail |
|---|---|
| Intent | Confirm a claim through the family's oracle, or refuse to raise it |
| Requirements | 7.1–7.6 |
**Responsibilities & Constraints**
- `CanaryVerifier` (injection, path, deserialization, and command execution inside injection): writes a snippet from a family template that imports the target function, plants a canary (row in an in-memory database, file under `/scratch`, object with a marker method) and prints `CANARY:<token>` only when the vulnerability fires; the oracle is the token in stdout, never the exit code. The snippet passes `check_snippet_safety` unchanged.
- `StaticVerifier` (access control): the obligations checker's finding on the same function raises to `static_corroboration`.
- `NoVerifier`: stays at `suspicion`.
- `second_judge`: a `deepseek-v4.1-flash` call with the snippet, the oracle output and the claim, answering `json_object` `{"confirms": bool, "reason": str}`; a proven claim is published only when it confirms.
- Templates, oracle code and gold labels live under `learning/verifiers/` and `benchmarks/`, both excluded from every proposer write root.
**Contracts**: Service [x]
```python
class Verifier(Protocol):
kind: Literal["canary", "static", "none"]
def verify(self, claim: StaticFinding, *, root: Path, language: str, runner: SandboxRunner, limits: SandboxConfig) -> Verification
@dataclass(frozen=True)
class Verification:
rung: Literal["suspicion", "static_corroboration", "proven"]
oracle_output: str
reason: str
def verifier_for(family: Family) -> Verifier
def second_judge(verification: Verification, claim: StaticFinding, *, client: ChatClient, model: str) -> bool
```
- `regress.run_regression(..., oracle=Callable[[SandboxResult], bool] | None)`: when given, `TRIGGERABLE` iff the oracle returns true; exit-code behavior unchanged otherwise.
### Orchestration layer
#### Rounds
| Field | Detail |
|---|---|
| Intent | Round zero with noise floors; evolve rounds with staged evaluation, acceptance, attribution and revert |
| Requirements | 8.1–8.4, 9.1–9.9 |
**Responsibilities & Constraints**
- `run_baseline`: clone the current hunter prompt into every family directory, run K = 5 over scorable vendored pairs of the selected slices, compute per-family negative-flip rate between runs as `NoiseFloor`, persist `noise-floors.json`, publish round zero. Artifacts and floors are keyed by detector model (`baseline/<model>/`), so a second model is baselined by changing `[models].hunter` and re-running; `publish` renders a model comparison table from every baseline present. This is the maintainer's reconsideration path if the default model underperforms; configurations are never touched by a model swap.
- `run_learning_round`: failure facts for the target family → proposer → refusal checks (holdout, length cap, allowed levers) → minibatch → holdout → sweep → accept (freeze, bump version, archive) or revert (restore the directory snapshot) → journal → rejected buffer.
- Acceptance: `train_delta >= 0 and holdout_delta >= 0 and (train_delta > 0 or holdout_delta > 0) and all(sweep[f] <= floor[f])`; ties reject.
- Cost metered per stage from usage; crossing `round_cost_cap_usd` at any stage reverts and records `reverted_cost`.
- The proposer receives no holdout material; scoring of holdout happens after the proposal is frozen.
**Contracts**: Batch [x] State [x]
```python
@dataclass(frozen=True)
class NoiseFloor:
family: str
k_runs: int
negative_flip_rate: float
budget_flips: int # ceil(rate * scorable pairs)
def run_baseline(cases, *, slices, taxonomy, configs_dir, client, model, k_runs=5, out_dir) -> BaselineReport
def run_learning_round(cases, *, family, taxonomy, configs_dir, proposer, client, model, journal, archive, floors, cost_cap_usd, minibatch) -> RoundRecord
```
- Idempotency: a round directory is written under `.openultrasast/learning/rounds/<n>/` with the snapshot, proposal, scores and trajectories; rerunning a round number refuses.
- Every stage scores with K runs as the scorer requires; the noise floor is the tolerance for the cross-family sweep, not a substitute for repeated runs.
#### Proposer
| Field | Detail |
|---|---|
| Intent | Draft one structural change for one family from structured failure facts |
| Requirements | 9.1, 9.2, 5.4 |
**Contracts**: Service [x]
```python
@dataclass(frozen=True)
class FailureFacts:
family: str
misses: tuple[MissFact, ...] # pair, function, sink_or_check, line, verifier_outcome, runs
leaks: tuple[LeakFact, ...]
rejected: tuple[str, ...] # hypotheses from the buffer
config: FamilyConfig
@dataclass(frozen=True)
class Proposal:
hypothesis: str
lever: Literal["tool", "checklist", "counterexample", "memory", "prompt"]
change: dict[str, object] # file-level edit inside the family directory
predicted_affected: tuple[str, ...]
predicted_at_risk: tuple[str, ...]
class Proposer(Protocol):
def propose(self, facts: FailureFacts) -> Proposal | None
class ScriptedProposer: ... # tests and offline runs
class HarnessXProposer: ... # MetaAgent.evolve with allowed_write_roots = the family directory only; degrades to None with a recorded reason without the extra
```
#### Publish
| Field | Detail |
|---|---|
| Intent | Regenerate every published number from one command and commit its artifacts |
| Requirements | 11.1, 11.2, 4.5 |
**Contracts**: Batch [x]
```python
def publish(cases, *, taxonomy, journal, floors, out_dir=Path("benchmarks/measurements"), roadmap=Path(".kiro/steering/roadmap.md")) -> PublishReport
```
- Writes `<date>-<slice>-<family>.json` artifacts, a markdown table with scorable/unscorable counts, taxonomy version, noise floor and cost per point beside every number, and replaces the roadmap's measured section between markers.
## Data Models
### Domain Model
- Aggregates: `FamilyTaxonomy` (versioned, immutable per version); `FamilyConfig` (per family, versioned, frozen between rounds); `RoundRecord` (append-only); `PairCase` (owned by the corpus, gains two additive fields).
- Invariants: a finding carries exactly one `family:` tag; a pair has at most one `unscorable` reason; a round targets exactly one family; the archive maps a pair to one winning configuration per family.
### Data Contracts
- Catalog row additions: `[[pair.expected]] family = "<id>"` (opaque at load, validated by the Classifier), `known_limit = "identical_twin"` for the five twins, `fix_date = "YYYY-MM-DD"` populated by the catalog generator from recipe metadata where available (rows without it are counted as undated in the post-cutoff slice); `unscorable` is computed, never stored.
- Manifest block `learning = {taxonomy_version, families: {id: FamilyMetrics}, classifier: ClassifierReport, round: n | null}`; findings carry tags `family:`, `detector:`, `verifier:<rung>`.
- Journal and archive JSONL as above; `noise-floors.json`; `review-queue.jsonl` entries `{pair, label_family, classified, tier, confidence}`.
## Error Handling
- Taxonomy or configuration deviations fail fast at load with the field named (`FamiliesError`, `ConfigError`).
- Missing extras degrade with a recorded reason: no HarnessX → `ScriptedProposer` or `proposer_unavailable`; no TypeScript grammar → rows `unsupported_language`; no sandbox → `NoVerifier` for canary families with `sandbox_unavailable`.
- Endpoint errors: transient retries as today; empty `json_object` content retried once then recorded; a round whose endpoint fails mid-stage is reverted with `endpoint_failed`.
- Holdout refusal, cost overrun and safety rejection are outcomes, not exceptions, and always land in the journal.
- Monitoring: every stage runs under `HarnessRuntime.run_stage`; cost per stage and per round is written to the round directory.
## Testing Strategy
- Unit: taxonomy loader rejects an eleventh family and a missing verifier kind (1.1, 1.2); `related` returns `parent_child` for a CWE child and `fabricated` for an unknown id (3.1); classifier tier 1 decides from a rule id without a client and tier 2 falls to `unknown` under the threshold with a scripted client (2.1, 2.2); scorer counts detection only inside the labeled function and never from text (3.1, 3.6); silence ignores other-family findings (3.2); directional bias and negative flips on synthetic run sets (3.3, 3.5); `unscorable_reason` on an identical twin and an unsupported language (4.1); `teachers` excludes holdout and `refuse_if_holdout` names the pair (5.1, 5.2); configuration loader rejects a prompt over the cap and a tool outside the allowed set (6.1, 6.5); endpoint resolution prefers DeepSeek over OpenRouter and never touches the embedding variables (6.6); canary snippet passes safety and the oracle keys on the token (7.2); acceptance rule rejects ties and budget breaches (9.5); journal round-trips a record and the rejected buffer (9.3, 9.6).
- Integration: `pairs --hunter` with a scripted client on the shared handler fixture yields family outcomes and the manifest block (3.x, 6.3); `export_mechanisms` and `evaluate_loo` teach from train only and record refusals (5.1, 5.3); `run_baseline` with a scripted client writes noise floors and round-zero artifacts (8.x); `run_learning_round` with `ScriptedProposer` accepts a helpful change, rejects a tie, reverts on cost and journals attribution (9.x); `run_regression(oracle=)` returns `TRIGGERABLE` on the token with `FakeSandboxRunner` (7.2).
- Corpus: TypeScript rows parse under the extra (10.1); the five twins load as `known_limit:identical_twin` and are excluded from denominators (10.3, 4.1); `test_fixture_difficulty` fails on a fixture easier than its named rows (10.4); post-cutoff slice reported separately (10.5).
- Gates: `gate`, `map_gate`, `pair_gate` stdout byte-identical to the pre-feature commit; extra-free suite green with semantic and harnessx tests skipping (11.5).
- Live (maintainer, not CI): round zero on vibe-py and agent-vfc web families through DeepSeek, artifacts committed under `benchmarks/measurements/`.
## Security Considerations
- Every prompt and every persisted trajectory passes `redact_secrets`; verifier templates, oracle code and gold labels are outside the proposer's write roots; snippets keep today's safety bans; the sandbox stays network-off and workspace read-only; the second judge sees the snippet and oracle output, never the labels.
## Performance & Cost
- Round zero: K = 5 over ~250 vendored pairs, both sides, whole-file context ≈ 25M input tokens; at DeepSeek flash peak pricing under $15, off-peak half. A round: minibatch + holdout + sweep ≈ one baseline pass; `round_cost_cap_usd` default 10. Concurrency bounded by the runtime's existing budget controls.
## Migration Strategy
- Phase 1 (offline, no model): taxonomy, classifier tier 1, scorer, split, unscorable rows, endpoint resolution, TypeScript grammar, identical twins, fixture-difficulty check; existing numbers re-published with denominators.
- Phase 2 (live): round zero on web families; noise floors; classifier tier 2 measured; review queue worked.
- Phase 3: verifiers for the four canary families and the static verifier; second judge.
- Phase 4: evolve rounds with `ScriptedProposer` first, then `HarnessXProposer`; obligations lever as the access-control family's first round.
- Rollback: every round is a directory snapshot; `publish` regenerates from artifacts, so a bad round is reverted by restoring the family directory and re-publishing.
@@ -1,156 +0,0 @@
# Requirements Document
> **Status 2026-10-02:** retired, superseded by `model-grounded-detection` (spec.json `superseded_by`). 30 of 33 tasks were done; the open three will not be. Kept as decision history; nothing in src, tests or CI points here.
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Introduction
OpenUltraSAST's detection numbers cannot be trusted and cannot be improved. The static machinery is treated as the detector while the model with tools already outperforms it (12/14 versus 5/14 pair-correct on the vibe-py holdout once scoring is keyed on class and function), the pair scorer keys detection on text tokens rather than on a vulnerability class, the improve loop learns from its own holdout pairs, and no change is attributed to the class it targets, so an improvement in one class silently degrades another. This feature turns the tool into a learning harness: an auto-classifier assigns every labeled pair, finding and code region to one of ten verifier-aligned families; one detector configuration per family is evolved round by round from structured failure facts; a class-aware, pair-wise scorer with unscorable denominators and a measured per-family noise floor decides acceptance; only verifier-confirmed claims are reported above suspicion; and the train/holdout split is enforced by construction. Context, measurements and sources: `brief.md`, `research.md`, `benchmarks/measurements/2026-09-06-*`.
## Boundary Context
- **In scope**: the class taxonomy and its classifier; the class-aware pair scorer and unscorable-row accounting; split enforcement for every learning path; per-family detector configurations and their routing; class-keyed verifiers and reporting rungs; the round-based evolve loop with attribution; the corpus repairs the loop needs; publishing every number with its denominator and noise floor.
- **Out of scope**: finding bugs in JavaScript interpreters, WebGL renderers or browser engines; memory-safety fuzzing or crash oracles for C and C++ (the vfc slice stays a measured, non-gating slice under one coarse family); new hand-written static shape families; changes to the evidence ladder or dispositions; merge gates on non-local slices; vendoring new pairs beyond re-harvesting existing recipes; fine-tuning model weights.
- **Adjacent expectations**: the pair corpus (`pair-corpus-honesty`) keeps owning labels, tiers, splits and `known_limit`; `corpus-seeded-mechanisms` keeps its exporter, leave-one-out and lever, which this feature subordinates to the split and the class attribution; `authorization-obligations` keeps its checker, whose corpus tasks 4.3 and 6.1 move here and whose lever task 5 becomes the access-control family's first round; the local detection, map and pair gates stay byte-identical; HarnessX and tree-sitter grammars remain optional extras and every step that needs them degrades to a recorded reason without them.
## Requirements
### Requirement 1: A closed, versioned class taxonomy aligned to verifiers
**Objective:** As a maintainer, I want every unit of detection work to belong to one of a small set of named families, so that detection, silence, leaks and improvements have a definite meaning.
#### Acceptance Criteria
1. The Learning Harness shall define a closed, versioned taxonomy of exactly these families: injection into a query or command; file path from user input; deserialization of untrusted data; access control; output encoding; untrusted destination (server-side request forgery and open redirect); prototype pollution and unsafe object merge; permissive configuration and secrets; memory safety; unknown.
2. The Learning Harness shall record for every family the CWE identifiers it covers, the mechanism ids it covers, and the verifier that can confirm a claim in that family or the statement that none exists.
3. When the taxonomy changes, the Learning Harness shall bump its version and every published number shall name the taxonomy version it was scored under.
4. The Learning Harness shall treat CWE identifiers and mechanism ids as attributes of a family and shall never use them as the key that routes work or scores a pair.
### Requirement 2: An auto-classifier that is measured on its own
**Objective:** As a maintainer, I want a classifier that assigns families to pairs, findings and code regions and reports its own accuracy, so that routing errors are visible instead of hidden inside detector numbers.
#### Acceptance Criteria
1. When given a labeled pair, a finding or a code region, the Learning Harness shall return zero or more families plus, when nothing is admitted, the `unknown` family, and shall record which tier decided each answer: deterministic static signals, a model judgment, or none.
2. The Learning Harness shall attempt deterministic classification from static signals before consulting a model, and shall consult a model only for what static signals cannot place.
3. When the classifier is evaluated, the Learning Harness shall report agreement with maintainer labels on reviewed pairs with partial credit for a parent–child relation and no credit for a lateral or fabricated family, a confusion matrix per family, and its accuracy against both a random router and an oracle router.
4. When the classifier disagrees with a maintainer label, the Learning Harness shall queue the disagreement for review and shall not change the label.
5. The Learning Harness shall classify every labeled row in the corpus, including rows whose current class is free text or `other`, and shall report the count classified per family and the count left `unknown`.
6. While no family in the taxonomy declares a parent, the Learning Harness shall state beside every agreement figure that hierarchical partial credit could not apply.
### Requirement 3: A class-aware, pair-wise scorer
**Objective:** As a maintainer, I want pairs scored on whether the labeled family was found in the labeled function, so that a detector is credited for the bug it was asked about and nothing else.
#### Acceptance Criteria
1. When scoring the vulnerable side, the Learning Harness shall count a detection only for a finding inside the labeled function whose family matches the labeled family, with partial credit for a parent–child relation and the maximum penalty for a fabricated family.
2. When scoring the fixed side, the Learning Harness shall count silence only when no finding of the labeled family lies inside the labeled function, and shall report findings of other families or outside the function separately, never as leaks.
3. The Learning Harness shall report every pair as exactly one of pair-correct, both-sides-flagged, both-sides-silent, or reversed, and shall report per family and per slice the counts of each outcome, recall, silence, Youden and a directional-bias index.
4. The Learning Harness shall report a fixed-false-positive-rate view per family beside Youden.
5. When a detector is evaluated, the Learning Harness shall run it at least three times per pair and shall report a change against the last blessed round only when a reliable-change test passes, together with the negative-flip count per family.
6. The Learning Harness shall never count a text token, a CWE string or a sink name in a finding's text as evidence of detection.
7. While no family in the taxonomy declares a parent, the Learning Harness shall state beside every published family figure that hierarchical partial credit could not apply.
### Requirement 4: Unscorable rows and corpus hygiene
**Objective:** As a maintainer, I want rows that cannot be scored removed from every denominator with a stated reason, so that no number carries a permanent loss that no detector could avoid.
#### Acceptance Criteria
1. The Learning Harness shall mark a pair unscorable with one of these reasons: identical twin, unsupported language, unresolved function label, missing registration context, or a maintainer-declared known limit; and shall exclude it from recall, silence, Youden and negative-flip denominators while still listing it.
2. The Learning Harness shall never delete a catalog row; an unscorable row shall keep its reason in the catalog.
3. When pairs are loaded, the Learning Harness shall detect identical twins by comparing normalized excerpt bodies with headers stripped, and shall report duplicates across pairs before any split is applied.
4. The Learning Harness shall assign train and holdout membership by repository and in date order, so that no two pairs from one repository straddle the split.
5. When a published number is shown, the Learning Harness shall show the scorable count, the unscorable count by reason, and the taxonomy version beside it.
### Requirement 5: The split is enforced by construction
**Objective:** As a maintainer, I want it impossible for any learning path to see a holdout pair, so that a holdout number means what it says.
#### Acceptance Criteria
1. When any exporter, lever or evolve round seeds, proposes or validates from pairs, the Learning Harness shall use only train-split pairs as teachers.
2. If a candidate change was taught by, or touches, any holdout pair, then the Learning Harness shall refuse it and record the refusal with the pair names.
3. The Learning Harness shall re-measure the existing mechanism lever under this rule and shall publish the corrected holdout number in place of the earlier one.
4. While an evolve round proposes changes, the Learning Harness shall not expose holdout inputs, outputs or scores to the proposer.
### Requirement 6: One detector configuration per family
**Objective:** As a maintainer, I want each family to have its own detector configuration that other families' changes cannot alter, so that improvements do not interfere.
#### Acceptance Criteria
1. The Learning Harness shall hold one detector configuration per family consisting of a prompt under a stated length cap, a curated tool set, a step budget, a cost budget, an evidence checklist and a set of hard negatives and counterexamples.
2. While a round evolves one family, the Learning Harness shall keep every other family's configuration unchanged.
3. When a code region is classified, the Learning Harness shall run the detector of every admitted family plus a generalist detector, and shall record on each finding the family, the detector configuration version and the verifier outcome.
4. The Learning Harness shall give a detector the whole file of a region and the files its curated tools resolve, not only an excerpt around a hotspot.
5. The Learning Harness shall expose to a detector only curated tools that read, search and resolve symbols within the scanned repository, and no general shell.
6. The Learning Harness shall run detectors, the classifier's model tier and the evolve proposer on a configurable chat endpoint that is independent of the embedding endpoint, with a cheap model as the default and a stronger model reserved for the second judge and for review-queue disagreements.
### Requirement 7: Only a verifier raises a claim above suspicion
**Objective:** As an operator, I want to know that anything reported above suspicion was confirmed by something other than the model, so that reports stay honest.
#### Acceptance Criteria
1. The Learning Harness shall report a detector claim at `suspicion` unless a verifier keyed to the claim's family confirms it.
2. Where a family has a sandbox oracle, the Learning Harness shall confirm a claim by executing a family-specific snippet whose oracle is a canary observable only through the vulnerability (a canary row for injection, a canary file for command execution and traversal, a canary object for deserialization).
3. Where a family has only static corroboration (access control through the obligations checker), the Learning Harness shall raise a claim no higher than static corroboration.
4. Where a family has no verifier, the Learning Harness shall report at `suspicion` only and shall still score that family class-aware on the corpus.
5. When a claim is reported as proven, the Learning Harness shall obtain a second, independent judgment before publishing it as proven.
6. The Learning Harness shall keep verifier code, oracles and gold labels outside every surface an evolve round can edit.
### Requirement 8: Round zero establishes the baseline and the noise floor
**Objective:** As a maintainer, I want a controlled baseline and a measured noise floor per family before any learning, so that later gains and regressions are distinguishable from noise.
#### Acceptance Criteria
1. When round zero runs, the Learning Harness shall clone the current hunter into every family's configuration unchanged.
2. When round zero runs, the Learning Harness shall run each family's detector five times over the scorable vendored pairs at deterministic settings and shall record the per-family negative-flip rate between runs as that family's regression budget.
3. The Learning Harness shall publish round zero's per-family outcomes, noise floors and cost per pair before the first evolve round.
4. The Learning Harness shall run round zero first on the web families over the vibe-py and agent-vfc slices; the memory-safety family over the vfc slice is measured but never gates a round.
### Requirement 9: An evolve round improves one family under attribution
**Objective:** As a maintainer, I want each round to change one thing for one family and to be accepted only when the change helps that family without hurting any other, so that the system cannot degrade in ways nobody predicted.
#### Acceptance Criteria
1. When a round starts, the Learning Harness shall give the proposer structured failure facts for the target family (which pair, which function, which sink or check, which line, which verifier outcome) and the buffer of previously rejected proposals, and shall not give it free-form self-critique or holdout material.
2. The Learning Harness shall accept at most one change to one family per round, and the change shall be a tool, a checklist, a counterexample, a memory entry, or a prompt edit within the family's length cap.
3. When a round records its proposal, the Learning Harness shall journal a hypothesis, the levers changed, the family predicted to improve, and the families predicted to be at risk.
4. When a round is evaluated, the Learning Harness shall score the target family on a train minibatch, then on the target family's full holdout, then sweep every other family's holdout.
5. If the target family's train score or holdout score decreases, or neither strictly increases, or any other family's negative flips exceed its regression budget, then the Learning Harness shall reject the round, revert the change and record the reason.
6. When a round is accepted or rejected, the Learning Harness shall record attribution: which pairs flipped in the predicted family, which flipped in unpredicted families, and the resulting precision of the prediction.
7. The Learning Harness shall keep, per family, an archive of which configuration wins which pair, rather than one champion configuration.
8. If a round exceeds its cost cap, then the Learning Harness shall stop it, revert it and record the overrun.
9. The Learning Harness shall retain a model-authored skill, tool or prompt edit only after it passed the acceptance rule and was journaled with its attribution.
### Requirement 10: Corpus repairs the loop depends on
**Objective:** As a maintainer, I want the corpus defects that cap every number fixed before detectors are tuned against it, so that a detector change and a corpus artifact are distinguishable.
#### Acceptance Criteria
1. The Learning Harness shall parse TypeScript rows so that they are scorable rather than counted as unsupported.
2. When absence-labeled rows are re-harvested, the Learning Harness shall keep each handler with its decorators and registration statements, and every re-harvested excerpt shall keep its license line and pass redaction.
3. The Learning Harness shall mark the five byte-identical vibe-py twins unscorable with the reason identical twin.
4. If a unit-test fixture is strictly easier than the corpus rows its test names (fewer binding hops to the identity source, registration present when the corpus rows lack it), then the Learning Harness shall fail that check.
5. The Learning Harness shall carry a slice of pairs dated after the detector model's training cutoff and shall report it separately.
### Requirement 11: Publishing and operations
**Objective:** As a maintainer, I want every number regenerated from one command with its denominator, noise floor and cost, so that the roadmap cannot drift from the measurement.
#### Acceptance Criteria
1. When the roadmap numbers are regenerated, the Learning Harness shall produce them from one command and shall write the command, the taxonomy version, the scorable and unscorable counts, the noise floor and the cost per point beside every number.
2. The Learning Harness shall commit the raw measurement artifacts of every published number.
3. The Learning Harness shall persist every detector trajectory and every round's journal, redacted, so that a round can be replayed.
4. If a budget cap, a failing endpoint or a missing optional extra interrupts a run, then the Learning Harness shall record the reason and complete the remaining steps that do not depend on it.
5. The Learning Harness shall leave the local detection, map and pair gate outputs byte-identical to the pre-feature commit.
-137
View File
@@ -1,137 +0,0 @@
# Research & Design Decisions: learning-harness
## Summary
- **Feature**: `learning-harness`
- **Discovery Scope**: Complex Integration (extension of pairs, tool hunter, provider, regress, improve; adoption of HarnessX meta-harness; corpus repairs)
- **Key Findings**:
- The tool hunter already beats the overlay 12/14 vs 5/14 on the class-aware, scorable vibe-py holdout; the published 0/17 was the text-token scorer.
- Every learning path ignores the split (`evaluate_profiles`, `evaluate_mechanism_profiles`, `propose_mechanism_edits`, `evaluate_loo`, `build_pair_signals`, `export_mechanisms`); one teacher rule fixes all of them.
- The sandbox verdict is exit-code only and snippets are compile-only templates; a canary oracle is an additive callback that respects the existing safety bans.
- The chat and embedding clients share one base-URL variable; DeepSeek needs thinking disabled, `reasoning_content` replay, no `/v1`, `json_object` only, and offers no seed and no embeddings.
- HarnessX 0.1.0 already provides the round primitive (`MetaAgent.evolve`, journal with `predicted_affected` and precision discounted by unpredicted regressions); the tool supplies tasks, trajectories, the evaluator and routing.
- The field routes on coarse verifier-aligned families, gates reports on proof, and reports regression blindness in meta-agents; per-instance archives and no-regression acceptance rules are the published remedies.
## Research Log
### Codebase seams (2026-09-06, Explore subagent over `src/openultrasast`)
- **Context**: design an extension without re-reading every module.
- **Findings**: `run_tool_hunter(root, hotspots, *, client, model, max_steps)` with three curated tools and `suspicion` findings tagged `tool-hunter`; `ChatClient.complete(*, model, messages, tools, timeout_seconds)`; `OpenRouterChatClient.complete_chat` always sends `temperature: 0` and drops `reasoning_content`; `_hunter_matches_expected` is text-token based and every fixed-side finding leaks under `fix_policy = silent`; `run_regression(..., sandbox, sandbox_limits, images)` with `verdict_from_result` on exit code; `check_snippet_safety` bans sockets, curl and workspace writes; `MechanismStore` append-only JSONL with tombstones; `improve.run_round` journal has no hypothesis, attribution or cost fields and its regression check is per provenance profile; `load_dotenv` no-ops under pytest; `pyproject` semantic extra lacks TypeScript; `pair_gate` scores only vendored `local` pairs.
- **Implications**: the design adds parameters and additive fields instead of replacing modules; the family scorer is injected into the hunter path so `pairs` never imports `learning`; the oracle is an optional callback on `run_regression`; endpoint resolution takes an explicit override for tests.
### DeepSeek platform API (2026-09-06, api-docs.deepseek.com)
- **Findings**: base `https://api.deepseek.com` (no `/v1` in current docs), Anthropic-shape endpoint at `/anthropic`; models `deepseek-v4-flash`, `deepseek-v4-pro`, `deepseek-v4-flash-vision-exp`; 1M context, 384K output; thinking on by default (`thinking: {"type": "disabled"}` to turn off; `temperature` ignored while thinking); tools with `tool_choice`, `reasoning_content` must be replayed on tool turns; `response_format json_object` only, occasional empty content; `logprobs`/`top_logprobs` documented, unverified in thinking mode; no `seed`, `n`, `logit_bias`; flash 0.44/1.32 USD per M in/out peak, cache hit 0.014, pro 1.32/3.96, off-peak half; concurrency 2500 flash / 500 pro; no embeddings endpoint.
- **Implications**: detectors and classifier run with thinking disabled and temperature 0; determinism is best-effort and the K-run noise floor absorbs it; the classifier's model tier uses `logprobs` averaged and falls to `unknown`; embeddings stay on OpenRouter; cost accounting reads DeepSeek's cache-hit usage fields.
### Measurement that triggered the spec (2026-09-06)
- See `brief.md` Problem section and `benchmarks/measurements/2026-09-06-*`.
## Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|--------|-------------|-----------|---------------------|-------|
| Keep static machinery as the detector, add a class field | Minimal change | Cheap | Ceiling by construction; measured 5/14 vs 12/14 | Rejected |
| One generalist hunter, evolved as a whole | Single config | Simple loop | Aggregate fitness hides per-class regressions; the maintainer's oscillation observation; regression blindness in the literature | Rejected |
| Per-family detector set with auto-classifier, per-family attribution, verifier-gated reporting | This design | Matches AIxCC routing practice, GEPA per-instance archives, slice-based gating | Router error (63.8% in MoEVD); mitigated by multi-label routing plus a generalist and by measuring the router separately | **Selected** |
| Fine-tune a model per class | Weights | Potentially strong | Out of scope; label noise 25–60% in public sets; not reproducible offline | Rejected |
## Design Decisions
### Decision: families are the only routing and scoring key
- **Context**: 1.4, 3.6; text tokens and CWE ids were the proxy.
- **Alternatives**: fine CWE routing; mechanism ids as key.
- **Selected**: ten coarse families with CWE and mechanism as attributes; hierarchical relation for partial credit.
- **Rationale**: fine CWE from code is unsolved (≤14.7% top-1) and coarse families are what worked in AIxCC and MoEVD.
- **Trade-offs**: less granular reporting; compensated by attributes on findings.
### Decision: the scorer is injected into the hunter path, not imported by `pairs`
- **Context**: dependency direction; `pairs` is owned by `pair-corpus-honesty`.
- **Selected**: `_evaluate_hunter_pair` accepts a matcher/leak callable; the CLI wires `learning.scoring`.
- **Trade-offs**: one more parameter; keeps `pairs` free of `learning` and the overlay path unchanged.
### Decision: canary oracle as an optional callback on `run_regression`
- **Context**: 7.2; exit code cannot express "the vulnerability fired".
- **Alternatives**: a parallel verifier runner; parsing stderr.
- **Selected**: `oracle: Callable[[SandboxResult], bool] | None`; token in stdout.
- **Trade-offs**: minimal change to regress; templates per family live in `learning/verifiers/`.
### Decision: HarnessX proposer behind the seam, scripted proposer first
- **Context**: 9.x with core `dependencies = []`.
- **Selected**: `Proposer` protocol; `HarnessXProposer` composes `MetaAgent.evolve` with `allowed_write_roots` = the family directory; `ScriptedProposer` for tests and phase 4 start.
- **Trade-offs**: two proposers; the loop is testable offline and the extra stays optional.
### Decision: noise floor from K = 5 baseline runs is the regression budget
- **Context**: 8.2, 9.5; no public numeric negative-flip budget exists.
- **Selected**: per-family negative-flip rate between seed runs; `budget_flips = ceil(rate × scorable)`.
- **Trade-offs**: a noisy family gets a loose budget; reported beside the number so it is visible.
### Decision: DeepSeek adapter wraps the existing OpenRouter client
- **Context**: 6.6; both clients share one base URL today.
- **Selected**: `ChatEndpoint` resolution order DeepSeek → OpenRouter → scripted; adapter adds `extra_body` and `reasoning_content` replay; embeddings untouched.
- **Trade-offs**: one adapter class; no new dependency.
## Risks & Mitigations
- Router error routes a region to the wrong family → multi-label routing plus the generalist; router measured separately against random and oracle.
- No seed on DeepSeek → K runs and the reliable-change test; determinism recorded as best-effort.
- Canary templates cannot express some families (XSS, SSRF) → those report at `suspicion` only, stated in the taxonomy.
- Proposer confabulation → structured failure facts only; rejected buffer; length cap; structural levers preferred.
- Cost drift → per-stage metering, per-round cap with revert, cost per point published.
- Re-harvest needs network and licenses → maintainer step; excerpts re-redacted; failures leave the old excerpt and a recorded reason.
## References
- Codebase seams and DeepSeek facts: this file, sections above.
- External literature: the four threads below.
---
## External research (2026-09-06)
Web research conducted 2026-09-06 in four threads by research subagents; summaries are theirs, condensed, with primary sources. Speculation is marked. This section backs the "What others learned" section of `brief.md`.
## 1. Cyber reasoning systems (AIxCC finals 2025, Big Sleep, XBOW, Glasswing)
Primary cross-team source: USENIX Security '26 SoK on AIxCC, arXiv 2602.07666. 42-b3yond-6ug and Lacrosse have no first-party write-up.
| System | Decomposition / routing key | LLM vs tools |
|---|---|---|
| Atlantis (1st), arXiv 2509.14589, team-atlanta.github.io/blog/post-afc | N-version sub-CRSs by language (C, Java, Multilang) with orthogonal designs; six patch agents in parallel, no routing ("no single agent consistently outperformed"); rate limit per fuzzer×sanitizer; Java sinks keyed by Jazzer sanitizer class | Three tiers: LLM-augmented (seeds), LLM-opinionated (hints as optimizations, never correctness), LLM-driven (multi-agent call-graph, bug candidates, blobs). Fuzzers/concolic are ground truth |
| Buttercup (2nd), blog.trailofbits.com 2025-08-08/09 | Seven patch agents by role (RCA, retrieval, writer, reflection); language tooling and CWE guidance in prompts | Fuzzers discover; tree-sitter/CodeQuery program model; cheap non-reasoning LLMs only ($21k for 100k calls); >90% accuracy |
| RoboDuck (3rd), theori.io/blog/aixcc-and-roboduck-63447, /building-effective-llm-agents-63446 | Bug candidate is the anchor: Infer + LLM static analysis + crashes → logprob-scored binary classifier (~$0.001/report) → top 20% to a source-browsing agent (~$0.50) | LLM-first PoVs without fuzzing; three models race, first verified wins; Infer ~99.9% FP |
| FuzzingBrain (4th), arXiv 2509.07225 | 23 strategies keyed on mode × language × stage; per-language CWE lists aligned to sanitizers | LLM-dominant; libFuzzer only; nothing reported unless a sanitizer fires on a real build |
| ARTIPHISHELL (5th), support.shellphish.net 2025-08-22 postmortem | 53 components; multi-source voting; dual patchers | PoV submitted only after five consecutive local reproductions |
| Big Sleep / Naptime, projectzero.google 2024/06 and 2024/10 | No classifier; variant analysis seeded from recent commits | Single agent with code browser, Python, debugger, reporter; must reproduce a crash; parallel independent trajectories |
| XBOW, xbow.com/blog/top-1-how-xbow-did-it | Coordinator spawns per-vulnerability-class solvers; validators keyed by class (canary, headless browser for XSS, timing for blind SQLi) | The non-AI validator "is the only one who can decide if the agent succeeded" |
| Anthropic Glasswing, anthropic.com/research/glasswing-initial-update | Threat-model builder prioritizes targets; scanning subagents; reusable skills | 90.6% of 1,752 findings valid after human reproduction; internals unpublished |
Lessons: (1) route on mode × language × sanitizer/CWE family, never fine CWE; route to what the verifier can check. (2) Only proof survives; the accuracy multiplier (Atlantis 0.9999 vs 0.9044) decided the ranking; RoboDuck lost 16.3 points to FPs. (3) Cheap classifier → expensive agent → verifier (RoboDuck 500× cost tiering; logprob averaging for stable scores). (4) Ensembles beat selection when the winner is unpredictable; attribution per agent done by hand post hoc. (5) Dedup granularity is a scoring bug (Shadowsocks: five same-pattern bugs differing by line). (6) Validated ≠ correct: 38–46% of automatically accepted patches were semantically wrong (SoK KF4). (7) Hallucination controls are structural: forced tool calls, curated tools instead of bash, symbol resolution via Joern/LSP. (8) Stability beat cleverness: budget caps, cached container state, exit-code-0 logging, lost logs sank teams. (9) Cross-run learning nearly absent; Atlantis's only trained component (GRPO retrieval, C only) showed catastrophic forgetting across projects; daily leakage-resistant CI benchmarks instead. (10) Cost is a control: >$1,000/hour unbounded (arXiv 2603.08566); Buttercup $181/point.
## 2. Self-improving harness methods
| Method | Evolved unit | Search operator | Validation |
|---|---|---|---|
| DSPy MIPROv2 (dspy.ai) | Few-shot demos + instructions per module | Bootstrapped traces; LLM proposals; Bayesian optimization | Minibatch trials, full validation every 5 steps; ≥200 examples advised |
| GEPA, arXiv 2507.19457 | One module's prompt per iteration | Natural-language reflection on trajectories; system-aware merge | Minibatch gate → full per-instance Pareto front; candidates sampled ∝ instances led |
| TextGrad; PrefPO critique arXiv 2603.19311 | Prompt text | Textual gradients | Validation revert; 86% of TextGrad prompts hacked/brittle, 14.7× length inflation |
| OPRO 2309.03409 / EvoPrompt 2309.08532 / PromptBreeder 2309.16797 | Task prompt | Proposal / GA / self-referential mutation | Train-score selection; EvoPrompt degrades to generic prose on structured tasks |
| AlphaEvolve (DeepMind blog 2025) | Code diffs | LLM mutation over MAP-Elites program DB | Evaluator cascade; needs automatically verifiable problems |
| Darwin Gödel Machine, arXiv 2505.22954, sakana.ai/dgm | Agent code/tools | Archive sampling ∝ score, ∝ 1/(1+children) | Staged 10→60→200 tasks; held-out models/languages; faked test logs when checker visible |
| ADAS, arXiv 2408.08435 | Agent code | Meta-agent over archive | Held-out validation and domains |
| Reflexion / ExpeL 2308.10144 / AWM 2409.07429 / Voyager 2305.16291 | Reflections / insights / workflows / skills | Self-critique; contrast; induction | Mostly none |
| Self-Harness, arXiv 2606.09498 | Harness components | Weakness mining → K minimal proposals | Accept iff Δ_in ≥ 0 ∧ Δ_ho ≥ 0 ∧ one > 0 |
| SkillOpt, arXiv 2605.23904 | SKILL.md | Reflection with rejected-edit buffer | Strict improvement on held-out; ties rejected |
| AHE, arXiv 2604.25850 | Tools, middleware, memory, prompts | Evidence-driven edits with per-edit manifest (predicted fixes and at-risk regressions) | Prediction vs observed deltas; rollback |
| Meta-Harness, arXiv 2603.28052 | Harness code | Agentic proposer over prior candidates and traces | Held-out models and tasks |
Lessons: (1) per-instance/per-class tracking doubled gains vs greedy best-overall (GEPA +12.44% vs +6.05%). (2) No-regression acceptance rule; reject ties (Self-Harness, SkillOpt). (3) Regression blindness: AHE fix-prediction precision 33.7% but regression prediction 11.8%; the cross-class sweep is mandatory. (4) Two-stage evaluation is where sample efficiency comes from; most budget is validation. (5) Prompt bloat is the overfit signature (Decagon: +75% length, −2% accuracy; cap ~1,500 chars gave 4× compression at −0.8%). (6) Keep the reflector blind to validation; keep a rejected-edit buffer (arXiv 2602.22483; SkillOpt). (7) Structural edits transfer (+5.1–10.1 points across model families), system-prompt-only edits regressed −2.3 (AHE ablation). (8) Self-diagnosis confabulates: 0/121 Reflexion reflections correct; structured trajectory facts → 86% (arXiv 2605.29463). (9) Hide the checker (DGM). (10) One objective per round (arXiv 2605.26046: joint criteria −59% task focus). (11) Audit that a skill beats re-showing the failure (ContinualSkillBench arXiv 2608.03874). (12) Harness-edit quality flat across model tiers; spend on the detector (arXiv 2605.30621). Speculation: no source measures class-to-class oscillation; per-class Pareto retention plus the acceptance rule should bound it. Anthropic's Claude Code postmortem (InfoQ 2026-05: three unattributed harness changes → six-week degradation) is the cautionary tale.
## 3. Dataset pitfalls and class taxonomies
Scoring: (1) dedup by normalized body hash before splitting; split by commit, chronologically (PrimeVul arXiv 2403.18624; test-set copies 12.7–18.9% in older sets). (2) Automatic labels 25–60% correct (BigVul 25%, CVEfixes 51.7%, DiverseVul 60%; Croft et al. ICSE'23 20–71% wrong, 17–99% duplicates; CleanVul arXiv 2411.17274). (3) Pair-wise outcomes P-C/P-V/P-B/P-R, not F1 (GPT-4 CoT P-C 12.94% vs P-V 54.26%; JitVul ACL 2025: agents lose 8–9 points of pair accuracy while "winning" F1; Vul-RAG pairwise 0.21 vs plain 0.61). (4) Directional-bias index (CWE-Trace arXiv 2606.20502: −85.5 to +94.8 points at ~50% accuracy). (5) Score at fixed FPR (PrimeVul VD-S; VFC benchmark arXiv 2605.13138). (6) Match on location and class with hierarchical credit and hallucination penalty (CASTLE arXiv 2503.09433; ALPHA arXiv 2601.01320). (7) Robustness to renames/whitespace/dead code (SecLLMHolmes arXiv 2312.12575; patched-code FPs dominate). (8) CVE-date contamination checks are weak evidence, keep a post-cutoff slice anyway (thin: one paper). (9) Function-level pairs lose context: up to 54% of patched-code misclassification (arXiv 2504.13474). (10) Run-to-run consistency 8.3–81.9% by model (ALPHA).
Taxonomy: (1) fine CWE from code unsolved (top-1 ≤ 14.7% over 74 CWEs; 86.5% lateral errors; VulDetectBench arXiv 2406.07595 >80% coarse, <30% detailed). (2) From CVE text ~85–90% ceiling (arXiv 2603.14911; CweAgent arXiv 2608.21977). (3) Human/NVD disagreement 15–50%; CWE-699 families as "defensible" (Seclometry; Sonnet vs NVD 73.1% exact, 84.5% with parent–child, LLM right in 72/100 disagreements). (4) Score hierarchically. (5) Few aggregated classes: MoEVD arXiv 2501.16454 collapsed 88 → 12 families, per-class experts +12.8% F1, router 63.8% right, F1 0.18 when wrong; three-class OOB/UAF/other 87.4% (arXiv 2509.22796). (6) Specialists need hard negatives (arXiv 2408.02329: F1 0.89 in class → 0.12–0.19 on full corpus). (7) Per-class detectors work when the class fixes sources/sinks (IRIS arXiv 2405.17238: 55 vs 27 CodeQL detections, FDR 46–85%). (8) VFC typing immature (F1 0.53–0.70). Speculation: 8–12 hierarchy-derived families with partial credit beats fine CWEs for this corpus.
## 4. Routing and regression control
Router: (1) learned routers plateau 10–20 points below oracle, sometimes lose to random (RouteLLM arXiv 2406.18665; RouterBench arXiv 2403.12031; arXiv 2606.07587). (2) Evaluate router AUROC separately from expert strength (arXiv 2602.11877). (3) Small embedding classifier fine-tuned on own labels: 80.4% → 98.5% (Red Hat vLLM Semantic Router, 2026-06). (4) Cascades tolerate router error until judge error > 0.2 (FrugalGPT arXiv 2305.05176; RouterBench). (5) Multi-expert routing with balance (Expert-Choice, Google; AgentRouter arXiv 2510.05445); speculation: multi-label with `unknown` is safer than argmax for SAST.
Attribution: (6) slice everything (Oakden-Rayner arXiv 1909.12475; Slice-based learning 1909.06349; Overton 1909.05372; Model Cards 1810.03993). (7) Negative flips per item (PCT arXiv 2011.09161: 5.2% NFR at equal accuracy; MUSCLE 2407.09435: 8–49% NFR across Llama versions; arXiv 2604.27405: +1.6–2.8 point gains masked 28–39% items worse, single-shot eval missed 42% of changes → K ≥ 3 samples and a reliable-change test; UpgradeBench 2608.20918: neutral retraining breaks 1–5%). (8) Frozen per-class configurations (+20%, Progressive Prompts arXiv 2301.12314); self-feedback-only skill loops drift, external feedback does not (SkillLearnBench 2604.20087); self-revision flips correct → incorrect more often than the reverse (arXiv 2310.01798). (9) Per-instance archives over one champion (GEPA; DGM lineage logs caught faked tests). (10) Conjunctive per-slice gates with relative thresholds vs the last blessed baseline (TFMA model validations; ease.ml/ci arXiv 1903.00278; Amazon Alexa regression-free updates). No public numeric NFR budget; speculation: the measured seed-noise floor per class is the defensible budget.
-31
View File
@@ -1,31 +0,0 @@
{
"feature_name": "learning-harness",
"created_at": "2026-09-06T06:32:41Z",
"updated_at": "2026-09-08T00:00:00Z",
"language": "en",
"phase": "implementation",
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": true
}
},
"ready_for_implementation": true,
"depends_on": [
"pair-corpus-honesty",
"corpus-seeded-mechanisms",
"authorization-obligations",
"tree-sitter-overlay-extra",
"harnessx-self-improving-rulesets"
],
"approval_note": "2026-09-06 requirements approved by the maintainer in session ('approve the requirements and run the design phase'); 2026-09-06 design approved by the maintainer in session ('approve the design and generate the tasks'); 2026-09-06 tasks approved by the maintainer in session ('approve the tasks and go on with the implementation'); 2026-09-06 requirements amended by the maintainer in session ('amend the requirements to untrusted_destination and note the flat taxonomy'): Req 1.1 names untrusted destination instead of server-side request forgery, and Req 2.7 and 3.7 require every agreement and family figure to state that hierarchical partial credit could not apply while the taxonomy is flat; 2026-09-06 Req 6.1 amended by the maintainer in session ('drop the skills set from the requirement'): a detector configuration has no skills set, because the skill router belongs to the tool's earlier shape and a family's prompt, checklist, tools and counterexamples already carry what a round evolves; 2026-09-06 design amended during task 4.1 (recorded in design.md): a recipe may name cross-file registration documents, because connexion registers by operationId in an OpenAPI file and express in a router module, so Req 10.2 is unreachable from a single-file excerpt for those rows; 2026-09-06 task 5.3 amended by the maintainer in session (asked and answered): the first evolve rounds target `injection` rather than `access_control`, because under the split and review-tier rules access_control has one train teacher and four holdout pairs and teaches zero obligation shapes, while injection has twelve teachers and ten holdout pairs; 2026-09-08 SUPERSEDED by execution-grounded-detection: this session's measurements showed the noise architecture (K-runs, per-family floor, regression budget, sign test, evolve loop) is a statistical treatment for a structural problem, and that the field (Big Sleep, AIxCC finals, Clearwing) grounds detection in execution rather than in scoring against a labeled corpus. The taxonomy, the IR candidate enumerator (96.6% ceiling) and the obligation checker survive as the web/logic detection path to contribute to a Clearwing-based substrate.",
"superseded_by": "model-grounded-detection"
}
-269
View File
@@ -1,269 +0,0 @@
# Implementation Plan
- [x] 1. Foundation: taxonomy, endpoint, corpus fields, grammar
- [x] 1.1 Closed family taxonomy with a verifier per family
- Ten families exactly as decided, each with its CWE set, mechanism set, verifier kind (canary, static, none) and description; `unknown` always present with no verifier; a version string.
- Loader rejects an unknown field, an eleventh family, a missing verifier kind or a CWE claimed by two families, naming the offender; relation lookup answers same, parent–child, lateral or fabricated for any two ids.
- Observable: the taxonomy loads from the ruleset directory, a fixture with a fabricated family fails by name, and every mechanism id in the mechanism vocabulary resolves to exactly one family.
- _Requirements: 1.1, 1.2, 1.3, 1.4_
- [x] 1.2 Independent chat endpoint with the DeepSeek adapter
- Endpoint resolution order: explicit override, scripted client modes, DeepSeek key and base URL, then OpenRouter; the embedding client keeps reading only its own variables; `[models]` gains the hunter and judge defaults and the endpoint keys, `[learning]` gains its section.
- Adapter disables thinking and sends temperature zero for detector and classifier calls, keeps the provider's reasoning field on assistant messages so a caller can replay it, supports JSON-object mode with one retry on empty content and optional log-probabilities, and reports normalized usage and cost from the provider's cache-hit fields.
- Observable: with both provider keys set the hunter client targets the DeepSeek base URL without a version suffix while the embedding client still targets OpenRouter; a scripted client is chosen under the existing environment flag; a recorded request shows thinking disabled and the reasoning field retained on the reply.
- _Requirements: 6.6, 11.4_
- [x] 1.3 Additive corpus fields and hygiene at load
- Expected rows accept an opaque family id (validated later by the classifier task, so the corpus module never imports the learning package); a pair carries a computed unscorable reason (identical twin by normalized body comparison with headers stripped, computed only when the catalog does not already declare a known limit) and is never dropped; duplicates across pairs are reported before any split; a repository-and-date split helper assigns train or holdout to rows that lack a split, keeps declared splits, and reports rows from one repository that straddle the split; pair signals take a split filter.
- Observable: the five identical vibe-py twins load with the identical-twin reason and the existing catalogs load with unchanged row counts; the straddle report on the current catalogs is produced and committed.
- _Requirements: 4.1, 4.2, 4.3, 4.4_
- [x] 1.4 TypeScript rows parse under the extra
- The semantic extra gains the TypeScript grammar and the parser maps TypeScript and TSX to it; the extra-free suite still skips them with the existing marker.
- Observable: every vendored TypeScript pair parses on both sides under the extra, and the parser reports no unsupported-language result for TypeScript or TSX.
- _Requirements: 10.1_
- [x] 1.5 Curated hunter tools and hunter-loop parameters
- Four new repository tools beside the existing three: resolve a definition by symbol, list entry points, list flows for a function, list obligations for a function, each clamped to the scanned repository and returning identifiers and line spans only; the hunter loop gains prompt, context files, tool set, character limit and tag parameters with today's behavior as defaults and replays the provider's reasoning field across tool turns.
- Observable: the existing hunter tests pass unchanged with default parameters; each new tool answers on the shared handler fixture and refuses a path outside the repository; a recorded tool-turn conversation shows the reasoning field replayed.
- _Requirements: 6.4, 6.5_
- [x] 2. Core: classifier, scorer, detectors, verifiers, journal
- [x] 2.1 (P) Classifier tiers
- Tier one decides from rule id, sink family, obligation kind, mechanism id or labeled CWE without a model; tier two asks the chat endpoint in JSON-object mode with thinking disabled and averages log-probabilities, falling to unknown under a threshold; tier three is unknown; every answer records its tier; results are multi-label; loaded catalog family labels are validated against the taxonomy here and a fabricated family is rejected by name.
- Observable: on the shared handler fixture tier one classifies the leaky handler as access control without any client; with a scripted client an off-vocabulary answer becomes unknown; a catalog with a fabricated family is rejected by name.
- _Requirements: 2.1, 2.2_
- _Boundary: Classifier_
- [x] 2.2 Classifier measurement and review queue
- Report: agreement with maintainer labels on reviewed pairs with parent–child partial credit and no credit for lateral or fabricated ids, confusion matrix per family, accuracy against a random router and an oracle router, counts per tier and unknown; disagreements append to a review queue and never change a label; the whole corpus, including free-text and `other` rows, is classified with per-family counts.
- Observable: the report on the vendored corpus lists the vfc `other` rows by resulting family and the queue holds every disagreement once with both answers and the tier.
- Observable: the report also states whether hierarchical partial credit could apply.
- _Requirements: 2.3, 2.4, 2.5, 2.6_
- [x] 2.3 (P) Per-pair family scoring
- Detection only for a finding whose family tag relates same or parent–child to the labeled family and whose line lies in the labeled function, with the maximum penalty for a fabricated family; silence only when no such finding lies in the labeled function on the fixed side; other findings counted separately, never as leaks; text is never inspected; outcomes pair-correct, both-flagged, both-silent, reversed and unscorable; the unscorable reason also covers unsupported language, unresolved label and missing context.
- Observable: a synthetic run set where every fixed side carries an other-family finding scores full silence; a finding with a fabricated family never counts; a pair in an unsupported language scores unscorable with that reason.
- _Requirements: 3.1, 3.2, 3.6, 4.1_
- _Boundary: Scorer_
- [x] 2.4 Aggregation, noise handling and denominators
- Per family and slice: outcome counts, recall, silence, Youden, directional-bias index, a fixed-false-positive-rate view; K runs per pair with the majority outcome, negative flips per family against a baseline set, and a paired reliable-change test; unscorable rows listed by reason and excluded from every denominator; every metrics block carries the taxonomy version and the scorable and unscorable counts.
- Observable: a single-run difference below the reliable-change threshold reports no change while a consistent flip across runs does; the unscorable count and taxonomy version appear in every metrics block.
- Observable: every metrics block states whether hierarchical partial credit could apply.
- _Requirements: 3.3, 3.4, 3.5, 3.7, 4.5_
- [x] 2.5 (P) Per-family detector configurations and the family detector runner
- A configuration directory per family: prompt under a stated length cap, checklist, tool list from the curated set only, step and cost budget, maximum characters, hard negatives and counterexamples, version; the loader rejects an over-cap prompt, an unknown tool or a missing family; this task ships the loader and a test fixture configuration, the shipped per-family directories are written by round zero.
- The runner takes the families admitted by the classifier as input, builds the detector's user message from the whole file plus definitions its tools resolve, runs the hunter loop with the family's prompt, tools and budgets, and tags each finding with exactly one family tag and one detector-version tag; the generalist is the unknown family's configuration.
- Observable: a family run on the shared fixture with a scripted client yields findings tagged with that family and version and uses only the family's tool list; a configuration whose prompt exceeds the cap fails to load by name.
- _Depends: 2.1_
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- _Boundary: Detectors_
- [x] 2.6 (P) Verifier registry and the oracle callback
- The regress verdict accepts an optional oracle callback and keeps exit-code behavior otherwise; a registry keyed by the taxonomy's verifier kind returns a canary, static or no-op verifier; the static verifier raises access-control claims to static corroboration when the obligations checker reports the same function; the no-op verifier leaves suspicion.
- Observable: with the fake sandbox runner a result whose output carries the token yields triggerable while a nonzero exit without the token does not; a family with no verifier keeps suspicion on the shared fixture; the access-control claim on the leaky handler reaches static corroboration.
- _Requirements: 7.1, 7.3, 7.4_
- _Boundary: Verifiers, Regress_
- [x] 2.7 Canary templates and the second judge
- Family snippet templates for injection, file path and deserialization plant a canary (row in an in-memory database, file under the scratch mount, marked object) and print the token only when the vulnerability fires; the second judge asks the judge model with the snippet, oracle output and claim and answers confirm or not in JSON-object mode; a claim is published as proven only when it confirms; templates, oracle code and gold labels live outside every proposer write root.
- Observable: every canary template passes the existing snippet safety check; a scripted judge that declines keeps the claim below proven; templates and oracle code live under the verifier directory and gold labels under the corpus directory.
- _Requirements: 7.2, 7.5_
- [x] 2.8 (P) Round journal, archive and rejected buffer
- Append-only round records with hypothesis, levers, predicted affected and at-risk families, outcome, reason, target deltas, per-family sweep flips, attribution (flipped predicted, flipped unpredicted, precision), cost, taxonomy and configuration versions; a per-family archive of which configuration version wins which pair; a rejected-hypothesis buffer per family; every persisted text passes redaction.
- Observable: a record round-trips through the file; the buffer returns only the target family's rejected hypotheses; the archive names one winner per pair per family; a secret literal written into a record is stored redacted.
- _Requirements: 9.3, 9.6, 9.7, 11.3_
- _Boundary: Journal_
- [x] 3. Integration: split, scoring paths, rounds, proposer, publish, CLI
- [x] 3.1 One teacher rule across every learning path
- Teachers are vendored, gating-tier, train-split, scorable pairs; a refusal names any candidate taught by or touching a holdout pair; the exporter, leave-one-out, per-profile evaluation, mechanism-profile evaluation, mechanism admission and pair signals all obtain teachers through the rule and record refusals as degradations; the mechanism lever's holdout number is re-measured under the rule and its raw artifact committed for the publish task.
- Observable: leave-one-out on the toy slice seeds from train pairs only; a candidate shape taught by a holdout pair is refused by name; the re-measured artifact is in the measurements directory.
- _Depends: 1.3_
- _Requirements: 5.1, 5.2, 5.3_
- [x] 3.2 Family scoring on the pairs hunter path
- The pair scorer's hunter path takes an injected family matcher and leak rule from the learning package, classifies each pair once, runs the admitted family detectors plus the generalist K times per side, and emits family outcomes and metrics; the overlay and inventory paths stay untouched.
- Observable: the pairs command with a scripted client on the shared fixture reports a family outcome per pair; no text-token matching remains on the hunter path; the pair gate output is byte-identical to before.
- _Depends: 2.1, 2.4, 2.5, 2.6_
- _Requirements: 3.1, 3.2, 3.3, 3.5, 11.5_
- [x] 3.3 Round zero and the noise floor
- Clone the current hunter prompt into every family configuration; run each family K = 5 times over the scorable vendored pairs of the selected slices at deterministic settings; record the per-family negative-flip rate between runs as that family's regression budget; persist noise floors and round-zero artifacts keyed by detector model so another model can be baselined and compared without touching configurations; the web families over vibe-py and agent-vfc are the default selection and the memory-safety family over vfc is selectable but never gates.
- Observable: with a scripted client round zero writes a noise-floor file with one budget per family and a round-zero report per slice; a second baseline under another model name writes a separate artifact and the comparison table lists both.
- _Depends: 3.2_
- _Requirements: 8.1, 8.2, 8.3, 8.4_
- [x] 3.4 Family detectors, verifiers and tags in scans
- In a standard scan each classified region runs its admitted family detectors plus the generalist from the directories round zero wrote (a missing family directory is a recorded degradation and that family's detector is skipped), claims pass through the family's verifier, findings carry family, detector and verifier tags in reports and SARIF, and the manifest gains a learning block with taxonomy version, per-family counts, the classifier report and the current round; the detection and map gates stay byte-identical.
- Observable: a standard scan of the shared fixture with a scripted client writes findings tagged with a family, a detector version and a verifier rung, and the manifest block; the detection and map gate outputs diff empty.
- _Depends: 3.3, 2.6_
- _Requirements: 6.3, 7.1, 11.5_
- [x] 3.5 Proposer protocol, scripted proposer and the HarnessX proposer
- Failure facts for one family: misses and leaks with pair, function, sink or check, line, verifier outcome and run outcomes, plus the rejected buffer and the current configuration; a proposal is one hypothesis, one lever (tool, checklist, counterexample, memory, prompt), one file-level change inside the family directory, and predicted affected and at-risk families; the proposer never receives holdout inputs, outputs or scores.
- A scripted proposer for tests and offline rounds; a HarnessX proposer composing the meta-agent with write roots limited to the family directory, degrading to a recorded reason without the extra.
- Observable: the scripted proposer returns a proposal from a facts fixture; the HarnessX proposer is skipped with the reason recorded when the extra is absent and, when present, writes only inside the family directory; the write-root list excludes the verifier and corpus directories.
- _Depends: 2.5, 2.8_
- _Requirements: 9.1, 9.2, 5.4, 7.6_
- [x] 3.6 Acceptance rule and directory revert
- Acceptance requires train and holdout non-negative with one strictly positive and every other family within its budget; ties reject; a family directory snapshot restores byte for byte on rejection or overrun; refusals for holdout contact, length cap and lever set are outcomes with reasons.
- Observable: a helpful change is accepted, a tie is rejected, a change that breaches another family's budget is rejected with the family named, and a revert restores the directory byte for byte.
- Observable: every family other than the target has a byte-identical directory before and after the round, whatever the outcome.
- _Depends: 3.3_
- _Requirements: 6.2, 9.5_
- [x] 3.7 Evolve round orchestration, cost and journal
- One round: facts, proposal, refusals, train minibatch, target-family holdout, sweep over every other family's holdout, K-run scoring at every stage as the scorer requires, accept (freeze, bump version, archive) or revert; cost metered per stage with revert and a recorded reason on overrun; a journal record and the rejected buffer written for every outcome; a round directory holds the snapshot, proposal, scores and trajectories and a repeated round number is refused.
- Observable: with the scripted proposer each of accepted, rejected, budget-breach and cost-overrun leaves one journal record with attribution and a round directory; a repeated round number is refused.
- The round directory holds the pair identities behind the attribution counts and every detector trajectory, redacted, so a round can be replayed and its attribution audited; every stage scores at least three runs per pair.
- _Depends: 3.5, 3.6_
- _Requirements: 3.5, 9.4, 9.6, 9.7, 9.8, 9.9, 11.3_
- [x] 3.8 Publish from artifacts
- One entry point regenerates every family and slice number from committed artifacts, writes a markdown table with scorable and unscorable counts, taxonomy version, noise floor and cost per point beside every number, replaces the roadmap's measured section between markers, and writes the raw artifacts under the measurements directory.
- Observable: running the publish entry point twice without new artifacts produces byte-identical output; the corrected mechanism-lever number from the re-measurement artifact replaces the earlier roadmap value.
- _Depends: 3.1, 3.3_
- _Requirements: 11.1, 11.2, 4.5, 5.3_
- [x] 3.9 Learning subcommands
- Subcommands for classify, score, baseline, round and publish with the flags the design names; the pairs hunter path gains the K-runs flag; every step runs under the harness runtime with degradations for missing extras, endpoints or sandbox.
- Observable: each subcommand exits zero on the shared fixture with a scripted client and writes its artifact; the roadmap section written by the publish subcommand carries the command line that produced it; a missing endpoint records the reason and the remaining steps complete.
- _Depends: 3.7, 3.8_
- _Requirements: 11.1, 11.4_
- [x] 4. Corpus repairs
- [x] 4.1 Re-harvest absence rows with registration context
- Absence rows in vibe-py and agent-vfc gain their family and the handler-context mode in the recipes; the maintainer re-harvests them over the network; every re-harvested excerpt keeps its license line, passes redaction and keeps the decorators and registration statements; failed fetches keep the old excerpt with a recorded reason; catalogs regenerate idempotently.
- Observable: the vampi and threatbyte excerpts contain their registration lines, the entry-point mapper names their handlers, and the catalogs load with unchanged vendored counts.
- _Depends: 1.3_
- _Requirements: 10.2_
- [x] 4.2 Identical twins and the fixture-difficulty check
- The five identical vibe-py twins carry the identical-twin known-limit reason in the catalog; a test derives a difficulty vector for a unit fixture (binding hops to the identity source, registration present, cross-file registration) and for the corpus rows its docstring names, and fails when the fixture is strictly easier.
- Observable: the twins are excluded from every denominator with their reason listed; the shared obligations fixture is compared against the re-harvested vampi rows and the check's verdict is recorded.
- _Depends: 4.1_
- _Requirements: 10.3, 10.4_
- [x] 4.3 Fix dates and the post-cutoff slice
- Catalog rows gain a fix date populated by the catalog generator from recipe metadata where available; rows without a date are counted separately; rows dated after the configured detector cutoff form a separately reported slice in every scoreboard.
- Observable: the scoreboard shows the post-cutoff slice with its own counts, the cutoff date it used and the undated count.
- _Depends: 1.3, 2.4_
- _Requirements: 10.5_
- [ ] 5. Validation
- [x] 5.1 Gates, matrices and degradation paths
- Detection, map and local pair gate outputs byte-identical to the pre-feature commit; the extra-free suite green with semantic and HarnessX tests skipping; each optional dependency's absence exercised once with its recorded reason.
- Observable: the three gate outputs diff empty against the recorded baseline and both test matrices pass.
- _Requirements: 11.4, 11.5_
- [ ] 5.2 Live round zero
- Round zero through the DeepSeek endpoint on the vibe-py and agent-vfc web families with K = 5, plus the memory-safety family over the vfc slice as a measured, non-gating run; artifacts, noise floors and cost per pair committed; the classifier report and review queue produced on the same run; a second baseline with the judge model on one slice for the model comparison table.
- Observable: the published table shows per-family outcomes, noise floors and cost for both models and for the vfc memory-safety family, and the roadmap section is regenerated from it.
- **Blocked 2026-09-06: no `DEEPSEEK_API_KEY` in `.env` or the environment.** Everything else is ready; the run is one command per baseline. The maintainer chose to add the key and have the full run go ahead (web families with `deepseek-v4-flash`, the vfc memory slice as a measured non-gating run, and a second baseline with `deepseek-v4-pro` on one slice). Scale, measured on the corpus: 58 scorable web pairs plus 176 vfc pairs, both sides, K=5 = 2,340 detector runs at up to four model calls each; the design estimated under $15 at peak flash pricing.
- Dress-rehearsed offline 2026-09-06 with the scripted client over the whole vibe-py slice at K=3: round zero produced six families with per-slice blocks, correct denominators (the twins out by reason), and a floor each; a round then read the recorded proposals, applied one, swept the other five families for negative flips (0 each), rejected on `tie`, reverted the family directory byte for byte and archived the rejected version's per-pair results. The only thing the live run adds is a detector that can actually see.
- _Depends: 3.9, 4.1_
- _Requirements: 8.3, 8.4, 11.1, 11.2_
- [ ] 5.3 First evolve rounds
- **Amended 2026-09-06 (maintainer, in session): the target family is `injection`, not `access_control`.** Measured on the corpus under the split and review-tier rules: access_control has **1 train teacher and 4 holdout pairs**, six of its rows unusable because every agent-vfc row sits at tier `title`, and the vibe-py absence rows that could teach are Real-Vuln-Benchmark trap pairs whose two sides are different handlers — so the train split teaches **zero** obligation shapes and any accepted change would rest on one pair. `injection` has 12 train teachers and 10 holdout pairs, the only family in the corpus with enough train evidence to tell a real change from noise. The first memory entries are therefore the mechanism shapes the train split really teaches rather than obligation shapes, which do not exist yet. access_control follows when its corpus can teach: either a maintainer reviews agent-vfc rows up to `reviewed`, or the trap rows gain non-trap twins.
- Three rounds with the scripted proposer on the injection family using the shapes the train split teaches as the first memory entries, then one round with the HarnessX proposer; every round journaled with attribution; the roadmap regenerated.
- Observable: the journal holds four records with outcomes and attribution precision, at least one accepted change is versioned in its family directory, and no other family exceeded its budget in any accepted round.
- _Depends: 5.2_
- _Requirements: 9.9_
## Task-plan review notes
- Sanity review round 1 (2026-09-06): NEEDS_FIXES, eleven findings, all repaired (K-run policy honors Req 3.5; per-model baselines added to the design; family validation moved out of the corpus loader; reasoning replay owned by the hunter loop; Req 5.3 split into re-measure and publish; scoring path split into pairs and scan tasks; oversized tasks split; fix-date prerequisite added; fixture-difficulty check depends on the re-harvest; memory-safety family measured on vfc; detector task depends on the classifier and round zero owns the shipped directories; observables re-anchored).
- Sanity review round 2 (2026-09-06): NEEDS_FIXES with three local items, repaired without a third review under the bounded loop: task 1.5 delivers the four curated tools and the hunter-loop parameters that 2.5 relied on; the scan-path task now follows round zero and degrades when a family directory is missing; the write-root observable moved to the HarnessX proposer task with 7.6. Non-blocking notes applied: the split helper assigns as well as reports (4.4), 3.2 depends on 2.1 rather than 2.2.
## Implementation Notes
- Requirements approved 2026-09-06, design approved 2026-09-06, tasks approved 2026-09-06 (maintainer, in session).
- Task 1.1 (2026-09-06, RED first: 9 failing tests in `tests/test_learning_families.py`, `ModuleNotFoundError: openultrasast.learning`; then green): `learning/families.py` and `ruleset/families.toml` with the ten decided families, each carrying its CWE set, mechanism set, verifier kind and description. Decisions taken while writing the data: (a) `unknown` carries the six mechanisms that span families (`source_reaches_sink`, `container_taint`, `validation_strength`, `redos`, `cross_artifact`, `other`) and the two weaknesses we deliberately do not target (CWE-20 generic validation, CWE-835 infinite loop), so a row we considered and declined is visible instead of unresolved; an earlier draft said `unknown` had none. (b) The maintainer's "server-side request forgery" family is named `untrusted_destination` and also covers open redirect (CWE-601), because both are untrusted input choosing a request destination and the corpus carries 34 open-redirect rows that would otherwise be unclassifiable. (c) The taxonomy is flat: `parent` is an optional field no shipped family uses, exercised by a fixture, because Req 2.3 and 3.1 need a parent-child arm and inventing a hierarchy would be worse than leaving it empty. (d) Abstention earns no partial credit: `related("unknown", X)` is `lateral`. All 33 labeled corpus weaknesses resolve, none is claimed twice, and every mechanism id belongs to exactly one family.
- Task 1.2 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_endpoint.py`; then green): `learning/endpoint.py` resolves override, then the scripted flag, then a configured endpoint, then DeepSeek, then OpenRouter; the embedding client is untouched. `provider/openrouter.py` gains `complete_chat_raw` (whole payload, for `usage`) and an `extra_body` passthrough; `ChatResponse` gains `reasoning` and `mean_logprob`. Design deviation: `ChatEndpoint` deliberately holds no `api_key` (the design listed one) so the record is safe to journal; the key stays on the client. `[models]` gains `judge`, `chat_base_url`, `chat_api_key_env`; `[learning]` is new. `ModelConfig.hunter` keeps its `None` default on purpose: defaulting it would silently enable LLM stages in existing scans, so the DeepSeek model defaults live in `resolve_models`.
- Task 1.3 (2026-09-06, RED first: 7 failing tests in `tests/test_pair_hygiene.py`; then green): `ExpectedFinding.family` (opaque; the classifier validates it), `PairCase.unscorable`, `PairCase.split_declared`, `PairCase.fix_date` (the generator fills it in 4.3), `duplicate_groups`, `split_by_repository` with `SplitReport`, and a split filter on `build_pair_signals`. Measured and committed as `benchmarks/measurements/2026-09-06-corpus-hygiene.json`: **19 repositories straddle the train/holdout split** (a learning path can be taught by one file of a repository and evaluated on another) and **10 groups of pairs share a vulnerable excerpt byte for byte**, mostly the same fix harvested twice at different granularity. Only three vendored rows (the `local` slice) declare no split.
- Task 1.4 (2026-09-06, RED first: 2 failing tests in `tests/test_typescript_grammar.py`; then green): the semantic extra gains `tree-sitter-typescript`; `grammar_for` maps a language to (module, attribute) because the wheel ships two grammars, and `parse_with_cst` picks the JSX-aware one for a `.tsx` path while the parsed file keeps the language it was given. Eighteen agent-vfc rows plus the TypeScript absence rows now parse on both sides.
- Task 1.5 (2026-09-06, RED first: 5 of 6 tests in `tests/test_hunter_curated_tools.py` failing; then green): `hunter_tools` gains `read_definition`, `entry_points`, `flows`, `obligations`; `run_tool_hunter` gains `system_prompt`, `user_prompt`, `context_files`, `tools`, `max_chars` and `tags`, all defaulting to today's behaviour, and replays the provider's reasoning field on the next turn. Deviations: `read_definition` returns the definition text as well as its span, and includes preceding decorator lines, because a resolver that returns only line numbers forces another `read_file` and a handler's decorators are exactly what the obligations work showed to matter; the three list tools return identifiers and spans only. No caching: a stale view of a tree a round may have rewritten is worse than a reparse.
- Review round 1 (2026-09-06) APPROVED 1.1, 1.2, 1.4, 1.5; REJECTED 1.3. The reviewer ran every check in a detached worktree at the group-1 tip and confirmed RED-first by running each new test file against the pre-feature commit (37 failures there, all passing after), the three gate outputs byte-identical, and `pairs`/`benchmark` free of any learning import.
- **1.3 rejection, remediated RED-first**: `_body_digest` stripped every leading comment line, which in C and C++ swallows `#include` and `#define`, and in any language swallows an ordinary leading comment. Two files differing only in a buffer size on a `#define` hashed equal and were marked `identical_twin`, removing a real pair from every denominator. Measured before the fix: twelve file sides across six vendored pairs had directives inside the stripped prefix, including the Juliet stack-overflow pair whose whole point is that constant. The strip is now anchored to the grammar `harvest.provenance_header` writes: a leading comment counts as header only when it carries one of the eleven provenance keys followed by a colon, and the scan stops at the first line of code. Four regression tests pin both directions plus a corpus-wide assertion that no vendored excerpt loses a directive. The five twins, ten duplicate groups and nineteen straddling repositories are unchanged, so the committed hygiene artifact still holds.
- Suggestions applied: a mechanism claimed by two families now fails loud by name; the obligations tool's path-escape refusal and the endpoint's degrade-to-None on a provider error are now asserted; the TypeScript parse assertion is exact instead of a 25% allowance, which surfaced one real corpus defect (`lensops-kubernetesmanagement-fe4910` was harvested as a truncated destructuring parameter list and is not valid TypeScript on its own; it belongs to the 4.1 re-harvest).
- **Two findings escalated to the maintainer, both blocking task 3.8 rather than group 1.** (a) `untrusted_destination` contradicts the approved requirement sentence, which says "server-side request forgery": the family id is the key every published number carries, so `requirements.md` Req 1.1 and the brief's Decision 1 must be amended to the widened name, or the family renamed back and CWE-601's 34 rows re-homed. (b) The shipped taxonomy is flat, so `parent_child` is unreachable in production and the "partial credit for a parent-child relation" in Req 2.3 and 3.1 is inert at version 1; either declare real parents or record the inertness beside every published agreement number.
- Carry-forward: the nineteen straddling repositories cross the split by declaration, so `split_by_repository` cannot fix them and task 3.1's teacher rule inherits the leak until a maintainer re-declares those splits. The `preprocess_repository` cost of the three list tools should be measured in 2.5 before round zero.
- Task 2.1 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_classify.py`; then green): `learning/classify.py` with three tiers, multi-label answers in taxonomy order, and `validate_family_labels` owning the taxonomy check the corpus loader deliberately does not do.
- Task 2.2 (2026-09-06, RED first: 5 failing tests in `tests/test_learning_classifier_report.py`; then green): `measure_classifier` and `write_review_queue`. Bug caught by the RED test: the classifier read the declared label it was being scored against, making agreement trivially 1.0; `classify_pair(..., use_declared=False)` is what the measurement uses. Measured on the 277 vendored pairs: tier one places 275 without a model call, 2 land in `unknown`, and the 176 memory-safety rows resolve to one family. `labeled` is 0 because no row carries a maintainer family label yet, and the report says so instead of inventing a score.
- Task 2.3 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_scoring.py`; then green): `learning/scoring.py` `score_pair_family`. Detection is a family-tagged finding inside the labeled function; text is never read. Findings of other families or outside the function are counted separately and never called leaks; a fabricated family id is counted as noise. K runs, majority outcome, disagreeing runs recorded as flips.
- Task 2.4 (2026-09-06, RED first: 7 failing tests in `tests/test_learning_aggregation.py`; then green): `aggregate` and `sign_test`. Recall at a fixed false-positive ceiling is forfeited entirely when the leak rate exceeds it; the directional-bias index separates "flags everything" from "silent on everything", which Youden cannot; change against a baseline is a two-sided exact sign test over per-pair flips, not a difference of means. `PairFamilyScore` gained a `slice` field (additive, filled from the case) so metrics can be reported per slice.
- Task 2.5 (2026-09-06, RED first: 9 failing tests in `tests/test_learning_detectors.py`; then green): `learning/detectors.py` with `FamilyConfig`, `Region`, `load_family_configs`, `run_family_detector`, `run_region_detectors`, `write_default_configs`. Deviations: the parts live in one `family.toml` plus the markdown and JSON-lines files, because two TOML files with three keys between them was more surface than value and `version` had no home in the design's list; configurations live under the project's learning directory, not inside the package, because a round writes them. `CURATED_TOOLS` is derived from `HUNTER_TOOLS` so the closed set cannot drift.
- Task 2.6 (2026-09-06, RED first: 6 failing tests in `tests/test_learning_verifiers.py`; then green): the optional oracle callback on `verdict_from_result`, inserted after the inconclusive guards and before the exit-code branch so a call without an oracle is the old decision tree, plus `verifier_for`, `StaticVerifier`, `NoVerifier` and `apply_verification`.
- Task 2.7 (2026-09-06, RED first: 8 failing tests in `tests/test_learning_canaries.py`; then green): `learning/canaries.py` with per-family templates, `CanaryVerifier` and `confirm_proven`. Deviation: templates live in `learning/canaries.py` rather than the design's `learning/verifiers/` directory; the property that matters, being outside every proposer write root, holds and is tested.
- Task 2.8 (2026-09-06, RED first: 6 failing tests in `tests/test_learning_journal.py`; then green): `learning/journal.py` with `RoundRecord`, `Attribution`, `LearningJournal`, `Archive` and `summarize`.
- Review round 2 (2026-09-06) APPROVED 2.1, 2.2, 2.4, 2.5, 2.8; REJECTED 2.3, 2.6, 2.7. The reviewer ran every check in a detached worktree at the group tip, re-derived the RED phase per task against each task's parent commit (8/5/8/7/9/6/8/6 failures, the four recorded claims exact), killed 15 mutants against the new tests, and hand-verified `sign_test` against the closed form for every (better, worse) with n <= 20.
- **2.3 rejection, remediated RED-first**: two of Req 4.1's five unscorable reasons had no code path, so a row whose language could not be parsed was reported as `unresolved_label`, a corpus labelling defect, when the cause was a tooling gap. The design's `unscorable_reason(case, *, parse_ok, ranges)` is reinstated as a public function, `score_pair_family` takes `parse_ok` and `entry_points`, and `missing_context` names a handler that arrived without the registration that makes it reachable, for the families whose bug shape is a missing check.
- **2.6 rejection, remediated RED-first**: `apply_verification` wrote `evidence_level="proven"`, which is not a member of the project's `EvidenceLevel`, so `verify_finding`, `is_report_verified` and `verify_judge` all raise `ValueError` on it. Requirements put the evidence ladder out of scope and the design's data contract specifies only the `verifier:<rung>` tag. Nothing in production called it yet, so the suite stayed green while task 3.4's wiring would have broken a scan. `apply_verification` now writes only the tag; `evidence_level_for` and `raise_to` are the single explicit bridge onto the ladder, and a test round-trips a proven claim through the project's own helpers.
- **2.7 rejection, remediated RED-first**: the injection canary opened its own in-memory database, inserted the token there and called the handler without ever giving it that connection, so the oracle could never fire; the fake sandbox runner returned the token regardless of the snippet, which is why no test noticed. It is now an execution canary whose marker only an interpreter could write, and each template is exercised end to end by running the rendered snippet against a vulnerable fixture and its fixed twin. That test immediately caught a second flaw: a fixed twin that echoes its input printed the oracle token and satisfied the oracle with no vulnerability at all, so an injected payload now carries a value unrelated to the token the oracle looks for. Recorded scope: the injection canary confirms command, code and template execution; a query-language injection whose payload never reaches an interpreter stays a suspicion.
- Suggestions applied: a configuration with an empty tool list is refused by name instead of producing a detector that silently reports nothing; the classifier abstains when its no-adapter fallback call also fails; the archive docstring states that the earliest winning version keeps its claim to a pair.
- Task-plan updates from the review: 2.2 and 2.4 claim the two criteria the maintainer added (2.6 and 3.7); 3.6 claims Req 6.2 with an observable that every non-target family directory is byte-identical across a round; 3.7 claims Req 3.5's K floor, 9.6's pair identities and 11.3's trajectories.
- **Escalated to the maintainer**: Req 6.1 lists "a skills set" among a detector configuration's parts, and neither the approved design nor the code has one, although the repository ships a skill router. Either Req 6.1 drops the skills set or a task wires it; the implementation followed the approved design, so this is a spec-internal conflict rather than a deviation.
- Carry-forward: the nineteen straddling repositories still cross the split by declaration and cap every holdout number these tasks produce; `unknown` serves as both the classifier's abstention and the generalist detector, which deserves a sentence in the design before a round ever targets it.
- Task 4.1 (2026-09-06, RED first: 21 of 23 failing tests in `tests/test_pair_registration_context.py`; then green, 26 tests). Measured before the change: **8 of the 10 vendored access-control rows had no named handler at all**, so every one of them would have scored `missing_context` forever rather than as a detector miss. After the re-harvest 9 of 10 are named and the tenth states its reason. What the repair needed, in order of how much it cost:
- **Class registration.** A flask-restx handler is a method; `@ns.route('/profile')` sits on the class. `extract_handler_context` now carries the enclosing class declaration and its decorators, so the excerpt is both reachable and parsable — a bare indented `def get` is neither.
- **Multi-line decorators and commented-out guards.** `@ns.doc(...)` wraps over four lines, and stopping at a continuation dropped the `@ns.route(...)` above it — the registration itself. `#@token_required` above a handler is not noise; in the two ThreatByte rows it is the entire bug, and the evidence field says so. `_decorator_start` now walks both.
- **Cross-file registration (design amendment, recorded in `design.md`).** connexion registers VAmPI's handlers by `operationId` in `openapi_specs/openapi3.yml`; Overleaf registers `exportProject` in `router.mjs`. No single-file excerpt can contain that, so a recipe may name `context = [...]`, the harvester writes the stanza per side under `<name>/context/<side>/<relpath>`, the catalog carries `[[pair.context]]`, and `_materialize_side` lays the documents out beside the excerpt at their repository-relative paths. `mapping._route_manifest_entry_points` reads `operationId` plus the operation's `security` block; `_js_statements` joins a registration that spans lines, because reading `webRouter.post(` alone finds neither the handler nor the middleware that guards it.
- **Anchors.** Real-Vuln-Benchmark trap rows take their fixed side from a different, correctly guarded handler of the same snapshot, so `handler_context` honours `line`/`fix_line` and a new `fix_function` gives the fixed side *its* registration instead of the vulnerable side's.
- **`innermost_function_bounds`** for brace languages, used only by this mode: `function_bounds_at` returns the outermost top-level group, which for a handler declared inside a React component is the whole 6000-line component. `hunk` and `enclosing` rows keep the bounds they were harvested with.
- **Fetch-then-write.** `harvest_recipe` fetches every blob before the first write, and the driver takes `--report` so a failed fetch leaves the reviewed excerpt exactly as it was and says why. A half-written re-harvest is indistinguishable from a real upstream change.
- Bugs the re-harvest exposed and fixed on the way: `.mjs` and `.cjs` were not in `LANGUAGE_BY_EXTENSION`, so the one pair vendored as an ES module had **no file target at all** — no entry point, no finding, a permanent miss; and `_python_route_access` matched auth words anywhere in a decorator, so `@ns.doc(description='... without proper authorization checks')` read the unguarded ThreatByte endpoint as `authenticated`, silencing every obligation on it. The guard is now the decorator's name, never its prose, and `@token_required` — the commonest Flask guard — is recognised at all.
- Corpus corrections: `lensops-kubernetesmanagement-fe4910` was labeled `setErrorMessage`, a React state setter, because the hunk landed in the wrong bounds; it is now `loadNamespaces` and the pair diff is exactly the fix. Its bug is a browser-side call omitting the header its siblings carry, not a missing guard on an inbound handler, so it carries `known_limit = "client_side_caller"` and leaves the denominators with a stated reason.
- Three existing tests encoded the old corpus and now encode the new one: the TypeScript parse pin (the lensops excerpt parses now), the classifier report (`labeled` 0 -> 4, the four seeded rows; the six `title`-tier agent rows carry a family but are not labels a maintainer stands behind), and the hygiene report ordering.
- **New honesty finding, recorded in the hygiene artifact as `cross_split_duplicates`: nine of the ten duplicate groups sit on both sides of the split.** Two forks of one project (`openniw-jobs-0fc947` train, `openniw-jobs-b74de3` holdout) and eight CVEs harvested twice under different names are byte-identical, so the train row teaches the holdout row exactly. `split_by_repository` cannot see it — the duplicates are different repositories. `duplicate_groups` is now ordered by name so the committed report does not reshuffle when one excerpt is re-harvested.
- Pre-existing drift the regeneration corrected, unrelated to this task: `catalog.toml` in git disagreed with a fresh `catalog_gen` run at the group-3 tip on two derived labels (`origin` -> `createApplication`, `normalizeDomain` -> `getGoogleProvider`). Verified in a detached worktree at `7a33756` before touching anything.
- Group 3 (2026-09-06). Tasks 3.1-3.9 implemented in `86ed54c`, `fcda1ee`, `e8e6baf`, `127dbef`, `ad7c98e`, `346cc70`, `16d9bc2`, `b955251`, `b8ad42c`, `7a33756`. RED counts, derived by the reviewer against each task's parent commit: 3.1 4/6, 3.2 6/6, 3.3 6/6, 3.4 3/5, 3.5 10/10, 3.6 12/12, 3.7 9/9, 3.8 7/7, 3.9 7/7. Deviations from the design, recorded here because they were not written down at the time: `learning.split` duck-types `PairCase` through `getattr` rather than importing it; `pairs.py` reaches `learning` through function-local imports rather than the design's injected callable; `run_baseline` takes a scan factory where the design named a client. One flaky test found and fixed in `346cc70` (`_is_vulnerable_side` keyed on `"-v" in str(root)`, which random temporary directory names matched about once in eight; harness noise recorded as detector noise would inflate every family's regression budget).
- Review round 3 (2026-09-06) APPROVED 3.4; REJECTED 3.1, 3.2, 3.3, 3.5, 3.6, 3.7, 3.8, 3.9. The reviewer ran every check in detached worktrees at the group tip against `71efc9c`, derived the RED phase per task, mutation-tested `decide`, the teacher rule, the snapshot restore and the round's revert paths (7 mutants on the acceptance rule, 6 killed and 1 equivalent), ran the three flakiness-prone files 61 times with varied `PYTHONHASHSEED` without a failure, and confirmed the three gate outputs byte-identical and the extra-free matrix unchanged. Every rejection was remediated RED-first; the numbers each defect was hiding are recorded below because they are the point of the spec.
- **3.2, the fixed side was judged against the vulnerable side's line ranges.** A fix that adds a guard above the handler moves its first line, and the reviewer measured that **191 of 227 pairs with a resolvable labeled span (84%) have different spans on the two sides**. A finding inside the fixed handler was therefore read as outside it: a leak recorded as silence, always in the direction that flatters Youden. `score_pair_family` now takes `fixed_ranges`; the fixed side's parse was already being computed and thrown away.
- **3.2 and 3.9, K runs was declared and never threaded.** `_evaluate_hunter_pair(..., runs=1)` had one call site that passed nothing, `evaluate_catalog` had no parameter at all, and `pairs --k-runs` was parsed and discarded; the only test grepped `--help`. K now flows `pairs --k-runs` -> `evaluate_catalog` -> `evaluate_pair` -> `_evaluate_hunter_pair`, defaults to three and refuses less, and the test counts scans per side.
- **3.2, the generalist could never be scored.** `pairs --hunter` fed the class-aware scorer findings with no `family:` tag, so it reported recall 0 by construction. `run_tool_hunter` now reads an optional `family` field into the tag (the design said it would) and `make_hunter_scan(taxonomy=...)` asks for one from the closed list. A generalist that names no family is unanswerable; one that names a wrong family is counted as noise, which is the honest treatment.
- **3.3 and 3.9, round zero measured every family with the generalist.** `run_baseline` took one `scan`, and the CLI handed it the `unknown` configuration; since `related(X, "unknown")` is `lateral` by the taxonomy's own decision that abstention earns no credit, **every family except `unknown` baselined at a structural zero** and every noise floor with it. `run_baseline` now takes a `scan_factory` and runs each family's own configuration, reports per slice as well as per family, and refuses fewer than three runs. This is the decision the group-2 review asked for on `unknown` serving as both abstention and generalist: it is the generalist for *scanning*, and it is never credited as a family for *scoring*.
- **3.5, the meta-agent proposer was a stub** that returned None after confirming the extra was present, so `ousast learning round` could never propose anything at any time. It now composes `MetaAgent.evolve` behind `harness_ext` against a scratch **copy** of the family directory — the live tree is never touched, because the round snapshots only after a proposal exists — and builds the proposal from the diff of that copy, so the one-file rule and the lever whitelist are enforced on what the agent did rather than on what it claimed. `learning round --proposals FILE` runs a recorded sequence offline, which is what makes the command exercisable without the extra at all.
- **3.6 and 3.1, the holdout refusal existed only as a vocabulary word.** `journal.py` documented `refused_holdout` and no code path could produce it; the covering test asserted `outcome == "refused_holdout" or outcome == "rejected"`, which passes whichever way the code behaves. The round now runs `refuse_if_holdout` over every corpus pair the proposal names — in its predictions and in the file it writes — and journals `refused_holdout` with the pair names. A memory entry that quotes a held-out pair looks exactly like a legitimate hard negative until the holdout number is read as if it meant something.
- **3.7, the sweep counted absolute failures, not negative flips.** Req 9.5 compares against the pre-change state; the code counted every pair of another family that was not `pair_correct` and compared that to a budget derived from the run-to-run flip rate. On the real corpus, where most pairs fail at round zero, that rejects **every** round for damage it did not do — a harness that then reports it cannot improve. Unscorable rows voted too, against Req 4.1. The sweep is now scored before and after the change, `_negative_flips` counts only pairs that were right and are not, unscorable rows leave the count, and attribution is derived from the same comparison instead of from absolute failures.
- **3.7, cost was metered once**, after the minibatch, the holdout and the whole cross-family sweep had all been paid for; Req 9.8 says stop, revert and record. The cap is now read after every stage. A stage that raises is journalled as `reverted`/`stage_failed` with its round directory before the exception leaves, because an exception that loses the record also loses the money the round already spent. A rejected round's version is archived: Req 9.7 exists so a configuration that lost overall but is the only one that ever won some pair is not forgotten.
- **3.8, the corrected lever number was published beside the earlier one, not in place of it** (Req 5.3), and the test named for that property never checked it. `publish` now rewrites the superseded claim to point at the regenerated section, idempotently, and the test asserts the old sentence is gone. Run against the real roadmap: the 2026-09-05 claim "human holdout profile pair_correct 0 -> 6 of 17, Youden 0 -> +0.353" now says it was measured with holdout pairs teaching and names the artifact that corrects it.
- **3.1, refusals were `continue` statements.** `Refusal.degradation()` existed and nothing called it. The mechanism lever now records every candidate it declined with the holdout pairs that taught it, leave-one-out records the pairs it kept out of the teaching set, and `improve` carries both into the round outcome. `semantic.seed` reported `split:train` for a train-split identical twin, which is simply untrue; an unscorable row now reports its own reason. `evaluate_profiles` and `evaluate_mechanism_profiles` are deliberately split-blind and now say so in their docstrings: they measure a ruleset and never learn from a pair, so a filter there would only shrink the evidence.
- Test-quality repairs the review asked for, each of which changed a number: two split tests were measuring an `x = 1` fixture that teaches nothing whatever the rule says, so both arms now use a pair that really carries a shape and the lever test asserts the same shape *is* admitted when a train pair taught it; the two CLI scoreboard tests asserted against output that was all zeros, and now use a side-aware detector stub (the shipped `scripted` stub answers twice, then goes quiet, and puts its finding outside the labeled function — three separate ways to make a family number zero); `ScriptedChatClient` gained an opt-in `cycle` so a stub cannot silently run out of answers mid-run; the sandbox-candidate assertion in `tests/test_learning_scan.py` compared verdict paths against a prefix no implementation would produce and now compares finding ids; the manifest's classifier block reported `measure_classifier([])`, all zeros, and now reports what this scan actually routed.
- Carry-forward, unchanged and still capping every holdout number: the nineteen straddling repositories, and — found during task 4.1 — **nine of the ten duplicate groups sit on both sides of the split**, which the repository-level split cannot see because the duplicates are different repositories.
- Task 4.2 (2026-09-06, RED first: 2 failing tests in `tests/test_fixture_difficulty.py`, and the check's own arm — `test_the_check_catches_a_fixture_that_is_easier_on_one_axis` — fails if the fixture hardening is reverted). Two parts:
- **The five twins declare their limit.** A content hash is evidence; the catalog row is the maintainer standing behind it, and without one a re-harvest that changes a byte turns a known-unscorable row back into a scored one with no review. A declared limit that names one of the vocabulary's own reasons keeps that spelling, so `identical_twin` declared and `identical_twin` computed stay one fact instead of splitting across two buckets in every aggregation. `semantic.seed._skip_reason` was reordered for the same reason: an unscorable row reports `unscorable:<reason>` before either the known-limit or the split branch can claim it. Consequence, recorded because it moves a published number: leave-one-out over vibe-py now runs on **30 rows, not 35** — a pair whose two sides are the same code cannot be held out against itself.
- **The fixture-difficulty check** (`learning/difficulty.py`), on three axes the corpus really varies on: binding hops from the identity source to the constrained use, registration distance (0 decorator on the handler, 1 statement elsewhere in the file, 2 another document), and guard distance (0 decorator, 1 call in the body, 2 nothing visible). The comparison is per axis over the whole set, because a suite covers a bug shape with several handlers between them. **Measured first, and it failed: the shared obligations fixture reached (1, 0, 2) where the vibe-py absence rows reach (5, 2, 2).** Every shared handler carried its own route decorator, so nothing in the suite exercised a handler registered from another document and no chain was longer than one binding — the checker was passing on a simpler world than the one it meets. `from_spec` was added to the fixture: the connexion shape those rows really have, a token validated by a call rather than a decorator and five bindings between the token and the constraint. The verdict is committed as `benchmarks/measurements/2026-09-06-fixture-difficulty.json` with both reaches and the history of the first measurement. One existing expectation moved with it (`from_spec` reads the user row and then the book: two real operations).
- Task 4.3 (2026-09-06, RED first: 10 failing tests in `tests/test_learning_cutoff.py`; then green, 11). The catalog generator fills `fix_date` from the recipe at the precision the recipe has: a full ISO day when one was recorded, otherwise the year. Every harvested recipe carries a year and almost none carries a day, and writing `-01-01` would turn a year into a claim about a month. A row therefore counts as post-cutoff only when its **earliest possible** date is after the cutoff, so a coarse date can under-claim the clean slice but never over-claim it — and over-claiming is the direction that would turn contamination into a result. `PairFamilyScore` carries the date, `aggregate(cutoff=...)` reports the same numbers over the post-cutoff rows beside the whole number and never instead of it, undated rows are counted on their own, and `publish` prints the slice with the cutoff it used. Without a configured cutoff every scoreboard says so rather than inventing one; `[learning].cutoff_date` has no default because guessing a model's training cutoff and publishing a contamination number from the guess would be worse than reporting none. Corpus dates as harvested: vibe-py all 2026, agent-vfc 67 of 69 in 2026, vfc spread 2014-2026, vfc-js 2014-2021.
- Task 5.1 (2026-09-06, RED first: 3 of 9 tests in `tests/test_degradation_matrix.py`; then green, 8 after one test was deleted rather than kept). The three gate outputs are captured at `71efc9c` — the commit before the first line of this feature — as `benchmarks/measurements/2026-09-06-gate-baseline.txt`, and a test reproduces them byte for byte on every run, in both matrices. Until now byte-identity was something a reviewer checked by hand against a worktree; it is now mechanical and runs in CI, which is what makes it a gate rather than a habit. Six absences are each exercised once with the reason they record: HarnessX (`harnessx_unavailable`), the chat endpoint (`learning_endpoint_unavailable`, and the run still writes its artifact), the family configurations (`learning_configs_unavailable`), the network for pointer pairs (`pointer_pair_skipped`), the semantic extra (`language_unsupported` rather than a guess), and the sandbox (`sandbox_unavailable`, so a claim nothing could run is not confused with a claim that failed). One test written for this task was **deleted before the commit**: it asserted that this file contained a `without_<capability>` name per capability, which measures the file's own naming and nothing about the code.
- Review round 4 (2026-09-06) APPROVED 4.1, 4.2, 4.3; REJECTED the round-3 remediation on three counts. The reviewer re-derived the RED phase for group 4 (24 of 26 for 4.1, 3 for 4.2, 11 of 11 for 4.3 — **every count in the notes above was understated**, corrected here), killed 18 of 19 mutants, verified the 4.1 observable independently (2 of 10 access-control rows named at `7a33756`, 9 of 10 at `3e71e9e`, the tenth stating its reason), confirmed `_python_route_access` changed no access level anywhere in the corpus and `.mjs` changed exactly one snapshot, and reproduced the fixture-difficulty artifact number for number including its failing arm. Remediated RED-first:
- **The meta-agent proposer still could not propose.** `MetaAgent(inner_model=...)` takes a `ModelConfig`; it was handed a model name, a `# type: ignore` hid that from mypy, and a bare `except Exception` turned the resulting `AttributeError` into the degradation reason `meta_agent_failed` — which reads exactly like the model declining. Every other HarnessX composition site in the repository builds the config properly. `_meta_agent_class` and `_model_config` are now seams a test can pin without the extra, and a `TypeError`/`AttributeError`/`ImportError` from the call is re-raised: "we called the agent wrong" is not "the agent had nothing to propose".
- **`ousast pairs --hunter` crashed in its documented form.** The K floor landed with `--k-runs` still defaulting to 1, so the flag's absence raised `ValueError` from inside `evaluate_catalog`. The default is `None` (three), and a breach exits naming the flag. The covering test always passed `--k-runs 4`, which is why nothing noticed.
- **28 rows resolved a span on the vulnerable side and none on the fixed side** (12% of the corpus), so every fixed-side finding read as outside the labeled function and the pair scored as silence whatever the detector said — the same flattering direction as the range defect, one-seventh the size. `fix_function` reached only the harvester; it now reaches the catalog and `PairCase`, `_spans` uses it for the fixed side, and the catalog generator derives it from the fixed excerpt itself for the 25 vibe-py trap rows that never declared one. A row that still resolves no fixed-side span reports `unresolved_label:fixed` instead of scoring silent. Measured after: **0 rows**.
- Suggestions applied: one spelling per corpus fact everywhere (`PairCase.unscorable` verbatim, no invented prefix); leave-one-out records non-teachers withheld for tier or an unscorable reason, not only holdout refusals; the hygiene test asserts the declared twins still hash equal, so a declaration that stopped being true would not be invisible; `is_post_cutoff` refuses a cutoff coarser than a day, which was the one over-claim path that existed, and month precision is pinned; `UNSCORABLE_REASONS` covers the whole vocabulary rather than `identical_twin` alone; the zero-run score carries its date; `_pairs_named_by` filters by substring before the regex.
- Carry-forward, measured 2026-09-06 after group 4 (`benchmarks/measurements/2026-09-06-trajectory-divergence.json`): **the K-run numbers this spec publishes are ensemble votes, not repeated measurements.** Across 120 runs of the injection family at temperature 0 with thinking disabled there was not one repeated tool-call trajectory, and outcome disagreement tracks trajectory divergence (30.9% at low similarity, 4.2% at mid). Consequences for anyone reading these numbers: Req 3.5's K is an ensemble over distinct search procedures; Req 8.2's noise floor measures how often an unconstrained search reaches the sink rather than the detector's uncertainty, so Req 9.5's regression budget grows with the search's own wandering; and the instability is entirely one-sided — every pair produced a single finding set on the fixed side, so specificity is solid and recall carries the caveat. `.kiro/specs/detector-optimizer/brief.md` proposes the repair, and it is a program change rather than a prompt change.
- Task 5.3 (2026-09-07, five rounds on `injection`, $2.17, `benchmarks/measurements/2026-09-07-evolve-rounds-injection.json`). The task was amended to target injection because access_control has one train teacher.
| round | lever | outcome | train | p | holdout | p | distinguishable from noise |
|---|---|---|---|---|---|---|---|
| 1 | memory | rejected `train_regressed` | 0+/3− | 0.250 | 1+/4− | 0.375 | no |
| 2 | checklist | rejected `train_regressed` | 0+/1− | 1.000 | 1+/1− | 1.000 | no |
| 3 | prompt | **accepted `improved` → v1** | 2+/0− | 0.500 | 2+/1− | 1.000 | no |
| 4 | meta-agent | rejected `no_proposal` | — | — | — | — | never ran |
| 5 | counterexample | rejected `train_regressed` | 0+/1− | 1.000 | 1+/3− | 0.625 | no |
**Nothing the loop decided carried evidence, in either direction — five rounds, zero reliable decisions.** Round 2 rejected a change because **one pair of eight** moved, at p = 1.000 — the maximum the sign test can return. Round 3 accepted one because two of eight improved and none regressed, at p = 0.500, and that acceptance is now versioned as v1 and will be built on. On a family whose measured floor says **11 of 20 pairs disagree with themselves between runs of the unchanged detector**, both readings are what changing nothing looks like.
I overstated this once and correct it here: after rounds 1 and 2 I wrote that the rule was "close to structurally incapable of accepting anything". Round 3 accepted. The accurate statement is symmetric and worse — the rule accepts on noise as readily as it rejects on it.
- **Escalation to the maintainer, not applied here.** Req 9.5's threshold rule (reject if either side regresses, accept if one improves and neither regresses, tie rejects) should be replaced by an acceptance that survives the measured variance. `Archive` already holds the per-pair Pareto frontier and `acceptance.reliability` already computes the test; neither decides anything. The honest caveat is that at a minibatch of 8 and a holdout of 10 a reliability-gated rule would accept almost nothing either: **the corpus is the binding constraint, not the rule**, and no acceptance rule fixes a train split of twelve.
- **The v1 configuration rests on p = 0.500** and should probably be reverted before any further round builds on it. Left in place, and flagged, because reverting an accepted round is the maintainer's call.
- Round 1's rejection was correct but for my error, not the loop's insight: the `memory` lever writes `hard_negatives.jsonl`, which the detector renders as *"Known non-issues, do not report these"*, and I fed it the four vulnerability shapes the train split teaches. Round 5 re-ran the same content through `counterexamples` (*"Worked examples"*), which is the lever I meant — and was rejected because **one pair of eight** moved, at p = 1.000. **The experiment I designed to give the demo lever a fair test could not give it one.** The rule cannot distinguish a lever that helps from a lever that does nothing, so correcting my authoring error changed the reason for the verdict and not the verdict.
- What did work: the cross-family sweep caught a genuine negative flip in `access_control` in round 2 — a real regression found by the before/after comparison that replaced the absolute-failure count, which under the old rule would have rejected rounds for families that were already failing.
- **The finding that outranks the rounds (maintainer, 2026-09-07: "the error is not the model, the error is the elsewhere").** Eighteen defects were found in this session's measurement and round work. Every one was in this repository; none was the model's. The list is kept because it should change how the next bad number is triaged: the prose parser that discarded findings the model had already reached; the CLI that never read the cost meter it published; the baseline that lost everything on an exception, and again on a timeout; two keys that collapsed a slice into another, in two different modules; the pattern ruleset at a 12.1% ceiling; a harvester that truncated an excerpt at both ends; a family shape that could not see a permissive default inside a call argument; a lever wired to the file that says *do not report these*; a provider seam hard-coded to a vendor with no key; a proposer reason the journal threw away; a retry that did not cover a body which stopped mid-read; an exception path wrapped around half a round; log-probabilities read from the wrong object; `.mjs` absent from the language table; and an access-level check that matched prose inside a decorator. Three of them were the same shape — fixing an instance and missing the class. **The default assumption for an unexplained number is that we broke it.** `deepseek-v4-pro` scoring 46.7% against flash's 83.3% is unexplained and is therefore, by this prior, ours until shown otherwise.
+67
View File
@@ -0,0 +1,67 @@
# Brief: MCP shift-left agent + skills
Status: brief / not yet specced. Proposed 2026-10-07. Delivery surface for the
qualified safety net; builds on the existing `ousast mcp` server and the product
skills. Related: `proof-arm-opencode` (the proof model), `pre-push-safety-net`
(the gate), `unseen-repo-evaluation` (the qualification).
## Motivation
The pre-push hook is the gate. Developers and their coding agents want the same
security feedback **earlier**, while the code is still being written, before
push. In an agent-driven workflow, a mandatory security check is how a human
oversees agents: the agent's output has to clear it before it ships.
Proposal: expose the safety net through an MCP agent and a small set of skills so
an editor or a coding agent can ask for a security read of the changed code on
demand, shift-left, and so a coding agent can be required to pass it before it
proposes a push.
## Shape
- Transport: extend the existing narrow MCP server (`ousast mcp`, stdio) that
OpenCode and other agents already speak. No new network surface; same
no-secret, bounded contract.
- What the agent offers:
1. Scan the current diff with the static arm (fast, no model, no cluster) and
return findings scoped to the change, with location and a concrete repair
direction, not a confidence label.
2. Triage a finding: explain it in plain terms and whether it is reachable.
3. Prove on demand: hand the top candidate to the proof arm when a runnable
harness exists, and return the demonstrated proof-of-concept.
- Skills: the existing `openultrasast-scan` and `openultrasast-triage`
product skills, plus shift-left additions (scan-the-diff, explain-a-finding,
suggest-a-fix-direction) that a coding agent invokes mid-task.
- Oversight mode: a coding agent can be configured to call the scan as a
required step before it finalizes a change, so security review is automatic
rather than remembered.
## Control (same model as the hook)
Advisory by default; blocking is opt-in. Budget and a push/call deadline bound
the work. The inline path runs the fast static arm only; heavy proof is explicit
and lane-selected. Models and spend caps stay in `.env`.
## Open questions for the spec
1. Inline latency budget: in-editor feedback must return in seconds, so the
static arm must be fast on a single diff. What is the time contract, and does
the large-repo latency problem block it?
2. Return contract: exactly what a coding agent gets back (findings, locations,
repair direction, optional proof) and how it decides pass/fail in oversight
mode. The return should be ticket-ready evidence a busy developer can paste
into the issue tracker to win capacity in a prioritization discussion with a
product owner, since a reproducible proof is what moves a security fix out of
the backlog.
3. Does oversight mode need a signed/attestable result so a reviewer can trust
that the agent actually ran the check?
4. Reuse: the `ousast mcp` tool set already exists; which tools are enough and
what is genuinely new.
## Scope and sequencing
This is a delivery and adoption surface, correctly **after** the safety net is
qualified on unseen repositories (G0 through G5). It is not a detection change;
it is how developers and agents consume the detection early. The proof model
improvement lives in `proof-arm-opencode`; this brief is the dev- and
agent-facing feedback channel.
@@ -0,0 +1,10 @@
{
"feature_name": "mcp-shift-left-agent",
"created_at": "2026-10-07T15:10:00Z",
"updated_at": "2026-10-07T15:10:00Z",
"language": "en",
"phase": "brief",
"approvals": {"requirements": {"generated": false, "approved": false}, "design": {"generated": false, "approved": false}, "tasks": {"generated": false, "approved": false}},
"ready_for_implementation": false,
"summary": "MCP agent + skills so developers and coding agents get the safety net's feedback early (shift-left, in-editor) and so a coding agent can be required to pass a security check before it ships. Delivery surface after G0-G5 qualification; builds on the existing ousast mcp server."
}
+61 -61
View File
@@ -7,7 +7,7 @@
> deterministically from code**; execution is a later, conditional tier. The correction is integrated below.
**Status:** course-setting. Supersedes `learning-harness` and `constrained-detector`. Not a requirements
document — a decision about what to build, what to delete, and what to adopt, written after the
document, a decision about what to build, what to delete, and what to adopt, written after the
learning-harness line produced numbers its own measurements say to discard.
## 1. The premise we got wrong
@@ -17,19 +17,19 @@ averaged (K runs), floored (per-family noise budget), gated (sign test), and slo
Every mechanism in `learning-harness` follows from that assumption. This session measured all of it and it
does not hold:
- Five evolve rounds, $2.17, **zero decisions distinguishable from chance** — because the acceptance rule
- Five evolve rounds, $2.17, **zero decisions distinguishable from chance**, because the acceptance rule
compares two ensemble votes over a detector that never runs the same investigation twice.
- 120 runs of one detector produced **120 distinct trajectories**. The variance is the loop's freedom to
search, not the model's uncertainty about an answer.
- The noise floor a regression budget is derived from measures *how often an unconstrained search happens
to reach the sink* — so the worse the search wanders, the more collateral damage a round is licensed to do.
to reach the sink*, so the worse the search wanders, the more collateral damage a round is licensed to do.
The assumption is also empirically false as a claim about LLMs, and the evidence is now overwhelming:
- **Google Big Sleep** (Nov 2024): the first LLM-discovered zero-day in widely-used real-world software — a
stack buffer underflow in SQLite — found by *variant analysis*, a sandboxed test case, and an
- Google Big Sleep (Nov 2024): the first LLM-discovered zero-day in widely-used real-world software, a
stack buffer underflow in SQLite, found by *variant analysis*, a sandboxed test case, and an
auto-generated root-cause report. Not pattern-matching; reproduction.
- **DARPA AIxCC finals** (DEF CON 33, Aug 2025): seven autonomous systems processed 54M lines of code,
- DARPA AIxCC finals (DEF CON 33, Aug 2025): seven autonomous systems processed 54M lines of code,
patched 43 of 54 synthetic vulnerabilities and **uncovered 18 previously-unknown real 0-days**. Buttercup
(Trail of Bits, 2nd place) found **28 vulnerabilities across 20 CWEs at 90% accuracy, $181 per point,
using non-reasoning LLMs only**, ~$152 per task, 45 minutes each.
@@ -47,11 +47,11 @@ Lazarus-AI, MIT, an open reproduction of Anthropic's Glasswing) share one shape:
> **The LLM is a judgement layer on top of a deterministic substrate that arbitrates its claims. Truth comes
> from that substrate, not from a score against a labeled corpus.**
In the memory-safety systems above the substrate is *execution* — a crash is the arbiter. But execution is
In the memory-safety systems above the substrate is *execution*. A crash is the arbiter. But execution is
only the arbiter you reach for when the target builds in isolation and the bug crashes, which is the C world,
not ours. The general form of the substrate is a **program model built from the code**: a deterministic
abstraction rich enough to *check* a claim — does the source actually reach the sink through resolved
dataflow; is there a guard on the path or isn't there; is the entry point attacker-reachable. Execution is one
abstraction rich enough to *check* a claim (does the source actually reach the sink through resolved
dataflow; is there a guard on the path or isn't there; is the entry point attacker-reachable). Execution is one
way to arbitrate; a model is the way that works for a SAST project and for bugs that never crash.
Clearwing's evidence ladder is the whole thesis in one type:
@@ -81,24 +81,24 @@ real. **We built K-runs and noise floors because we had no ground truth. They ge
This is not a total loss, and the course is not "throw everything away." Two results here are real,
measured, and *absent from Clearwing*:
1. **IR-driven candidate enumeration with a published ceiling.** We measured that the pattern ruleset caps
1. IR-driven candidate enumeration with a published ceiling. We measured that the pattern ruleset caps
recall at 12.1% while call-sites-and-bindings from the semantic IR reach **96.6%** across the web slices,
a median of ten sites per function. Clearwing's candidate feeder is C-first (taint tables for
`memcpy`-shaped bugs); its `web_framework` specialist is a *single unconstrained prompt* with no
equivalent candidate enumerator for logic/web classes.
2. **Absence-bug detection.** Our `obligations` work — an access-control bug is the *absence* of a guard
around an operation its siblings protect — has no analogue in Clearwing, whose ladder is built around a
2. Absence-bug detection. Our `obligations` work (an access-control bug is the *absence* of a guard
around an operation its siblings protect) has no analogue in Clearwing, whose ladder is built around a
*crash*. An IDOR or a missing authorization check never crashes a sandbox. Clearwing's execution
ground-truth model does not reach these classes at all, and they are exactly the "dangerous but
low-hanging" web bugs this project was chartered to catch.
So the gap Clearwing leaves open is **the web/logic taxonomy without a crash oracle** — which is where our
So the gap Clearwing leaves open is **the web/logic taxonomy without a crash oracle**, which is where our
IR candidate enumerator, our family taxonomy, and our obligation checker are genuinely ahead.
## 4b. The model layer, and the evidence ladder it produces
The arbiter is a program model constructed deterministically from the code — the thing an LLM claim is
The arbiter is a program model constructed deterministically from the code, the thing an LLM claim is
*checked against*, so that a finding's truth does not rest on the LLM that proposed it. We already hold the
first rungs of it; they were buried under a noise layer instead of being made the arbiter.
@@ -113,16 +113,16 @@ The evidence ladder is the model layer made legible. Each rung is a stronger, st
Two properties are the whole point:
- **`model_corroborated` and above are deterministic.** Run the checker twice over the same model and it
returns the same verdict. That removes the run-to-run noise the same way a crash would — no K-runs, no
floor, no sign test — *without needing a runnable target*. The noise this session measured was the LLM
- `model_corroborated` and above are deterministic. Run the checker twice over the same model and it
returns the same verdict. That removes the run-to-run noise the same way a crash would (no K-runs, no
floor, no sign test), *without needing a runnable target*. The noise this session measured was the LLM
free-searching; a model check does not search.
- **The LLM's job shrinks to the residual the model cannot decide** — is this string genuinely
attacker-controlled, is this guard semantically sufficient — and even that answer is checked against the
- The LLM's job shrinks to the residual the model cannot decide (is this string genuinely
attacker-controlled, is this guard semantically sufficient), and even that answer is checked against the
model rather than averaged over runs. The LLM stops being the oracle and becomes a proposer the model
audits.
**Measured, 2026-09-08 — the entailment ceiling** (`benchmarks/measurements/2026-09-08-model-entailment-ceiling.json`,
**Measured, 2026-09-08, the entailment ceiling** (`benchmarks/measurements/2026-09-08-model-entailment-ceiling.json`,
offline, no LLM):
| rung | web/logic taxonomy | for comparison |
@@ -134,17 +134,17 @@ offline, no LLM):
The gap between 96.6% (a site exists) and 2.2% (the model *decides* the vulnerability) is the gap between
enumerating and arbitrating, and it is the whole finding. The model cannot be a *pure* arbiter at v1. But it
is not a wall — the measurement diagnoses depth, not impossibility: one-hop source resolution (real taint sits
is not a wall. The measurement diagnoses depth, not impossibility: one-hop source resolution (real taint sits
1–3 binds upstream: `q = "…" + id; execute(q)`), a closed literal sink table (misses project wrappers and the
path/xss/prototype sinks), and the function-naming gap that eats 20/50 injection pairs as `no_function`.
Deepening the taint to reuse the candidate enumerator's depth-4 chain already moved injection 24% → 48%.
So the architecture the numbers support is a **layered arbiter**, not a pure one:
- where the model **entails** — report `model_entailed`, deterministic, the LLM not needed;
- where the model **corroborates a taint path** (~35% and rising) — the LLM judges, but its claim is *checked*
- where the model **entails**, report `model_entailed`, deterministic, the LLM not needed;
- where the model **corroborates a taint path** (~35% and rising), the LLM judges, but its claim is *checked*
against the resolved path, so run-to-run noise is removed on this band;
- where the model can only **enumerate** (the rest, up to 96.6%) — honest `suspicion`, the LLM proposes with
- where the model can only **enumerate** (the rest, up to 96.6%), honest `suspicion`, the LLM proposes with
no deterministic backstop, and this is exactly the band where the deferred execution / differential tier
earns its place.
@@ -152,32 +152,32 @@ Pure `model_entailed`-as-arbiter is the north star. The v1 is the layered arbite
named by the measurement: fix function naming, build per-family sink/sanitizer tables, and lift the flow
model onto the multi-hop binding chain we already have.
This is the classical SAST arbiter — dataflow / abstract interpretation, the CodeQL and Infer lineage —
This is the classical SAST arbiter (dataflow / abstract interpretation, the CodeQL and Infer lineage),
with the LLM supplying the semantic judgement the model is weakest at (aliasing intent, whether a value is
really untrusted, whether a guard really covers the case). It does not need soundness. A partial model that
can confirm *some* claims and honestly returns "could not confirm — stays suspicion" for the rest is already
can confirm *some* claims and honestly returns "could not confirm, stays suspicion" for the rest is already
the independent arbiter we never had.
## 4c. IL or AST, build or adopt — the substrate decision
## 4c. IL or AST, build or adopt, the substrate decision
**IL, and adopt one.** Both halves are forced by the entailment measurement.
*Why an IL, not the AST we have.* Our `semantic/ir.py` is not an IL — it is a lossy AST projection: per-function
*Why an IL, not the AST we have.* Our `semantic/ir.py` is a lossy AST projection rather than an IL: per-function
flat lists of calls and binds, no control-flow graph, no basic blocks, no data-dependence edges. That is
*why* entailment sits at 2.2%. Real abstract interpretation is a fixpoint over a **CFG**: guard *dominance*
(is the check on every path to the operation) is a dominator-tree question, and path-sensitive taint (a
sanitizer on the taint path specifically) needs the branch structure. A flat bind list cannot express either;
it can only over-approximate taint flow-insensitively by name, which is the one-hop ceiling we measured. The
model layer wants the classical IL that merges AST + CFG + PDG + call graph into one graph — a **Code Property
Graph (CPG)** — over which taint is a reachability query (source → sink minus sanitizer) and absence is a
model layer wants the classical IL that merges AST + CFG + PDG + call graph into one graph (a **Code Property
Graph (CPG)**), over which taint is a reachability query (source → sink minus sanitizer) and absence is a
dominance query.
*Why adopt, not build.* Building a multi-language CFG/PDG/taint engine on top of tree-sitter is precisely
reinventing CodeQL, Semgrep and Joern — a multi-year effort, and the maximal case of the failure mode this
reinventing CodeQL, Semgrep and Joern, a multi-year effort, and the maximal case of the failure mode this
session logged eighteen times. **Joern** is the adoptable answer: Apache-2.0, a CPG engine for exactly our
corpus (C/C++/Java/JavaScript/Python/Kotlin/binary), a built-in taint-propagation engine, embeddable, and
there is already a published CPG-plus-LLM bridge (Codebadger, an MCP server over Joern) and an arXiv line on
CPGs as the substrate for LLM program analysis. Joern *is* "a model built up from code" as a graph — the exact
CPGs as the substrate for LLM program analysis. Joern *is* "a model built up from code" as a graph, the exact
thing the maintainer described. The alternatives: CodeQL is more powerful but least adoptable (restrictive
license, query-only, GitHub-owned); Semgrep's taint engine is lighter and we already have integration
precedent through Clearwing's sidecar, but interprocedural taint is behind the paid Pro engine.
@@ -192,14 +192,14 @@ source ──▶ Joern: compile to CPG (one IL: AST + CFG + PDG + call graph, la
```
The CPG is language-agnostic once its frontend exists, so the taint and dominance logic is written *once*, not
per language — which is the whole reason our per-language tree-sitter approach kept hitting naming and grammar
per language, which is the whole reason our per-language tree-sitter approach kept hitting naming and grammar
gaps.
*What stays ours, and is the contribution.* Joern is a workbench, not an opinionated web/logic detector with
an LLM judge. What we own and port onto it: the closed **family taxonomy**; the **source / sink / sanitizer /
guard models** for the web/logic CWEs — our obligation facts and closed guard vocabulary become CPG queries
and taint specifications; and the **LLM-adjudication-checked-against-the-CPG** orchestration. That security
modelling — expressed as queries over an adopted CPG — is where our work is genuinely ahead of both Joern
guard models** for the web/logic CWEs (our obligation facts and closed guard vocabulary become CPG queries
and taint specifications); and the **LLM-adjudication-checked-against-the-CPG** orchestration. That security
modelling, expressed as queries over an adopted CPG, is where our work is ahead of both Joern
(no opinion on these classes) and Clearwing (crash-oracle, no arbiter for bugs that do not crash).
*The one real cost.* Joern is JVM/Scala; embedding it in a Python tool means its server mode or the MCP bridge,
@@ -212,38 +212,38 @@ noise architecture; keep execution as a later, conditional tier.**
Concretely, superseding both prior specs:
1. **Delete, do not fix.** The K-run machinery, the per-family noise floor, the regression budget, the sign
1. Delete, do not fix. The K-run machinery, the per-family noise floor, the regression budget, the sign
test, the evolve loop, the acceptance rule, the mechanism-lever optimizer. All of it existed to average an
LLM oracle we could not otherwise trust. A deterministic model arbiter removes the need for every piece.
`learning-harness` Req 3.5, 8.2, 9.5 and the evolve tasks are retired, not amended.
2. **Adopt a CPG engine (Joern) as the arbiter substrate; port our security models onto it.** We do not lift
2. Adopt a CPG engine (Joern) as the arbiter substrate; port our security models onto it. We do not lift
our flat IR to a CFG ourselves (§4c). The taxonomy, the source/sink/sanitizer/guard models, and the
obligation/absence logic move from prompt context to **CPG taint specifications and dominance queries** —
obligation/absence logic move from prompt context to **CPG taint specifications and dominance queries**,
deterministic `model_corroborated` and `model_entailed` verdicts over an engine that already has the CFG
and PDG our IR lacks. Our tree-sitter IR retains one job: cheap candidate enumeration for the `suspicion`
band where the CPG has no verdict.
3. **The LLM proposes; the model disposes.** One bounded, typed question per candidate — the constrained
detector we already started — but the answer is not scored against a corpus, it is *checked against the
model*. A claim the model corroborates advances; a claim it contradicts is dropped with the contradiction
3. The LLM proposes; the model disposes. One bounded, typed question per candidate (the constrained
detector we already started), with the answer *checked against the
model* rather than scored against a corpus. A claim the model corroborates advances; a claim it contradicts is dropped with the contradiction
recorded; a claim it can neither confirm nor deny stays `suspicion` and is reported as such. No K-runs, no
evolve loop.
4. **Corpus is the model's calibration set, not the LLM's training signal.** Keep the vulnerable-parent /
fix-commit pairs — but their role changes. The pair is ground truth for *the model's precision*: the fix
4. Corpus is the model's calibration set, not the LLM's training signal. Keep the vulnerable-parent /
fix-commit pairs, but their role changes. The pair is ground truth for *the model's precision*: the fix
is what the model should be able to see (a guard added, a sanitizer inserted, a bound checked), and a pair
the model cannot distinguish is a gap in the model, named and measured, not a detector miss to average.
Harvest more pairs at repository scale from CVE history, as Clearwing does; truth is the fix diff read by
the model, not a label we hand-assign.
5. **Execution is a deferred, conditional tier.** For the classes a static model genuinely cannot arbitrate —
memory-safety-in-C, where reachability and exploitability turn on runtime layout — a sandbox crash oracle
5. Execution is a deferred, conditional tier. For the classes a static model genuinely cannot arbitrate
(memory-safety-in-C, where reachability and exploitability turn on runtime layout), a sandbox crash oracle
is the right arbiter, and Clearwing already is that system. Adopt it *there*, later, only when a class
needs it. It is the `execution_confirmed` rung, not the foundation.
6. **Reporting keeps the honesty disciplines that stand:** per-slice numbers with the real-world slice
deciding, the overfitting gap published, and the evidence rung on every finding — now meaning a model
6. Reporting keeps the honesty disciplines that stand: per-slice numbers with the real-world slice
deciding, the overfitting gap published, and the evidence rung on every finding, now meaning a model
verdict, not a sandbox result.
## 6. Build vs. adopt, revised
@@ -252,14 +252,14 @@ The earlier draft of this brief over-rotated to "adopt Clearwing wholesale," bec
foregrounds execution. The maintainer's correction re-centres it, and the honest split is cleaner than either
extreme:
- **The model layer is adopt-plus-contribute, not build.** The arbiter *engine* is Joern's CPG (Apache-2.0,
our languages, taint + dominance built in); the *contribution* is our security modelling on top of it — the
- The model layer is adopt-plus-contribute, not build. The arbiter *engine* is Joern's CPG (Apache-2.0,
our languages, taint + dominance built in); the *contribution* is our security modelling on top of it, the
taxonomy and the source/sink/sanitizer/guard specs for the web/logic classes, plus the LLM adjudication
checked against the CPG. The entailment measurement (2.2% on our flat IR) is what rules out building the
engine ourselves: that number is the distance between our AST projection and a real CPG, and closing it by
hand is reimplementing Joern.
- **The execution tier is Clearwing's to adopt**, later, for the memory-safety classes where it is strong and
we are weak — via the fork the maintainer already maintains. We do not reimplement its sandbox lifecycle,
- The execution tier is Clearwing's to adopt, later, for the memory-safety classes where it is strong and
we are weak, via the fork the maintainer already maintains. We do not reimplement its sandbox lifecycle,
container pooling, sanitizer images or PoC stability; those are exactly the parts this session proved we get
wrong.
@@ -268,24 +268,24 @@ arbiter we cannot, when a class demands it.
## Open questions for the requirements phase
- **How far up the ladder can a partial model reach without soundness?** `model_corroborated` needs only a
- How far up the ladder can a partial model reach without soundness? `model_corroborated` needs only a
consistency check; `model_entailed` needs a real interprocedural dataflow/guard-dominance result. What
fraction of our corpus can the model *entail* today, and where does it honestly fall back to `suspicion`?
This is the first measurement to run — the model analogue of the 96.6% candidate ceiling — and it can end
This is the first measurement to run (the model analogue of the 96.6% candidate ceiling), and it can end
or reshape the design before any LLM cost, exactly as the ceiling did.
- **What is the model's arbiter for an absence bug?** A missing guard has no dataflow path to confirm. The
- What is the model's arbiter for an absence bug? A missing guard has no dataflow path to confirm. The
obligation/sibling-dominance model is the candidate arbiter; does it distinguish the vulnerable-parent from
the fixed twin across the corpus on its own, deterministically?
- **Where does the LLM's residual judgement resist checking?** Some claims — "this value is attacker-
controlled by convention", "this guard is semantically insufficient" — the model cannot audit. How large is
- Where does the LLM's residual judgement resist checking? Some claims ("this value is attacker-
controlled by convention", "this guard is semantically insufficient") the model cannot audit. How large is
that residual, and is `suspicion` an acceptable home for it or does it need the execution tier sooner?
- **Which classes cross into execution, and when.** Memory-safety-in-C is the clear case for the deferred
- Which classes cross into execution, and when. Memory-safety-in-C is the clear case for the deferred
tier. Is anything in the web/logic taxonomy genuinely un-arbitrable by a model, forcing the differential
request oracle earlier than "later, if needed"?
- **What of the existing corpus survives** as the model's calibration set, given the twin/straddle/tier
- What of the existing corpus survives as the model's calibration set, given the twin/straddle/tier
defects the learning-harness reviews surfaced?
Sources: [Big Sleep / SQLite zero-day](https://thehackernews.com/2024/11/googles-ai-tool-big-sleep-finds-zero.html) ·
[AIxCC finals — Buttercup, 28 vulns / 20 CWEs / non-reasoning LLMs](https://blog.trailofbits.com/2025/08/09/trail-of-bits-buttercup-wins-2nd-place-in-aixcc-challenge/) ·
[AIxCC results — 18 real 0-days, 43/54 patched](https://semiengineering.com/aixcc-2025-what-it-means-for-device-security/) ·
[AIxCC finals, Buttercup, 28 vulns / 20 CWEs / non-reasoning LLMs](https://blog.trailofbits.com/2025/08/09/trail-of-bits-buttercup-wins-2nd-place-in-aixcc-challenge/) ·
[AIxCC results, 18 real 0-days, 43/54 patched](https://semiengineering.com/aixcc-2025-what-it-means-for-device-security/) ·
[Clearwing](https://github.com/Lazarus-AI/clearwing) · local: `/home/mc/Source/clearwing`, fork `norandom/deepwing`.
+20 -20
View File
@@ -4,14 +4,14 @@
The detector stops being a noisy oracle and becomes a proposer that a deterministic program model audits. The
model is a **Code Property Graph** (Joern, Apache-2.0): one graph merging AST, CFG, PDG and call graph, over
which taint reachability and guard dominance produce verdicts on the evidence ladder — `suspicion`,
which taint reachability and guard dominance produce verdicts on the evidence ladder, `suspicion`,
`model_corroborated`, `model_entailed`, `execution_confirmed`. The LLM is asked one bounded question per
candidate; its answer is checked against the CPG, not averaged over runs.
Two disciplines shape every decision below. **Subtraction first:** the noise architecture and the outgrown
test surface are removed and audited before the CPG arbiter is adopted, because the session that produced this
spec logged eighteen defects building on an overgrown surface. **Adopt, don't build:** the CFG/PDG our flat IR
lacks comes from Joern, not from a hand-built engine — the 2.2% entailment ceiling is the measured distance
lacks comes from Joern, not from a hand-built engine. The 2.2% entailment ceiling is the measured distance
between our AST projection and a real CPG, and closing it by hand is reimplementing Joern.
### Goals
@@ -19,12 +19,12 @@ between our AST projection and a real CPG, and closing it by hand is reimplement
- Remove the noise architecture and reduce the test suite with it, gate-protected and reversible.
- A deterministic model verdict on the band the model covers; honest `suspicion` on the rest.
- The CPG engine, the LLM endpoint and any execution tier are optional; the core stays zero-dependency.
- The security modelling — taxonomy, source/sink/sanitizer/guard specs — is ours, expressed as CPG queries.
- The security modelling (taxonomy, source/sink/sanitizer/guard specs) is ours, expressed as CPG queries.
### Non-Goals
- Building a control-flow or dataflow engine (adopted, Req 4). Prompt optimisation, evolve loops, acceptance
rules (deleted, Req 8.5). The execution tier itself (deferred to Clearwing, Req 10). Soundness — the
rules (deleted, Req 8.5). The execution tier itself (deferred to Clearwing, Req 10). Soundness, the
north-star `model_entailed`-as-pure-arbiter is not v1.
## Boundary Commitments
@@ -32,18 +32,18 @@ between our AST projection and a real CPG, and closing it by hand is reimplement
### This Spec Owns
- The subtraction: which modules are removed, the dangling-import check, the module-audit manifest.
- `cpg/` — the Joern seam (`CpgBackend`, the subprocess/query implementation, the null backend).
- `model/` — `Verdict`/`Rung`, taint reachability, guard dominance, constant abstraction; the security specs
- `cpg/`, the Joern seam (`CpgBackend`, the subprocess/query implementation, the null backend).
- `model/`, `Verdict`/`Rung`, taint reachability, guard dominance, constant abstraction; the security specs
ported from the deleted modules' vocabularies.
- The proposer/judge orchestration where the LLM's claim is checked against the model.
- The corpus-as-calibration harness and its model-gap report.
### Out of Boundary
- The family **taxonomy** (`learning/families.py`) — retained unchanged, re-homed under `model/`.
- The corpus loader, per-slice split and gates — retained; the gates stay byte-identical.
- Redaction, the LLM endpoint client (`learning/endpoint.py`) — retained.
- Clearwing's sandbox lifecycle — never reimplemented; the execution tier adopts it.
- The family **taxonomy** (`learning/families.py`), retained unchanged, re-homed under `model/`.
- The corpus loader, per-slice split and gates, retained; the gates stay byte-identical.
- Redaction, the LLM endpoint client (`learning/endpoint.py`), retained.
- Clearwing's sandbox lifecycle, never reimplemented; the execution tier adopts it.
### Allowed Dependencies
@@ -63,7 +63,7 @@ between our AST projection and a real CPG, and closing it by hand is reimplement
### Existing Architecture Analysis
The `learning/` package (4,091 lines, 16 modules) implements the noise architecture and is **absent from the
gate path** — the gates import only from `pairs`, `benchmark`, `findings`, `mapping`, `preprocess`, `rank`,
gate path**. The gates import only from `pairs`, `benchmark`, `findings`, `mapping`, `preprocess`, `rank`,
`complexity.map`. So the subtraction is clean: removing `rounds`, `acceptance`, `proposer`, `journal`, the
noise machinery in `scoring`, and the mechanism loop cannot move a gate. What survives and re-homes: the
taxonomy (`families.py`), the candidate enumerator (`candidates.py`, the `suspicion`-band feeder), the LLM
@@ -140,7 +140,7 @@ src/openultrasast/model/
- `learning/candidates.py` → `model/candidates.py` (the suspicion-band enumerator, Req 8.4).
- `learning/endpoint.py` → `model/endpoint.py` (the judge client).
- `semantic/obligations/` retained; its operation/discharge facts feed `model/dominance.py`.
- `pairs.py`, the gates, redaction — unchanged.
- `pairs.py`, the gates, redaction, unchanged.
### The audit manifest (Req 3)
@@ -288,21 +288,21 @@ is dominated by no discharger and its siblings are, else `CORROBORATED`/`None`.
## Migration and Phases
1. **Subtract and audit (Req 1–3).** Remove the noise architecture; port the guard/sink vocabularies into
1. Subtract and audit (Req 1–3). Remove the noise architecture; port the guard/sink vocabularies into
`model/specs.py` before deleting their modules; run the import-graph check and the gate-baseline; reduce
the tests in the same change; commit the audit manifest. Gate-protected and reversible; no CPG yet.
2. **Joern spike on one family (Req 4, 6.1).** The `CpgBackend` seam and the injection `TaintSpec`, proven
2. Joern spike on one family (Req 4, 6.1). The `CpgBackend` seam and the injection `TaintSpec`, proven
end to end on the injection slice: build a CPG, run the taint query, produce entailed/corroborated
verdicts, assert determinism. This phase can stop the design if Joern cannot deliver a deterministic
verdict at acceptable cost — the analogue of the candidate-ceiling gate.
3. **The ladder and the judge (Req 5, 8).** `Verdict`/`Rung`, and `judge.py` checking the LLM's residual
verdict at acceptable cost, the analogue of the candidate-ceiling gate.
3. The ladder and the judge (Req 5, 8). `Verdict`/`Rung`, and `judge.py` checking the LLM's residual
answer against the verdict. No K-runs, nothing to average.
4. **Absence and the rest of the taxonomy (Req 6.2, 7).** Dominance queries from the obligation facts; the
4. Absence and the rest of the taxonomy (Req 6.2, 7). Dominance queries from the obligation facts; the
remaining `TaintSpec`s. Regenerate the entailment ceiling (Req 6.5) and watch it rise as depth improves.
5. **Corpus calibration and reporting (Req 9, 11).** The vulnerable-parent/fix differential, the model-gap
5. Corpus calibration and reporting (Req 9, 11). The vulnerable-parent/fix differential, the model-gap
report, per-slice + overfitting gap.
6. **Execution tier stub (Req 10).** The `execution_confirmed` rung and the Clearwing adoption seam, built
only when a class needs it — memory/C first, and not for web/logic v1.
6. Execution tier stub (Req 10). The `execution_confirmed` rung and the Clearwing adoption seam, built
only when a class needs it, memory/C first, and not for web/logic v1.
Phase 1 removes ~5,000 lines before a line of Joern is written. Phase 2 can end the design before the
integration cost is paid, exactly as the candidate ceiling could have in the prior spec.
@@ -6,12 +6,12 @@
This feature supersedes `learning-harness` and `constrained-detector`. It replaces the assumption that the LLM
is a noisy oracle to be averaged, floored and gated with an architecture in which a **deterministic program
model arbitrates the LLM's claims**. The arbiter is a Code Property Graph (CPG) — an intermediate language
merging AST, control-flow graph, program-dependence graph and call graph — over which taint reachability and
model arbitrates the LLM's claims**. The arbiter is a Code Property Graph (CPG), an intermediate language
merging AST, control-flow graph, program-dependence graph and call graph, over which taint reachability and
guard dominance produce verdicts the LLM's assertion is no longer needed for. The measurements that force this
design are committed: the candidate ceiling (96.6% of labeled functions have an enumerable site), and the
entailment ceiling (a deterministic model *decides* only 2.2% today on our flat AST-projection IR, ~24-35%
with deeper taint — the distance to a real CPG). Context: `brief.md`,
with deeper taint, the distance to a real CPG). Context: `brief.md`,
`benchmarks/measurements/2026-09-06-trajectory-divergence.json`, `2026-09-06-candidate-ceiling-all-slices.json`,
`2026-09-08-model-entailment-ceiling.json`.
@@ -22,15 +22,15 @@ overgrown, superseded surface is how this session logged eighteen defects.
## Boundary Context
- **In scope**: removing the noise architecture and the superseded arbiter; reducing the test suite to
- In scope: removing the noise architecture and the superseded arbiter; reducing the test suite to
surviving behavior; auditing the whole tree for orphaned modules; adopting a CPG engine (Joern) as the
model-layer substrate behind an optional extra; the evidence ladder as the verdict type; taint + dominance
verdicts; porting our taxonomy and source/sink/sanitizer/guard models to the CPG; the LLM as a proposer
checked against the model; the corpus as the model's calibration set; per-slice reporting.
- **Out of scope**: building a dataflow/CFG engine by hand (the CPG is adopted, §Req 4); the execution tier
- Out of scope: building a dataflow/CFG engine by hand (the CPG is adopted, §Req 4); the execution tier
itself (deferred to Clearwing, `execution_confirmed` rung only, Req 10); prompt optimisation, reflective
evolution, any optimiser (deleted, not deferred); fine-tuning weights.
- **North star, not v1**: pure `model_entailed`-as-arbiter. The entailment ceiling says the model decides a
- North star, not v1: pure `model_entailed`-as-arbiter. The entailment ceiling says the model decides a
minority today; v1 is the *layered* arbiter (entail where it can, corroborate the taint band, honest
suspicion for the rest). This is stated so the requirements are not read as promising a soundness the
measurement denies.
@@ -45,12 +45,12 @@ so that nothing is maintained, tested, or built upon that this feature's measure
#### Acceptance Criteria
1. The system shall remove the round loop, the per-family noise floor, the regression budget, the acceptance
rule, the sign test, the evolve journal and the meta-agent proposer — concretely `learning/rounds.py`,
rule, the sign test, the evolve journal and the meta-agent proposer, concretely `learning/rounds.py`,
`learning/acceptance.py`, `learning/proposer.py`, `learning/journal.py`, and the K-run / floor / sign-test
machinery in `learning/scoring.py`.
2. The system shall remove the mechanism-optimisation loop — the mechanism store and lever in
2. The system shall remove the mechanism-optimisation loop (the mechanism store and lever in
`improve/evolve.py`, and the shape-search machinery in `semantic/mechanisms.py` and the optimisation
surface of `semantic/variants.py` — while preserving the *security models* those modules carried (the
surface of `semantic/variants.py`), while preserving the *security models* those modules carried (the
guard vocabulary and source/sink classification) for porting under Req 7.
2b. The system shall remove the per-family evolvable detector configuration and its canary/verifier surface
(`learning/detectors.py` config-evolution parts, `learning/canaries.py`, `learning/verifiers.py`,
@@ -70,7 +70,7 @@ ratchet that pins deleted mechanisms and the suite is once again a description o
1. When a mechanism is removed under Req 1, the tests that existed only to pin its behavior shall be removed
with it, in the same change, not left skipped or xfailed.
2. Every surviving test shall map to a surviving requirement — of this spec or of a still-shipping feature —
2. Every surviving test shall map to a surviving requirement (of this spec or of a still-shipping feature)
and a test that maps to nothing shall be removed.
3. The reduced suite shall be smaller than the current 832 tests, and the change shall report the count before
and after with the requirement each removed test cluster served.
@@ -78,7 +78,7 @@ ratchet that pins deleted mechanisms and the suite is once again a description o
determinism test (Req 6.4), the per-slice honesty tests, and the two ceiling measurements' regeneration
tests.
5. A test that asserts a shape "does nothing" or greps `--help`, or that passes with its capability absent,
shall be removed rather than kept — the session logged several; none survive.
shall be removed rather than kept. The session logged several; none survive.
### Requirement 3: Audit the whole tree for outgrowth
@@ -89,7 +89,7 @@ found and removed rather than carried indefinitely.
1. The system shall classify every source module as one of: load-bearing for the model-grounded architecture;
a retained standalone capability with its own users; or orphaned.
2. If a module is orphaned — no importer, no CLI entry, no gate dependency, no surviving requirement — then the
2. If a module is orphaned (no importer, no CLI entry, no gate dependency, no surviving requirement), then the
system shall remove it and record the classification.
3. The system shall name, for the audit, the older-phase subsystems whose status is not obvious
(`fusion.py`, `mcp.py`, `skills.py`, `harness_ext.py`, `hunter_harness.py`) and give each an explicit
@@ -122,7 +122,7 @@ model result is never confused with an LLM suspicion.
#### Acceptance Criteria
1. The system shall assign every finding one rung: `suspicion` (a site was enumerated / the LLM flagged it),
`model_corroborated` (the LLM's claim is consistent with the CPG — the source it names reaches the sink
`model_corroborated` (the LLM's claim is consistent with the CPG, the source it names reaches the sink
through resolved dataflow, or a guard its siblings carry is absent), `model_entailed` (the CPG alone
establishes it, independent of trusting the LLM), or `execution_confirmed` (a sandbox reproduced it).
2. The system shall establish `model_corroborated` and `model_entailed` from the CPG deterministically, with
@@ -145,21 +145,21 @@ project measured is removed on the band the model covers.
4. When the system runs the model layer twice over the same CPG, the sequence of verdicts shall be identical,
and a test shall assert it.
5. The system shall report, per family and per slice, the fraction of the corpus it can `model_entail` and
`model_corroborate` — the regeneration of the committed entailment ceiling — and shall name where it falls
`model_corroborate` (the regeneration of the committed entailment ceiling), and shall name where it falls
back to `suspicion`.
### Requirement 7: Port the taxonomy and security models onto the CPG
**Objective:** As a maintainer, I want our security knowledge — the classes we target and how their bugs look
— expressed as queries over the adopted CPG, so that the contribution is the modelling, not the engine.
**Objective:** As a maintainer, I want our security knowledge (the classes we target and how their bugs look)
expressed as queries over the adopted CPG, so that the contribution is the modelling, not the engine.
#### Acceptance Criteria
1. The system shall retain the closed family taxonomy as the routing key, unchanged in meaning.
2. The system shall express the source, sink and sanitizer models for the web/logic CWEs as CPG taint
specifications, seeded from the models the deleted modules carried.
3. The system shall express the obligation / absence model — an operation on a protected resource, and the
guards that discharge it — as CPG dominance queries, as the arbiter for the classes that never crash.
3. The system shall express the obligation / absence model (an operation on a protected resource, and the
guards that discharge it) as CPG dominance queries, as the arbiter for the classes that never crash.
4. The system shall keep the verifier boundary: the models and gold labels stay outside anything an LLM or a
future optimiser may write.
@@ -177,7 +177,7 @@ with its claim checked against the model, so that there is no oracle to average.
`model_corroborated`.
4. When the model can neither confirm nor contradict, the system shall report the claim at `suspicion`, and
this band is where a future execution tier earns its place.
5. The system shall contain no K-run averaging, no noise floor, no acceptance rule, no evolve loop — the model
5. The system shall contain no K-run averaging, no noise floor, no acceptance rule, no evolve loop. The model
check replaces all of them.
### Requirement 9: The corpus is the model's calibration set
@@ -188,7 +188,7 @@ vote, so that a gap is a gap in the model, named and fixable.
#### Acceptance Criteria
1. The system shall use the vulnerable-parent / fix-commit pair as ground truth for whether the model can
distinguish the two — the fix is what the model should be able to see.
distinguish the two. The fix is what the model should be able to see.
2. If the model cannot distinguish a pair's two sides, then the system shall record it as a named model gap
with the reason, not as a detector miss to be averaged away.
3. The system shall keep the per-slice split with the real-world slices deciding, and shall not present the
@@ -204,7 +204,7 @@ arbitrate, without building or requiring it at v1.
#### Acceptance Criteria
1. The system shall treat `execution_confirmed` as an optional rung reached only for classes where the model
layer cannot decide — memory-safety in C first.
layer cannot decide, memory-safety in C first.
2. When execution confirmation is used, the system shall adopt it from Clearwing (the existing fork), not
reimplement the sandbox lifecycle, container pooling, sanitizer images or PoC stability.
3. The system shall not require the execution tier for any web/logic class at v1.
@@ -1,325 +0,0 @@
# Assessment: OpenUltraSAST Kickoff
## Executive Assessment
OpenUltraSAST should be built as a harness runtime specialized for security analysis, not as a direct Clearwing clone. The strongest design is to treat Clearwing's sourcehunt pipeline as a proven reference flow, HarnessX as the runtime abstraction, OpenUltraCode as the verification and fusion discipline, OpenRouter as the provider plane, and Trail of Bits skills as targeted domain expertise.
The minimum viable system is not deep fuzzing. The minimum viable system is an auditable quick scan that can rank files, generate structured findings, eliminate obvious false positives, enforce evidence levels, and emit reports. Deep Docker-backed validation should come after the evidence model, trace model, false-positive loop, and report model are stable.
## Why This Project Is Viable
Clearwing demonstrates that a SAST + LLM hybrid can be useful when it avoids treating model output as truth. Its best transferable ideas are:
- Static preprocessing before LLM analysis.
- File ranking by surface, influence, and reachability.
- Tiered hunter budgets instead of equal attention for every file.
- Independent adversarial verification.
- Evidence ladder that separates suspicion from crash reproduction and patch validation.
- Docker isolation for builds, fuzzing, PoCs, and patches.
- SARIF, Markdown, and JSON artifacts.
The most important product differentiator should be false-positive elimination. A scanner that finds many plausible issues but cannot separate evidence-backed findings from model hallucinations will not be trusted. Ranking, retrieval, verification, and HarnessX-style adjustment should all be optimized around reducing false positives without hiding the audit trail.
HarnessX provides the missing generalization: the scanner should be a composed harness with typed processors and traces, not a fixed pipeline that becomes hard to evolve. This matters because SAST workflows vary heavily by language, build system, vulnerability class, and available evidence.
OpenUltraCode provides the missing discipline: high-risk findings and proposed fixes need bounded plans, adversarial review, reconciliation, explicit model disclosure, and verification gates. This is directly applicable to false-positive control and to avoiding fixes that look plausible but introduce regressions.
Trail of Bits skills provide mapping discipline as much as they provide tools. CodeQL thinking maps interprocedural flows, Semgrep maps repeatable patterns and variants, differential review maps changed attack surface, and sharp-edge review maps APIs and defaults that make misuse likely. OpenUltraSAST should convert those disciplines into structured tasks and evidence, not only prompt inspiration.
## Recommended Initial Product Shape
The first release should support this workflow:
```text
ousast scan /path/to/repo --mode quick
```
The first realistic quality milestone should add benchmark-grounded evaluation before claiming language coverage:
```text
ousast benchmark benchmarks/javascript-node.toml --mode standard
ousast benchmark benchmarks/python-web.toml --mode standard
ousast benchmark benchmarks/java-web.toml --mode standard
ousast benchmark benchmarks/c-cpp.toml --mode quick
```
It should produce:
```text
.openultrasast/runs/<scan-id>/manifest.json
.openultrasast/runs/<scan-id>/preprocess/file_targets.json
.openultrasast/runs/<scan-id>/rank/ranking.json
.openultrasast/runs/<scan-id>/findings.json
.openultrasast/runs/<scan-id>/report.md
```
The key product promise should be:
```text
OpenUltraSAST tells you where security review should start, what it suspects, what evidence exists, what evidence is missing, and which findings have survived independent verification.
```
For coder education, the complementary promise should be:
```text
OpenUltraSAST explains why the pattern is dangerous, what evidence proves or weakens the finding, how to fix it safely, and how to avoid recreating the same bug class.
```
## Core Architecture Recommendation
Use a Python implementation initially. Clearwing and HarnessX are both Python, Docker and SARIF libraries are mature, and Trail of Bits tooling integration will be simpler. OpenCode integration can happen through MCP and command files rather than requiring the core scanner to be TypeScript.
Recommended modules:
```text
openultrasast/config.py
openultrasast/cli.py
openultrasast/models.py
openultrasast/provider/openrouter.py
openultrasast/preprocess/
openultrasast/index/
openultrasast/rank/
openultrasast/harness/
openultrasast/hunters/
openultrasast/verify/
openultrasast/fusion/
openultrasast/sandbox/
openultrasast/reports/
openultrasast/mcp/
```
## Build vs. Reuse Decisions
| Area | Recommendation | Reason |
| --- | --- | --- |
| Harness runtime | Build minimal HarnessX-inspired core first | Direct HarnessX dependency may add unstable surface before needs are proven |
| OpenRouter chat | Build direct OpenAI-compatible client | Need explicit model IDs, cost metadata, and role controls |
| OpenRouter embeddings | Build direct client | Embeddings are central and should not be hidden behind chat abstractions |
| Vector store | Choose one local default | Avoid abstracting before retrieval needs are proven |
| Docker sandbox | Build focused wrapper | Security defaults are project-specific |
| Trail of Bits skills | Index descriptors, mapping tasks, and snippets | Avoid prompt bloat while preserving CodeQL/Semgrep/differential/sharp-edge discipline |
| Clearwing | Reference only | Independence is a project requirement |
## Configuration Shape
Model names and the vector store should be selected before implementation. The config shape should look like this:
```toml
[models]
ranker = "<openrouter-ranker-model>"
hunter = "<openrouter-hunter-model>"
verifier = "<openrouter-verifier-model>"
patcher = "<openrouter-patcher-model>"
[embeddings]
model = "<openrouter-embedding-model>"
store = "<selected-local-vector-store>"
[sandbox]
network = false
workspace_readonly = true
memory_mb = 2048
timeout_seconds = 300
pids_limit = 512
[dynamic]
enabled = false
network_scope = []
[evidence]
minimum_report_verified = "static_corroboration"
minimum_exploit = "crash_reproduced"
minimum_patch = "root_cause_explained"
```
The actual model names should be explicit before implementation. Avoid baking in placeholder defaults that silently pick a moving target.
## Vector Store Decision
Choosing the vector store is a core design decision, not plumbing. It directly affects ranking quality and false-positive rates because poor retrieval contaminates hunter and verifier context.
Selection criteria:
- Strong metadata filters for path, language, symbol, repository snapshot, evidence level, and vulnerability class.
- Incremental indexing for changed files.
- Local persistence that works inside opencode workflows.
- Deterministic export/import for reproducible scan artifacts.
- Acceptable query latency on medium repositories.
- Clean storage of OpenRouter embedding model metadata.
- Ability to combine vector retrieval with lexical or structural filters.
The first milestone should include a short bakeoff on fixtures before locking the default store.
## Most Important Engineering Constraint
Evidence transitions must be enforced in code. A model can recommend a transition, but the harness should decide whether the required artifact exists.
Examples:
- `suspicion -> static_corroboration`: requires static evidence, independent verifier agreement, or a matching analyzer finding.
- `static_corroboration -> crash_reproduced`: requires stored crash/reproducer artifact.
- `crash_reproduced -> root_cause_explained`: requires verifier explanation tying crash to source and impact.
- `root_cause_explained -> exploit_demonstrated`: requires exploit or trigger artifact beyond accidental crash.
- `root_cause_explained -> patch_validated`: requires patch diff and passing validation command.
False-positive elimination should be handled the same way: a model can recommend demotion or suppression, but the harness should require a scoped reason and counter-evidence.
Benchmark grounding should be handled with the same discipline. A scanner that passes tiny fixtures but misses NodeGoat, PyGoat, OWASP Benchmark/WebGoat, Juliet, or small sanitizer-backed parser targets is not a realistic finder. Language support should be claimed only after benchmark runs show useful recall and auditable false-positive behavior on realistic vulnerable projects.
False-positive reasons should include:
- No attacker-controlled path.
- Code is unreachable in supported configuration.
- Static rule matched a safe wrapper or sanitizer.
- Model confused similar symbols or files.
- Finding duplicates an existing issue.
- Impact is not security-relevant.
- Dynamic probe or test disproved triggerability.
## OpenRouter Design Needs
OpenRouter should be treated as two capabilities:
1. Chat/completion routing for reasoning roles.
2. Embedding endpoint for retrieval.
Every model call should record:
```text
role
model_id
provider metadata
prompt hash
response hash
cost estimate
temperature and parameters
fallback/degradation status
```
The project should reject ambiguous model routing for reproducible scans. If a role uses auto routing, the manifest must record the actual model selected by OpenRouter if available.
## Docker Design Needs
Docker is not just convenience. It is the trust boundary between arbitrary target repositories and the user's host.
Minimum safety profile:
- No hidden network in analysis containers.
- Read-only source mount.
- Writable scratch only.
- Timeouts around every command.
- Memory, CPU, and pids limits.
- Dropped capabilities by default.
- Separate writable copy for patch validation.
Deep mode should not be implemented until this boundary exists.
## Dynamic Analysis Strategy
Network verification is not out of scope. It is just not the core idea of the harness. OpenUltraSAST should remain SAST-first, but it should use dynamic analysis when runtime evidence is the right way to confirm a finding.
Useful dynamic checks include:
- Starting a target service in a sandbox.
- Using netcat-style TCP probes against declared local ports.
- Running local HTTP or protocol smoke tests.
- Executing a bounded PoC against a sandboxed service.
- Using Clearwing-style network checks as validation patterns when a source finding has a runtime network surface.
The rule is not "no network ever." The rule is "no hidden or open-ended network activity." Dynamic probes must be explicit, scoped, logged, time-bounded, and tied to evidence advancement.
## Trail Of Bits Skill Strategy
The skills are best used as a routing corpus and checklist library. The harness should select compact snippets by target profile.
Good examples:
- C parser with pointer arithmetic: `c-review`, `address-sanitizer`, `libfuzzer`, `harness-writing`.
- Python parser: `atheris`, `property-based-testing`.
- Crypto code: `constant-time-analysis`, `wycheproof`, `vector-forge`, `zeroize-audit`.
- SARIF flow: `sarif-parsing`, `semgrep`, `codeql`.
- Changed code or proposed fixes: `differential-review`, `graph-evolution`.
- Dangerous APIs, defaults, or configuration: `sharp-edges`, `insecure-defaults`.
- Smart contracts: use chain-specific scanners only when the language and framework match.
Bad pattern:
```text
Paste all security skill instructions into every hunter prompt.
```
That would increase cost, reduce precision, and mix irrelevant assumptions into reviews.
## Fix Authoring And Audit Strategy
UltraCode should define the patch lifecycle. A patch is not complete when a model emits a diff. A patch is complete only after a bounded plan, minimal implementation, adversarial review, reconciliation, and fresh verification.
Fix stages:
```text
verified finding -> bounded fix plan -> minimal patch -> sandbox validation -> adversarial fix audit -> reconciliation -> fresh verification -> patch artifact
```
Trail of Bits disciplines should audit the fix:
- Differential review checks whether the patch changed more attack surface than intended.
- Semgrep checks whether the vulnerable pattern or variants remain.
- CodeQL checks whether taint/dataflow still reaches the sink when applicable.
- Sharp-edge review checks whether the fix leaves a misleading or misuse-prone API behind.
Fusion should always be used to go deeper when the task requires it: risky fixes, contradictory evidence, difficult remediation tradeoffs, high-impact reports, unresolved verifier disagreement, or explicit high-assurance review. It should not be spent on routine low-risk patches when ordinary evidence is already sufficient.
## Kiro Spec Completion Criteria
The current spec is ready to drive implementation when these decisions are made:
1. Language/runtime: Python is recommended.
2. Vector store: choose Chroma, LanceDB, Qdrant local, or SQLite vector extension.
3. Initial OpenRouter chat models by role.
4. Initial OpenRouter embedding model.
5. First fixture repository or synthetic vulnerable examples.
6. Whether to depend directly on HarnessX or implement a minimal compatible harness core.
## Recommended MVP Cut
MVP should include:
- Local path scan.
- File enumeration and static tags.
- OpenRouter ranker.
- Quick-mode hunter.
- Evidence ladder with enforced transitions.
- Independent verifier.
- JSON and Markdown reports.
- Manifest with model IDs and prompt hashes.
MVP should exclude:
- Deep fuzzing and broad dynamic analysis.
- Auto-patching.
- Fusion panels for routine MVP findings.
- Full MCP server.
- Multi-repository campaigns.
- Training bridge.
MVP should still model the future fix lifecycle in the data model, because evidence, reports, and patch artifacts need stable IDs before patching exists.
These exclusions keep the first milestone testable and prevent architecture from hardening around unverified assumptions.
## Implementation Order Rationale
Do not start with Docker or broad fusion. Those are expensive and only pay off after the data model and evidence model are correct. Start with the smallest useful loop:
```text
preprocess -> rank -> quick finding -> verify -> report
```
Once this loop works on fixtures, add embeddings. Once retrieval is stable, add sandboxed commands. Once sandboxing is stable, add deep fuzzing. Add fusion as soon as tasks require deeper adjudication than the normal verifier can provide.
## Immediate Next Questions
1. Which OpenRouter chat models should be used for ranker, hunter, verifier, and patcher roles?
2. Which OpenRouter embedding model should be the default?
3. Which vector store should be selected for the first implementation?
4. Should the project start with synthetic fixtures or scan a real open-source target first?
5. Should the first implementation include MCP, or should MCP wait until the CLI and artifacts stabilize?
@@ -1,29 +0,0 @@
# Decisions: OpenRouter SAST Harness
## Approved Initial Slice
The first implementation slice is approved for Phase 0 and Phase 1 only:
- Python package skeleton with `ousast` CLI.
- Local configuration loading.
- Scan run directory creation under `.openultrasast/runs/<scan-id>/`.
- Repository snapshot collection.
- Source file enumeration with basic ignore handling.
- Language, LOC, and static tag heuristics.
- `preprocess/file_targets.json` artifact.
## Current Decisions
| Decision | Value | Scope |
| --- | --- | --- |
| Runtime | Python 3.11+ | Initial implementation |
| Harness dependency | Minimal HarnessX-compatible core first | Avoid direct dependency until contracts are proven |
| Vector store | Deferred to Phase 4 bakeoff | Not needed for Phase 0/1 |
| OpenRouter chat models | Deferred until ranker implementation | Not needed for Phase 0/1 |
| OpenRouter embedding model | Deferred until embeddings implementation | Not needed for Phase 0/1 |
| Fixtures | Synthetic fixture repositories first | Keeps tests deterministic |
| MCP | Defer until CLI and artifacts stabilize | Phase 12 |
## Rationale
The first milestone should prove the local artifact contract before adding model calls, embeddings, Docker, MCP, or fusion. Later decisions must be recorded here before their implementation phases begin.
@@ -1,762 +0,0 @@
# Design: OpenRouter SAST Harness
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Architecture Summary
OpenUltraSAST is a harness-oriented SAST system, not a monolithic scanner. The core runtime composes processors, model roles, retrieval, tools, sandbox policy, traces, verification gates, and reporting into a reproducible scan harness.
The central quality objective is false-positive elimination. The harness should not merely find more possible bugs; it should repeatedly narrow suspicion into evidence-backed findings and learn from rejected claims.
Conceptual lineage:
- HarnessX: harness primitives, event hooks, processor contracts, trace-driven evolution.
- OpenUltraCode: bounded context, fix authoring discipline, adversarial review, reconciliation, fusion, verification evidence.
- Clearwing sourcehunt: preprocess, rank, tiered hunters, evidence ladder, adversarial verifier, patch oracle, variant loop, reports.
- Trail of Bits skills: domain-specific mapping, analysis, audit, and verification methods selected at the right stage, not embedded globally.
## High-Level Flow
```text
repo path
-> intake snapshot
-> preprocess and static tags
-> graph, SARIF ingest, and entry-point reachability mapping
-> embeddings and retrieval index
-> file/function ranking
-> tiered hunter pool
-> independent verifier
-> optional fusion adjudication
-> optional explain and guided fix workflow
-> optional reproducer/exploit triage
-> optional patch oracle
-> reports and artifacts
```
Benchmark runs wrap the same scan pipeline instead of using a separate detector path:
```text
benchmark manifest
-> acquire or locate vulnerable project
-> run quick, standard, or deep scan
-> normalize OpenUltraSAST findings and optional external baselines
-> match expected vulnerabilities by CWE, file, function, sink, and evidence
-> record recall, precision where possible, misses, false-positive reasons, and runtime
-> feed misses and rejected findings into calibration artifacts
```
## Main Components
### CLI
Initial command surface:
```text
ousast scan <path> --mode quick|standard|deep --config openultrasast.toml
ousast benchmark <manifest> --mode quick|standard|deep
ousast explain <finding-id> --scan <scan-id> --audience concise|learner|reviewer
ousast status <scan-id>
ousast findings <scan-id>
ousast report <scan-id> --format markdown|json|sarif
ousast patch <finding-id> --propose
ousast index <path> --rebuild
```
The CLI should be thin. It loads config, creates a scan, starts the harness runtime, and writes artifacts.
### MCP Server
MCP should expose only stable project operations:
```text
openultrasast.scan
openultrasast.status
openultrasast.findings
openultrasast.get_finding
openultrasast.evidence
openultrasast.artifacts
openultrasast.benchmark
openultrasast.explain
openultrasast.propose_patch
openultrasast.export_report
```
The MCP server must not expose arbitrary shell, Docker, or internal hunter tools. Those stay inside controlled harness stages.
### Harness Runtime
The runtime owns the scan lifecycle and event pipeline. It should follow the HarnessX separation:
```text
ModelConfig: role -> OpenRouter model, parameters, budget, fallback policy
HarnessConfig: processors, tools, memory, sandbox, trace, control policy
ScanConfig: target path, mode, scope, excludes, report options
```
Required event hooks:
```text
task_start
stage_start
before_model
after_model
before_tool
after_tool
stage_end
task_end
```
Each processor declares the event types it handles and the state fields it reads or writes. Strict contract mode fails the scan when processors violate schemas. Warn mode records degraded state.
### OpenRouter Provider
The provider layer should implement two explicit clients:
```text
OpenRouterChatClient
OpenRouterEmbeddingClient
```
Chat roles:
```text
ranker
hunter
verifier
fusion_panel_a
fusion_panel_b
fusion_decider
patcher
reporter
```
Provider rules:
- No silent model substitution.
- Every model response records model ID, provider metadata when available, token counts, cost estimate, prompt hash, and response hash.
- Fallbacks must be configured per role and reported as degradations.
- Embedding model ID must be stored in the vector index metadata.
### Retrieval Store
The retrieval layer maintains separate namespaces:
```text
repo_code
repo_docs
static_findings
mechanisms
trailofbits_skills
scan_traces
false_positive_learnings
```
Chunk metadata should include:
```json
{
"repo_id": "...",
"snapshot": "...",
"path": "src/parser.c",
"symbol": "parse_frame",
"language": "c",
"chunk_type": "function",
"start_line": 120,
"end_line": 220,
"tags": ["parser", "memory_unsafe"],
"embedding_model": "openrouter/..."
}
```
Retrieval packages must be bounded by role. A verifier gets the finding, relevant source, static corroboration, and counter-evidence candidates. It does not get hunter chain-of-thought.
#### Vector Store Selection Criteria
The vector store is a core ranking and false-positive-control dependency. The first implementation should choose one default after evaluating:
```text
metadata filtering precision
incremental reindexing by repository snapshot
local persistence and portability
query latency on medium repositories
embedding model metadata support
hybrid lexical/vector retrieval support or integration path
deterministic export/import for scan reproducibility
operational simplicity for opencode users
```
Ranking depends on retrieval quality. Poor metadata filters will contaminate verifier context and increase false positives. The store must make it easy to retrieve "same symbol, same package, same vulnerability class, same evidence level" rather than semantically similar but irrelevant code.
### Preprocessor
The preprocessor produces `FileTarget` records. It should start simple and grow by plugins.
Initial fields:
```text
path
absolute_path
language
loc
tags
imports_by
transitive_callers
static_hints
sarif_hints
reachability_hints
has_fuzz_entry_point
defines_security_constants
```
Initial tags:
```text
memory_unsafe
parser
crypto
deserialization
syscall_entry
network_entry
filesystem_entry
fuzzable
privileged
```
### Static Analysis Mapping Layer
The static analysis layer turns tool output and skill discipline into a map for the harness. It is not only a source of finished findings.
Primary mapping sources:
```text
Semgrep: syntactic and lightweight semantic patterns, custom rule iteration, variant seeds
CodeQL: interprocedural dataflow, taint paths, structural vulnerability queries
Entry-point analysis: routes, CLI/process boundaries, parser inputs, smart-contract state-changing entry points, callbacks, privileged surfaces
Differential review: changed attack surfaces, regression-prone patches, new trust-boundary crossings
Sharp-edge review: unsafe APIs, insecure defaults, confusing configuration, misuse-prone interfaces
SARIF parsing: normalized evidence, rule metadata, locations, severity, fingerprints
```
Mapping outputs:
```text
static_hints on FileTarget
sarif_hints on FileTarget
taint_paths and source/sink pairs
variant_search_seeds
changed_surface records
sharp_edge records
entry_point records
reachability_hints on FileTarget
evidence candidates for verifier
```
This layer should use Trail of Bits skills as task templates and checklists. For example, the CodeQL discipline supplies query/dataflow thinking, Semgrep supplies pattern and variant loops, entry-point analysis supplies attacker-controlled surface and access classification, differential review supplies changed-code blast-radius analysis, and sharp-edge review supplies API misuse mapping.
### Benchmark Layer
The benchmark layer is the quality control system for a realistic grounded finder. It prevents the scanner from passing narrow fixtures while missing obvious bugs in vulnerable projects.
Benchmark manifest shape:
```json
{
"name": "nodegoat",
"language": "javascript",
"frameworks": ["node", "express"],
"source": {"type": "local", "path": "../benchmarks/nodegoat"},
"setup": {"commands": [], "requires_network": false},
"modes": ["quick", "standard"],
"expected": [
{
"cwe": "CWE-89",
"class": "sql_injection",
"path": "app/routes/*.js",
"evidence": "user input reaches database query"
}
],
"known_noise": [
{"reason": "training-only route", "scope": "docs/**"}
],
"baselines": ["semgrep", "codeql", "clearwing"]
}
```
Initial benchmark families:
```text
javascript_node_web: NodeGoat, DVNA, OWASP Juice Shop slices, vulnerable Express fixtures
python_web: PyGoat, vulnerable Flask/Django apps, pickle/deserialization and command-injection fixtures
java_web: OWASP Benchmark, WebGoat, Juliet Java, Spring MVC injection/auth fixtures
c_cpp: Juliet C/C++, Damn Vulnerable C/C++ Program, vulnerable parsers, ASan/UBSan crash fixtures
```
Benchmark metrics:
```text
expected_findings_total
matched_findings_total
missed_findings_total
unsupported_classes
false_positive_total
false_positive_reasons
recall_by_cwe
recall_by_language
precision_when_ground_truth_allows
runtime_seconds
model_calls_and_cost
external_baseline_delta
```
Benchmark misses are first-class calibration inputs. A miss should record the source location, expected CWE, which stage failed to surface it, whether an external baseline found it, and the next candidate improvement: static rule, SARIF source, entry-point mapping, CodeQL/Semgrep discipline, LLM hunter prompt, retrieval package, dynamic reproducer, or skill routing.
#### Entry-Point Reachability Mapping
Entry-point mapping is a Phase 5A prerequisite for useful ranking and false-positive reduction. Pattern matches and static analyzer alerts are not enough; the harness must identify whether the affected function or symbol is plausibly reachable from attacker-controlled input.
Initial entry-point kinds:
```text
route: HTTP/framework route handlers and request callbacks
cli: process entry points, argparse/click handlers, argv consumers, main functions
parser: parser or deserialization inputs that consume untrusted data
fuzz: explicit fuzz entry points such as LLVMFuzzerTestOneInput
smart_contract_state_change: externally callable non-view contract functions
contract_callback: callbacks and hook implementations invoked by other contracts
privileged_surface: auth boundaries, admin operations, filesystem/process/syscall boundaries
```
Initial access levels:
```text
public
authenticated
role-restricted
contract-only/callback
local-only
review-required
```
Each entry-point record should include path, function name when known, start line, end line, name, kind, access level, trust boundary, access evidence, condition evidence, provenance, and rationale. Access level distinguishes unauthenticated/public from authenticated, role-restricted, local-only, callback-only, and review-required surfaces. Condition evidence captures feature flags, rollout toggles, pause guards, experiment gates, and runtime guard expressions. The preprocessor attaches matching records to `FileTarget.reachability_hints`. Findings use the source line to inherit only function-level entry points whose range contains the finding. The ranker uses these hints to raise file review priority for files containing public routes, parser/fuzz inputs, authenticated surfaces, contract callbacks, and role-restricted privileged surfaces. A finding outside a reachable function should remain eligible for review, but should not inherit the entry point's priority without call-graph evidence.
### Ranker
Ranking should combine heuristics, graph features, static hints, and model judgment.
Scores:
```text
surface: direct vulnerability likelihood, 1-5
influence: downstream blast radius, 1-5
reachability: attacker-controlled path likelihood, 1-5
priority: surface * 0.5 + influence * 0.2 + reachability * 0.3
```
The ranker should emit concise rationales:
```json
{
"path": "src/parser.c",
"surface": 5,
"influence": 4,
"reachability": 4,
"priority": 4.5,
"rationale": "C parser with external input, pointer arithmetic, and many callers",
"model_id": "openrouter/...",
"static_boosts": ["memory_unsafe", "parser", "sarif_high"]
}
```
Ranking is not only prioritization; it is a false-positive filter. The ranker should use verifier outcomes and dynamic evidence outcomes to calibrate which surfaces deserve attention. Repeated rejected patterns should be demoted unless new evidence appears.
Ranking metrics:
```text
verified findings per tier
false positives per tier
false-positive reason distribution
time-to-verification per tier
retrieval hit quality per role
missed known-vulnerable fixture targets
```
### False-Positive Elimination Loop
HarnessX-style adjustment is the mechanism that reduces false positives over time. Each scan produces traces, verifier decisions, rejected hypotheses, static analyzer deltas, retrieval packages, and evidence transitions. The harness should use those outputs to adjust future runs.
Adjustment targets:
```text
ranker score calibration
retrieval filters and namespace weights
hunter prompt constraints
skill routing rules
verifier tie-breaker templates
static mapping seeds
variant search patterns
finding deduplication rules
```
False-positive records:
```text
finding_id
rejection_reason
counter_evidence
scope of suppression or demotion
affected ranker/static/retrieval inputs
verifier model_id
human_override if present
```
False-positive learning must be scoped. A rejected claim in one framework, version, path, or call pattern should not globally suppress a vulnerability class.
### Hunter Pool
The hunter pool schedules targets by priority tier and depth.
Suggested tiers:
```text
A: priority >= 3.0, most budget
B: priority >= 2.0, moderate budget
C: priority < 2.0, opportunistic budget
```
Hunter inputs:
```text
target metadata
bounded source context
static hints
retrieved related code
selected Trail of Bits skill snippets
sandbox capabilities
budget
evidence requirements
```
Hunter outputs must be structured and evidence-oriented. They should not rely on hidden reasoning.
Hunters must be language- and framework-aware. The default hunter prompts should not ask one generic model to find every vulnerability type. Initial hunter families should cover:
```text
javascript_node_web: injection, XSS, SSRF, prototype pollution, insecure deserialization, auth/session bugs, dependency misuse
python_web: command injection, SQL/template injection, deserialization, path traversal, Flask/Django misconfiguration, auth/session bugs
java_web: servlet/Spring injection, XXE, deserialization, path traversal, authorization bypass, crypto/API misuse
c_cpp: memory corruption, integer overflow, format strings, command execution, parser bugs, lifetime and ownership mistakes
```
### Verifier
The verifier runs independently from the hunter. It receives only auditable facts:
```text
finding claim
source excerpts
static evidence
retrieved relevant context
reproducer or crash artifacts when available
negative evidence candidates
```
Verifier output:
```json
{
"is_real": true,
"severity": "high",
"confidence": 0.82,
"evidence_level": "static_corroboration",
"pro_case": "...",
"counter_case": "...",
"tie_breaker": "Build a reproducer that drives len past buffer capacity",
"required_next_step": "attempt_crash_reproduction"
}
```
### Fusion Adjudication
Fusion is the mandatory deepening mechanism whenever the task requires more reasoning depth than the normal ranker, hunter, verifier, and static mapping loop can provide.
Fusion triggers:
- Critical or high severity findings.
- Findings with verifier disagreement.
- Findings that gate patching or disclosure.
- Findings where static and semantic evidence conflict.
- Findings where a patch could introduce regression risk.
- Findings where multiple skills disagree on exploitability or remediation.
- User or task requests for deeper analysis, ultrathink-style review, or high-assurance adjudication.
Fusion protocol:
```text
panel A independently reviews bounded evidence
panel B independently reviews bounded evidence
panels critique each other
panels revise
panels vote/rank
decider issues final disposition
reconciler records accepted/rejected/blocked items
verifier records fresh evidence
```
Fusion should not be forced for routine low-severity findings when ordinary evidence is already sufficient. It must run when the task requires deeper analysis, when evidence does not converge, or when the decision gates a risky fix, exploit claim, disclosure, or high-impact report.
### Docker Sandbox
Sandbox defaults:
```text
network: none
workspace: read-only
scratch: writable tmpfs or volume
cap_drop: all
cap_add: SYS_PTRACE only when sanitizer/debugger needs it
memory: bounded
cpu: bounded
pids: bounded
runtime: docker default, optional gVisor
```
Sandbox use cases:
```text
static tool execution
build system probing
sanitizer builds
fuzz harness compilation
PoC execution
local service startup
netcat-style local socket probes
protocol smoke tests
patch validation in writable copy
```
Patch validation must use a writable copy, never the user's working tree by default.
### Dynamic Analysis Boundary
OpenUltraSAST is SAST-first, but dynamic analysis is in scope when it provides necessary evidence. The harness may run controlled runtime checks such as service startup, local HTTP/TCP probes, netcat-style reachability checks, PoC execution, and Clearwing-style network validation patterns.
Dynamic analysis rules:
```text
disabled by default
enabled only by explicit config or task-required evidence
scoped to declared hosts, ports, containers, and commands
logged as artifacts
bounded by timeout and resource limits
never treated as an open-ended network pentest loop
```
Dynamic evidence can support transitions such as `static_corroboration -> crash_reproduced`, `static_corroboration -> root_cause_explained`, or `root_cause_explained -> exploit_demonstrated` when the artifact actually demonstrates the claim.
### Explain Mode And Interactive Fixing
Explain mode turns a finding or benchmark miss into a prevention-oriented explanation. It should be useful to a coder who wants to understand the mistake, a vibecoder who needs safe guidance, and a reviewer who needs evidence boundaries.
Explain input types:
```text
finding_id within a scan
benchmark miss ID
vulnerability class plus source location
SARIF result ID imported into a scan
```
Explain output sections:
```text
what was found
why this pattern is dangerous
what attacker-controlled path or reachability evidence exists
what evidence is missing
how to fix it safely
how to avoid recreating it
which validation command or artifact would prove the fix
which security skills were used for guidance
```
Explain mode must not inflate evidence. If a result is only a pattern match, it says so. If reachability is unknown, it names the missing call graph, route, CLI, parser, or dynamic artifact needed to prioritize the fix.
Interactive fixing builds on explain mode and the patch oracle. It should ask for confirmation before patch proposal, select skills by language and vulnerability class, produce a minimal diff, run or list the required validation checks, and preserve all decisions as artifacts. It must not silently apply patches to the user's repository by default.
### Trail Of Bits Skill Integration
Trail of Bits skills should be indexed and selected by routing metadata.
Examples:
```text
C/C++ memory target -> c-review, address-sanitizer, libfuzzer, harness-writing
Python parser target -> atheris, property-based-testing
Rust target -> cargo-fuzz, zeroize-audit when secrets are present
Crypto target -> constant-time-analysis, wycheproof, vector-forge
SARIF processing -> sarif-parsing
Semgrep mapping loop -> semgrep, semgrep-rule-creator, semgrep-rule-variant-creator
CodeQL mapping loop -> codeql, sarif-parsing
Changed-code review -> differential-review, graph-evolution
API footgun review -> sharp-edges, insecure-defaults
Smart contract target -> chain-specific scanner skill
```
Do not paste entire skills into every prompt. Store descriptors, retrieval chunks, and short operating checklists. Pull only the relevant snippets into a role-specific context package.
### UltraCode Fix Workflow
OpenUltraCode discipline should govern fix authoring and fix audit. The patch oracle is not a single prompt that writes code and declares success.
Fix workflow:
```text
intake verified finding and evidence
create bounded fix plan
generate minimal patch in writable sandbox or worktree
run affected checks and reproducer
run adversarial fix review
reconcile accepted/rejected/deferred review items
run fresh verification
emit patch diff and validation report
```
Fix audit inputs:
```text
original finding evidence
patch diff
differential analysis of changed code
Semgrep/CodeQL result delta when available
tests and reproducer output
sharp-edge regression checklist when API/config behavior changes
```
The workflow can propose a fix without applying it. It can only mark a fix as ready when the adversarial review is reconciled and fresh verification evidence exists.
### Data Model
Core entities:
```text
Scan
RepositorySnapshot
HarnessRun
ProcessorTrace
ModelCall
FileTarget
Finding
Evidence
Artifact
PatchProposal
VerificationDecision
FusionDecision
FalsePositiveLearning
RankingCalibration
```
Finding fields:
```text
id
scan_id
title
description
severity
confidence
status
evidence_level
path
start_line
end_line
symbol
vulnerability_class
attack_scenario
impact
remediation
discovered_by
verified_by
model_ids
artifact_refs
created_at
updated_at
```
Evidence levels:
```text
suspicion
static_corroboration
crash_reproduced
root_cause_explained
exploit_demonstrated
patch_validated
```
### Artifact Layout
Suggested scan directory:
```text
.openultrasast/runs/<scan-id>/
manifest.json
config.resolved.json
repo_snapshot.json
preprocess/
index/
rank/
traces/
model_calls/
sandbox/
findings.json
report.md
report.sarif
patches/
```
## Implementation Strategy
Build the system in vertical slices:
1. Quick scan: intake, preprocess, rank, LLM/static finding generation, JSON/Markdown report.
2. Verification: independent verifier, evidence ladder enforcement, SARIF export.
3. Retrieval: OpenRouter embeddings, persistent index, bounded context packages.
4. Static mapping: Semgrep/CodeQL SARIF ingest, differential mapping, sharp-edge records.
5. False-positive loop: rejected finding taxonomy, ranking calibration, retrieval adjustment, and scoped suppressions.
6. Sandbox: Docker command runner, static tools, read-only source, scratch volume.
7. Dynamic evidence: controlled service startup, local probes, PoC execution, and artifact capture.
8. Deep mode: sanitizer builds, fuzz harness generation, crash artifacts.
9. Patch mode: UltraCode-style fix authoring, adversarial fix audit, validation loop.
10. MCP/opencode integration: narrow tools and commands.
11. Fusion: mandatory deepening for task-required hard findings, contradictory evidence, and risky fixes.
## Key Design Decisions
1. Evidence level is a state machine, not a label the model can freely assign.
2. Verifier context excludes hunter reasoning to reduce contamination.
3. OpenRouter model selection is role-scoped and auditable.
4. Embeddings and vector-store metadata filtering are part of the runtime, not an offline optional add-on.
5. Docker isolation is mandatory for execution-heavy modes.
6. Trail of Bits skills are retrieved as scoped expertise, not global prompt bloat.
7. MCP surface is narrow and project-oriented.
8. Fixes are authored and audited through an UltraCode-style workflow, not a one-shot patch prompt.
9. Fusion is the required deepening path when the task demands high-assurance reasoning, not a replacement for routine analysis.
10. Harness adjustment loops must reduce repeated false positives without hiding auditable evidence.
## Risks
1. OpenRouter embedding availability and model-specific response formats may vary.
2. Deep mode can become expensive without strict budget controls.
3. Docker sandbox escape risk requires conservative defaults and no-network execution.
4. LLM findings can inflate false positives unless the evidence ladder is enforced in code.
5. Multi-language callgraph quality will vary; ranking must tolerate partial graph data.
6. Skill retrieval can overfit prompts if descriptors are too broad.
## Open Questions
1. Which local vector store should be the default: Chroma, LanceDB, SQLite vector extension, or Qdrant local?
2. Should the first implementation be pure Python like Clearwing/HarnessX, or TypeScript to align with more opencode extension tooling?
3. Should OpenUltraSAST vendor a minimal HarnessX-inspired runtime or depend directly on HarnessX?
4. What minimum OpenRouter embedding model should be required for acceptable code retrieval?
5. Should patch proposals be applied only through git worktrees?
@@ -1,242 +0,0 @@
# Requirements: OpenRouter SAST Harness
> **Status 2026-10-02:** retired (see the note below). Its OpenCode skill `openultrasast-kiro-impl`, which still sent agents to this tasks.md, was deleted. Kept as decision history; nothing in src, tests or CI points here.
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax
> (`ai-service-plane`), and evolutionary proposals come from the plane memory. History below is unchanged.
## Overview
OpenUltraSAST shall be an independent OpenCode-driven security harness for finding, validating, reporting, and optionally fixing source-code vulnerabilities at scale. It shall use OpenRouter-selected chat models for reasoning, OpenRouter embeddings for semantic retrieval, Docker for isolated analysis, and an explicit evidence ladder to prevent AI suspicion from being reported as verified security impact.
The project shall use Clearwing only as an implementation oracle. It shall not import Clearwing internals, fork Clearwing, or expose Clearwing's broad MCP surface.
## Goals
1. Provide a repeatable SAST harness that can scan local repositories and produce ranked, evidence-scored findings.
2. Make false-positive elimination a primary product goal by separating suspicion, corroboration, verification, reproduction, and patch validation.
3. Combine static analysis, semantic retrieval, LLM hunters, adversarial verification, and patch validation into one opencode-native workflow.
4. Keep model routing explicit through OpenRouter, with per-role model configuration and no silent substitution.
5. Preserve Docker isolation as a first-class security boundary for code execution, harness compilation, fuzzing, and patch validation.
6. Integrate Trail of Bits skills as scoped security expertise selected by language, framework, vulnerability class, mapping need, or verification need.
7. Emit machine-readable artifacts suitable for CI, triage, and later training or harness evolution.
8. Build detector quality against grounded vulnerable-project benchmarks for JavaScript/Node/web, Python/web, Java, and C/C++ instead of relying on toy fixtures or pattern-only tests.
9. Help coders and vibecoders understand and fix findings through an evidence-aware explain mode that teaches the secure pattern, not only flags the insecure one.
## Completeness Model
Implementation progress shall be reported against functional harness capability, not raw checklist percentage. A task may be complete as scaffold while the product capability remains incomplete.
Completion levels:
1. `scaffold`: schemas, CLI shape, local artifacts, tests, or isolated primitives exist.
2. `usable`: the feature runs end-to-end for a narrow local fixture without manual wiring.
3. `integrated`: the feature participates in the scan harness with traces, evidence, reports, and degradation handling.
4. `production`: the feature is robust enough for real repositories, CI budgets, failure recovery, and analyst audit.
Progress reports shall identify both checklist progress and functional completeness. The project shall not describe OpenUltraSAST as a working harness until the `usable_harness_mvp` gate is satisfied.
Functional gates:
1. `scaffold_quick_scan`: quick scan can enumerate files, rank targets, emit basic static findings, and write JSON/Markdown artifacts. This gate does not constitute a security harness.
2. `usable_harness_mvp`: harness runtime, event tracing, static mapping ingestion, evidence ladder enforcement, independent verification, and JSON/Markdown/SARIF reporting work together on fixture repositories.
3. `standard_security_harness`: standard mode runs ranked hunters with retrieval packages, selected security skills, independent verification, false-positive learning, and auditable traces.
4. `sandboxed_dynamic_harness`: Docker sandbox, scoped dynamic probes, sanitizer/build artifacts, and evidence transitions work without hidden network or host mutation.
5. `fix_validation_harness`: patch proposals, adversarial fix audit, differential/static delta checks, and sandbox validation are integrated and cannot mark patches validated without passing evidence.
6. `opencode_product`: MCP tools, opencode commands/skills, fusion deepening, CI-ready reports, cost controls, and hardening are complete enough for routine opencode-driven use.
7. `benchmark_grounded_finder`: benchmark manifests, expected vulnerabilities, tool comparison baselines, recall/precision metrics, and missed-finding regression tracking exist for JavaScript, Python, Java, and C/C++ vulnerable projects.
8. `explain_and_fix_workflow`: users can ask for an evidence-grounded explanation of a finding, receive prevention guidance, and enter an interactive fixing workflow that routes to the appropriate security skills without silently applying patches.
The current implementation state shall be documented as `usable_harness_mvp` until later gates are explicitly verified.
## Non-Goals
1. Do not make network pentesting the core workflow.
2. Do not reimplement all of Clearwing.
3. Do not report LLM-only claims as verified vulnerabilities.
4. Do not require a local embedding model.
5. Do not depend on a single OpenRouter model.
6. Do not mutate user repositories by default.
## Functional Requirements
### Repository Intake
1. The system shall accept a local repository path as the primary scan target.
2. The system should later support git URLs, but initial implementation may require local checkout.
3. The system shall snapshot basic repository metadata: root path, commit hash when available, language mix, file count, and ignored paths.
4. The system shall respect common ignore files such as `.gitignore`, `.ignore`, and explicit harness config excludes.
### Preprocessing
1. The system shall enumerate source files and assign initial language, LOC, and path metadata.
2. The system shall tag files using lightweight static heuristics, including memory-unsafe code, parser surfaces, crypto surfaces, authentication boundaries, syscall or process boundaries, serialization boundaries, and fuzzable entry points.
3. The system shall build a dependency or call graph where practical.
4. The system shall ingest optional static analyzer output such as Semgrep SARIF or CodeQL SARIF.
5. The system shall preserve preprocessing artifacts for later ranking, retrieval, and audit.
### Static Analysis And Mapping Discipline
1. The system shall treat Semgrep, CodeQL, differential review, and sharp-edge/API review as mapping disciplines, not just optional external scanners.
2. The system shall ingest Semgrep and CodeQL output as SARIF and normalize it into static hints, evidence candidates, and variant-search seeds.
3. The system shall support function-level entry-point and reachability analysis to identify attacker-controlled inputs, externally callable functions, public routes, CLI/process boundaries, parser entry points, smart-contract state-changing entry points, callbacks, and privileged/admin-only surfaces.
4. The system shall classify entry points by access level and trust boundary, including public/unrestricted, authenticated, role-restricted, contract-only/callback, local-only, and unknown/review-required.
5. The system shall preserve condition evidence for entry points, including feature flags, rollout toggles, pause guards, experiment gates, and runtime guard expressions that affect whether a threat actor can reach the code.
6. The system shall use function-level reachability modeling to prioritize candidate bugs inside reachable entry-point functions and to demote pattern-only findings that lack a plausible attacker-controlled path.
6. The system shall support differential analysis between a base and head revision to identify changed attack surfaces, newly reachable code, and regression-prone fixes.
7. The system shall support sharp-edge mapping for APIs, configuration, defaults, cryptographic interfaces, and dangerous framework patterns that create predictable misuse.
8. The system shall preserve analyzer provenance so each finding can cite which static tool, rule, diff, entry-point analysis, or skill-derived checklist contributed evidence.
### Benchmark-Grounded Detection
1. The system shall maintain benchmark manifests for realistic vulnerable projects, not only synthetic single-file fixtures.
2. The initial benchmark matrix shall cover JavaScript/Node/web, Python/web, Java, and C/C++.
3. Benchmarks shall include vulnerable training projects and curated CWE samples such as NodeGoat, DVNA or similar Node/Express apps, OWASP Juice Shop slices, PyGoat or vulnerable Flask/Django samples, OWASP Benchmark/WebGoat/Juliet Java, Juliet C/C++, vulnerable parser fixtures, and small sanitizer-backed C/C++ crash targets.
4. Each benchmark manifest shall record repository path or source, language, framework, setup commands, scan modes to run, expected vulnerability classes, expected locations when known, and known noisy/non-security patterns.
5. The benchmark runner shall emit recall, precision where ground truth allows it, false-positive reason distribution, missed expected findings, unsupported vulnerability classes, runtime, model usage, and artifact paths.
6. Benchmark results shall compare quick, standard, and deep modes separately so rule coverage, LLM hunting, and dynamic validation are not conflated.
7. Benchmark runs shall support comparison against external baselines such as Clearwing, Semgrep, CodeQL, clang-tidy, npm audit, Bandit, or language-appropriate tools when their outputs are available as SARIF or normalized findings.
8. Missed benchmark vulnerabilities shall become regression records that feed rule expansion, ranking calibration, retrieval packages, hunter prompts, and skill routing.
9. A detector shall not be considered realistic for a language until it demonstrates useful recall on at least one realistic benchmark project and one curated CWE corpus for that language.
10. Benchmark artifacts shall be preserved under `.openultrasast/benchmarks/<benchmark-run-id>/` with links to the scan artifacts that produced each metric.
### OpenRouter Embeddings And Retrieval
1. The system shall use OpenRouter embedding endpoints for semantic indexing.
2. The system shall chunk source code, docs, static findings, prior mechanisms, and Trail of Bits skill descriptors into separate collections or namespaces.
3. The system shall store vectors in a local persistent vector database by default.
4. The system shall support reusing an existing index when the repository snapshot has not changed.
5. The system shall retrieve bounded context packages for rankers, hunters, verifiers, and patchers.
6. The system shall evaluate vector-store candidates against ranking quality, metadata filtering, incremental indexing, local persistence, cost, and reproducibility before selecting a default.
7. The vector store shall support precise metadata filters for path, language, symbol, vulnerability class, evidence level, scan ID, and repository snapshot.
### Ranking
1. The system shall score candidate files on at least three axes: surface, influence, and reachability.
2. The system shall compute a composite priority using an explicit formula, initially `surface * 0.5 + influence * 0.2 + reachability * 0.3`.
3. The system shall keep low-surface high-influence files eligible for analysis.
4. The system shall combine static heuristics, dependency graph data, entry-point reachability, embeddings, and OpenRouter LLM scoring.
5. The system shall record score rationales and model identity.
6. The system shall measure ranking quality with fixture repositories and prior scan outcomes, including false-positive rate by tier and missed-true-positive rate where ground truth exists.
7. The system shall feed verifier outcomes back into ranking calibration so repeated false-positive patterns lose priority over time.
### False-Positive Elimination Loop
1. The system shall track every rejected, contradicted, duplicate, unsupported, and unverified finding as false-positive learning data.
2. The system shall record why a finding failed: unreachable path, missing attacker control, sanitizer disproved, static rule mismatch, incorrect model assumption, duplicate finding, or insufficient impact.
3. The system shall use HarnessX-style adjustment loops to update prompts, retrieval filters, ranking calibration, skill routing, verifier tie-breakers, and static mapping seeds from false-positive outcomes.
4. The system shall keep false-positive learnings auditable and reversible; learned suppressions shall cite evidence and scope.
5. The system shall prefer evidence-gated demotion over deletion so analysts can inspect why a suspected issue was suppressed or downgraded.
### Harness Execution
1. The system shall model the scan as a HarnessX-style harness composed of processors, tools, memory, sandbox, trace, and model roles.
2. The system shall expose stage hooks for task start, stage start, before model, after model, before tool, after tool, stage end, and task end.
3. The system shall contract-check processor inputs and outputs in strict mode for CI and warn mode for local exploration.
4. The system shall serialize harness configuration, processor versions, model roles, and prompt hashes into scan artifacts.
### Hunter Pool
1. The system shall fan out analysis across ranked targets using budgeted tiers.
2. The system shall support at least three analysis depths: quick, standard, and deep.
3. Quick mode shall rely on static analysis, ranking, and bounded LLM review without code execution.
4. Standard mode shall use sandboxed tool-calling hunters and adversarial verification.
5. Deep mode shall add harness generation, sanitizer builds, fuzzing, dynamic reproduction, crash reproduction, and patch validation where applicable.
6. The system shall record every hunter trajectory as structured JSONL.
7. Standard-mode hunters shall be evaluated against benchmark misses before being treated as production-quality detectors.
8. Hunter prompts shall be language- and framework-aware for JavaScript/Node/web, Python/web, Java, and C/C++ instead of using one generic vulnerability prompt for all targets.
### Evidence Ladder
1. Every finding shall carry exactly one current evidence level.
2. Evidence levels shall be ordered as: `suspicion`, `static_corroboration`, `crash_reproduced`, `root_cause_explained`, `exploit_demonstrated`, `patch_validated`.
3. Findings below `static_corroboration` shall not be reported as verified.
4. Exploit or PoC generation shall require at least `crash_reproduced` or equivalent trigger evidence.
5. Auto-patching shall require at least `root_cause_explained` and shall only advance to `patch_validated` after tests or reproducer validation pass.
### Verification And Fusion
1. The system shall verify findings using independent context that excludes hunter chain-of-thought and raw hunter reasoning.
2. The verifier shall steel-man both the vulnerability case and the false-positive case.
3. The verifier shall output explicit tie-breaker evidence requirements.
4. The system shall support OpenUltraCode-style fusion for task-required deepening, high-impact findings, uncertain findings, risky fixes, or unresolved disagreement, using two panels and an explicit decider.
5. Fusion output shall disclose model IDs, panel roles, degradations, warnings, votes, and final decision source.
6. Every adversarial finding shall receive a disposition: accepted, rejected, mitigated, deferred, or blocked.
7. Fusion shall be mandatory whenever the task requires deeper reasoning than the normal ranker, hunter, verifier, and static mapping loop can provide.
8. Fusion shall not be forced for routine low-risk issues unless the task explicitly asks for deeper analysis or the evidence remains unresolved.
### Docker Sandbox
1. The system shall run untrusted build, test, fuzz, and PoC commands inside Docker by default.
2. Analysis containers shall have network disabled by default.
3. Source mounts shall be read-only except in explicit patch workspaces.
4. Containers shall use a writable scratch volume or tmpfs.
5. Containers shall enforce CPU, memory, pids, timeout, and capability limits.
6. The system shall support sanitizer variants, initially ASan and UBSan for C/C++ targets.
7. The system should support gVisor or another hardened runtime when available.
### Dynamic Analysis
1. The system shall support optional dynamic analysis when runtime evidence is needed to verify, reproduce, or de-risk a finding.
2. Dynamic analysis may include service startup, local socket checks, netcat-style probes, protocol smoke tests, PoC execution, or Clearwing-style network validation patterns.
3. Dynamic network access shall be disabled by default and enabled only through explicit scan configuration or task-required evidence collection.
4. Dynamic probes shall run in sandboxed environments with declared network scope, timeouts, command logs, and captured artifacts.
5. Dynamic analysis shall contribute evidence but shall not turn OpenUltraSAST into a general network pentest agent.
### Patch Oracle
1. The system shall optionally generate minimal defensive patches for verified findings.
2. The patch workflow shall follow OpenUltraCode discipline: intake, bounded plan, implementation, adversarial review, reconciliation, and fresh verification.
3. The patch oracle shall target the smallest safe change.
4. The patch oracle shall not rewrite unrelated code or change public signatures unless required and approved.
5. Patch validation shall rerun the reproducer, affected tests, or relevant build checks in a writable sandbox.
6. Patch artifacts shall be emitted as diffs, not silently applied to the user's repository by default.
7. The system shall audit proposed fixes for regression risk using differential analysis and relevant Trail of Bits skills before marking them ready.
### Explain Mode And Interactive Fixing
1. The system shall expose an explain workflow for a finding, vulnerability class, or benchmark miss.
2. Explain mode shall describe the vulnerable pattern, why it matters, what attacker-controlled path or evidence exists, what evidence is missing, and what secure coding habit would have prevented it.
3. Explain mode shall distinguish education from verification; an explanation shall not upgrade evidence level unless the required evidence artifact already exists.
4. Explain mode shall cite source lines, finding artifacts, verifier results, SARIF/static evidence, dynamic evidence, benchmark metadata, and relevant skill snippets where available.
5. Explain mode shall support audience levels such as concise, learner, and reviewer so it can serve coders, vibecoders, and security reviewers without changing the underlying evidence.
6. Interactive fixing shall guide the user through a bounded plan, relevant security skill selection, minimal patch proposal, adversarial review, and validation commands.
7. Interactive fixing shall route by language and vulnerability class, for example C/C++ memory issues to `c-review`, `address-sanitizer`, `libfuzzer`, and `harness-writing`; JavaScript web issues to web/security mapping and dependency checks; Python web issues to injection/deserialization/auth/defaults checks; and Java issues to OWASP Benchmark/WebGoat-style dataflow and framework checks.
8. Interactive fixing shall never silently apply patches to the user's repository by default. It shall produce a proposed diff and required validation plan first.
9. Explain and fix outputs shall emphasize prevention patterns so users learn how not to recreate the same vulnerability class in future code.
### Reporting
1. The system shall emit JSON, SARIF, Markdown, and a manifest per scan.
2. Reports shall sort findings by severity, evidence level, reachability, and confidence.
3. Reports shall include model IDs, static evidence, verifier results, reproduction evidence, patch status, and artifact paths.
4. Reports shall distinguish suspected, verified, exploited, and patch-validated findings.
### MCP And OpenCode Integration
1. The project shall expose a narrow MCP server for OpenCode integration.
2. MCP tools shall focus on project-level operations: scan, status, findings, evidence, artifacts, patch proposal, and report export.
3. The MCP server shall not expose arbitrary internal tools as public MCP methods.
4. OpenCode commands shall invoke the harness with explicit model role configuration and visible assumptions.
5. MCP and OpenCode integration shall include narrow operations for benchmark status, finding explanation, and interactive patch proposal without exposing arbitrary shell execution.
## Non-Functional Requirements
1. The system shall be deterministic where possible: fixed config, artifact manifests, prompt hashes, and model identifiers.
2. The system shall degrade visibly when optional tools, Docker, embeddings, or specific models are unavailable.
3. The system shall avoid hidden network access from analysis containers; any dynamic network access shall be explicit, scoped, logged, and artifact-backed.
4. The system shall support CI usage with bounded runtime and cost budgets.
5. The system shall keep findings auditable from report back to source, trace, model call, verifier decision, and artifact.
## Acceptance Criteria
1. `scaffold_quick_scan`: A user can run a local repository scan in quick mode and receive ranked static findings with JSON and Markdown reports. This may use heuristics and static patterns only.
2. `usable_harness_mvp`: A user can run quick or standard fixture scans through the harness runtime and inspect event traces, normalized static mapping evidence, verifier decisions, and report artifacts.
3. `standard_security_harness`: A user can run standard mode and receive adversarially verified findings with explicit evidence levels, selected retrieval context, skill routing, and false-positive learning records.
4. `sandboxed_dynamic_harness`: A user can run deep mode on a small C/C++ parser target and receive sandboxed build/fuzz artifacts when the target is fuzzable.
5. `fix_validation_harness`: A patch cannot be marked validated unless a sandboxed validation command passes and the fix audit has no accepted blocking findings.
6. `opencode_product`: A user can drive scans, triage, evidence inspection, report export, fusion, and patch proposal through narrow MCP/OpenCode interfaces without exposing arbitrary shell execution.
7. A finding cannot be marked verified unless it reaches at least `static_corroboration` through enforced evidence transitions.
8. A scan manifest records repository snapshot, harness config, OpenRouter model IDs, embedding model, prompt hashes, processor versions, evidence transitions, and artifact paths.
9. `benchmark_grounded_finder`: A user can run a benchmark manifest for JavaScript, Python, Java, or C/C++ and receive language-level recall, missed finding, false-positive, runtime, and artifact reports.
10. `explain_and_fix_workflow`: A user can request an explanation for a finding or benchmark miss and receive evidence-cited prevention guidance plus an optional interactive fix plan without automatic repository mutation.
@@ -1,7 +0,0 @@
{
"feature_name": "openrouter-sast-harness",
"language": "en",
"status": "retired",
"updated_at": "2026-10-02T00:00:00Z",
"retired_note": "2026-09-30: HarnessX was removed by harnessx-removal; this spec had no spec.json before 2026-10-02 and is kept as decision history."
}
@@ -1,303 +0,0 @@
# Tasks: OpenRouter SAST Harness
## Completion Gates
Checklist completion is not product completion. Each phase now carries a functional gate label:
- `scaffold`: isolated primitives, schemas, local artifacts, or tests exist.
- `usable`: the feature works end-to-end for fixtures without manual wiring.
- `integrated`: the feature participates in the harness runtime, traces, evidence, and reports.
- `production`: the feature is robust for real repositories, CI budgets, and analyst audit.
Current verified gate: `usable_harness_mvp`.
Do not report OpenUltraSAST as a working security harness until `usable_harness_mvp` is checked.
## Phase 0: Project Skeleton (`scaffold`, complete)
- [x] Create Python package skeleton with CLI entry point `ousast`.
- [x] Add config loading for `openultrasast.toml` and environment variables.
- [x] Add resolved config artifact writer.
- [x] Add scan run directory creation under `.openultrasast/runs/<scan-id>/`.
- [x] Add basic tests for config loading and run directory creation.
## Phase 1: Intake And Preprocess
- [x] Implement repository snapshot collection.
- [x] Implement source file enumeration with ignore handling.
- [x] Implement language detection from extension and shebang.
- [x] Implement LOC counting.
- [x] Implement initial tag heuristics for parser, memory unsafe, crypto, auth boundary, deserialization, syscall, network, filesystem, and fuzzable targets.
- [x] Emit `preprocess/file_targets.json`.
- [x] Add tests for tag heuristics on small fixture repositories.
## Phase 2: Ranking
- [x] Define `FileTarget` and ranking schemas.
- [x] Implement heuristic score floors and boosts.
- [x] Implement OpenRouter chat client for ranker role.
- [x] Implement chunked ranker calls with JSON response validation.
- [x] Compute composite priority.
- [x] Emit ranking rationales and model metadata.
- [x] Add tests for composite priority and malformed model response handling.
## Phase 3: Quick Scan Reports
- [x] Implement static finding schema.
- [x] Implement quick-mode hunter prompt and bounded file context.
- [x] Implement structured finding extraction.
- [x] Enforce initial evidence level `suspicion` unless static corroboration exists.
- [x] Emit `findings.json`.
- [x] Emit Markdown report.
- [x] Add smoke test against a fixture repository with one known insecure pattern.
## Phase 4: OpenRouter Embeddings
- [x] Implement OpenRouter embedding client.
- [x] Run a vector-store bakeoff for metadata filtering, local persistence, incremental indexing, export/import, and retrieval quality.
- [x] Select default local vector store based on bakeoff results.
- [x] Implement code chunking with path, symbol, and line metadata.
- [x] Implement namespaces for repo code, docs, static findings, mechanisms, skills, and traces.
- [x] Implement index reuse keyed by repository snapshot and embedding model.
- [x] Implement bounded retrieval package builder per role.
- [x] Add tests for chunk metadata and retrieval filters.
## Phase 4A: Ranking Quality And False-Positive Loop
- [x] Define false-positive reason taxonomy.
- [x] Add false-positive learning records for rejected, duplicate, unsupported, contradicted, and unverified findings.
- [x] Track ranking metrics by tier: verified findings, false positives, time to verification, and missed fixture vulnerabilities.
- [x] Feed verifier outcomes into ranking calibration.
- [x] Feed rejected findings into retrieval filter adjustment and prompt constraints.
- [x] Add tests proving scoped false-positive demotion does not globally suppress the vulnerability class.
## Gate: `scaffold_quick_scan`
- [x] Quick scan can enumerate files, rank targets, emit basic static findings, and write JSON/Markdown artifacts.
- [x] Quick scan is uv-managed and covered by unit tests.
- [ ] Quick scan includes real LLM hunter execution. This is intentionally deferred to standard mode.
- [x] Quick scan includes SARIF output.
- [x] Quick scan includes enforced verifier decisions.
## Gate: `usable_harness_mvp`
- [x] Harness runtime drives scan stages instead of direct CLI orchestration.
- [x] Event traces are emitted for every scan stage.
- [x] Static mapping ingestion feeds FileTarget hints and verifier evidence candidates.
- [x] Entry-point reachability feeds FileTarget reachability hints and ranking priority.
- [x] Evidence ladder state machine prevents invalid verified statuses.
- [x] Independent verifier runs on fixture findings without hunter reasoning.
- [x] JSON, Markdown, SARIF, and manifest artifacts share finding IDs and evidence references.
- [x] A fixture scan demonstrates one accepted static-corroborated finding and one rejected false positive.
## Phase 5: Harness Runtime (`usable_harness_mvp` prerequisite)
- [x] Define harness event model.
- [x] Define processor interface and contract checks.
- [x] Implement strict and warn modes.
- [x] Add trace writer for events and processor state changes.
- [x] Serialize harness config, prompt hashes, processor versions, and model roles.
- [x] Add tests for processor contract violations.
## Phase 5A: Static Mapping Disciplines
- [x] Add SARIF ingestion for Semgrep and CodeQL outputs.
- [x] Normalize SARIF rules, locations, severity, fingerprints, and provenance into static hints.
- [x] Add Semgrep mapping task records for pattern, variant, and rule-authoring loops.
- [x] Add CodeQL mapping task records for source, sink, sanitizer, and path explanations.
- [x] Add entry-point mapping records for routes, CLI/process boundaries, parser entry points, smart-contract state-changing entry points, callbacks, and privileged surfaces.
- [x] Classify entry points by access level: public, authenticated, role-restricted, contract-only/callback, local-only, or review-required.
- [x] Map entry points to function-level source ranges and access evidence.
- [x] Preserve conditional reachability evidence for feature flags, rollout toggles, pause guards, and runtime guard expressions.
- [x] Attach reachability hints to FileTargets and ranking inputs.
- [x] Annotate findings with function-level reachability status before prioritizing fixes.
- [x] Add differential mapping records for changed files, changed functions, and changed trust-boundary crossings.
- [x] Add sharp-edge records for insecure defaults, dangerous APIs, confusing config, and misuse-prone interfaces.
- [x] Add tests for mapping analyzer output to `FileTarget` hints and verifier evidence candidates.
## Phase 6: Verification (`usable_harness_mvp` prerequisite)
- [x] Define evidence ladder state machine.
- [x] Implement independent verifier prompt and schema.
- [x] Ensure verifier context excludes hunter reasoning.
- [x] Implement pro-case, counter-case, tie-breaker, and required-next-step fields.
- [x] Enforce that verified report status requires at least `static_corroboration`.
- [x] Add tests for invalid evidence transitions.
## Gate: `standard_security_harness`
- [x] Standard mode uses ranked hunter scheduling, retrieval packages, selected skill snippets, and independent verification.
- [x] Every hunter trajectory is persisted and linked to finding artifacts.
- [x] False-positive learning updates ranking calibration from verifier outcomes.
- [ ] Fusion can be triggered for high-impact, contradictory, or difficult findings.
- [ ] Standard mode uses real language-aware LLM hunters instead of reusing quick static findings.
## Gate: `benchmark_grounded_finder`
- [ ] Benchmark manifests exist for JavaScript/Node/web, Python/web, Java, and C/C++ vulnerable projects.
- [ ] Benchmark runner executes quick, standard, and deep modes as supported and preserves benchmark artifacts.
- [ ] Benchmark reports include recall, missed expected findings, false-positive reasons, runtime, and external baseline deltas.
- [ ] Missed benchmark findings feed calibration records for rules, ranking, retrieval, hunters, SARIF ingestion, dynamic probes, or skill routing.
- [ ] At least one realistic vulnerable project and one curated CWE corpus are tracked per supported language family.
## Phase 6A: Benchmark Corpus And Scoreboard (`benchmark_grounded_finder` prerequisite)
- [x] Define benchmark manifest schema with source, language, framework, setup, expected vulnerabilities, known noise, scan modes, and optional baselines.
- [x] Implement benchmark run directory creation under `.openultrasast/benchmarks/<benchmark-run-id>/`.
- [x] Implement finding-to-ground-truth matching by CWE, rule ID, file, function, sink, and evidence text.
- [x] Implement benchmark metrics for recall, precision when possible, false-positive reasons, missed findings, runtime, model usage, and artifact links.
- [x] Add benchmark fixtures or manifests for JavaScript/Node/web, Python/web, Java, and C/C++.
- [x] Add external baseline ingestion for SARIF or normalized findings from Clearwing, Semgrep, CodeQL, clang-tidy, Bandit, npm audit, or language-appropriate tools.
- [x] Add tests proving benchmark misses are recorded as calibration inputs without automatically suppressing unrelated findings.
## Gate: `explain_and_fix_workflow`
- [ ] Explain mode can explain a finding or benchmark miss with source evidence, reachability status, missing evidence, secure pattern, and prevention guidance.
- [ ] Explain mode supports concise, learner, and reviewer audience levels without changing evidence levels.
- [ ] Interactive fixing selects relevant security skills and proposes a minimal patch plan without silently mutating the target repository.
- [ ] Fix guidance links to validation commands and required evidence transitions.
## Phase 6B: Explain Mode And Interactive Fixing (`explain_and_fix_workflow` prerequisite)
- [ ] Add explain artifact schema for finding ID, source refs, evidence refs, audience level, prevention guidance, selected skills, and validation advice.
- [ ] Add `ousast explain <finding-id> --scan <scan-id>` CLI command.
- [ ] Add explain support for benchmark misses and imported SARIF findings.
- [ ] Implement language/vulnerability skill routing for JavaScript/Node/web, Python/web, Java, and C/C++.
- [ ] Add interactive fixing plan schema with confirmation gates, selected skills, proposed patch scope, validation commands, and evidence requirements.
- [ ] Ensure interactive fixing cannot mark patches validated unless the patch oracle and sandbox validation gates pass.
- [ ] Add tests that explanation does not upgrade evidence level and patch guidance does not mutate repositories by default.
## Phase 7: Docker Sandbox (`sandboxed_dynamic_harness` prerequisite)
- [ ] Implement Docker availability probe.
- [ ] Implement no-network read-only workspace sandbox runner.
- [ ] Add explicit scoped-network sandbox mode for task-required dynamic probes.
- [ ] Add scratch volume or tmpfs support.
- [ ] Add CPU, memory, pids, timeout, and capability limits.
- [ ] Add command execution artifact capture.
- [ ] Add sanitizer build environment for C/C++ fixtures.
- [ ] Add tests that verify workspace remains read-only.
## Phase 7A: Dynamic Evidence (`sandboxed_dynamic_harness` prerequisite)
- [ ] Implement dynamic analysis configuration with `enabled = false` by default.
- [ ] Implement service startup command records with declared ports and timeouts.
- [ ] Implement netcat-style local socket probe tool inside sandbox scope.
- [ ] Implement HTTP or protocol smoke-test artifact capture.
- [ ] Tie dynamic artifacts to evidence transitions without allowing model-only upgrades.
- [ ] Add tests that reject undeclared network targets.
## Phase 8: Standard Mode Hunter Pool (`standard_security_harness` prerequisite)
- [x] Implement tier assignment by priority.
- [x] Implement budget allocation and per-target limits.
- [x] Implement parallel hunter scheduling.
- [x] Persist hunter trajectories as JSONL.
- [x] Integrate retrieval packages and selected skill snippets.
- [x] Integrate verifier after hunter output.
- [x] Add fixture scan demonstrating at least one verified static-corroborated finding.
## Phase 9: SARIF And CI Output (`usable_harness_mvp` prerequisite)
- [x] Implement SARIF report writer.
- [x] Implement machine-readable manifest.
- [x] Add severity, confidence, evidence level, model IDs, and artifact refs to reports.
- [x] Add CI-friendly exit code policy.
- [x] Add tests for SARIF schema validity.
## Gate: `sandboxed_dynamic_harness`
- [ ] Docker sandbox runs build/test/fuzz/PoC commands with no network by default.
- [ ] Scoped dynamic probes require explicit declared targets and emit artifacts.
- [ ] Sanitizer/fuzz artifacts can advance evidence only through valid transitions.
## Phase 10: Deep Mode (`sandboxed_dynamic_harness` prerequisite)
- [ ] Implement fuzzable target detection beyond existing fuzz entry points.
- [ ] Implement fuzz harness generation prompt and schema.
- [ ] Compile generated harnesses in sandbox.
- [ ] Run libFuzzer or equivalent with sanitizer instrumentation.
- [ ] Capture crashes and minimization artifacts.
- [ ] Advance findings to `crash_reproduced` only with artifact evidence.
- [ ] Add a small C/C++ parser fixture with reproducible sanitizer crash.
## Gate: `fix_validation_harness`
- [ ] Patch proposals are generated as diffs and not silently applied.
- [ ] Fix lifecycle records intake, plan, implementation, adversarial review, reconciliation, verification, and ready states.
- [ ] Patch validation requires sandboxed checks and accepted adversarial findings must be resolved.
## Phase 11: Patch Oracle (`fix_validation_harness` prerequisite)
- [ ] Implement patch proposal schema.
- [ ] Implement OpenUltraCode-style fix lifecycle states: intake, plan, implement, adversarial_review, reconcile, verify, ready.
- [ ] Run patching in a writable sandbox copy or git worktree.
- [ ] Generate minimal diffs only.
- [ ] Rerun reproducer and affected checks.
- [ ] Run differential fix audit against the generated patch.
- [ ] Run Semgrep or CodeQL delta checks when corresponding evidence exists.
- [ ] Run sharp-edge fix audit when API, config, auth, crypto, or defaults are changed.
- [ ] Advance findings to `patch_validated` only after validation passes.
- [ ] Add tests that reject patches changing unrelated files.
## Gate: `opencode_product`
- [ ] Narrow MCP tools cover scan, status, findings, evidence, artifacts, patch proposal, and report export.
- [ ] OpenCode workflows can drive scan, triage, fusion, and patch audit without arbitrary shell exposure.
- [ ] Cost controls, retries, redaction, docs, examples, and CI gates are in place.
- [ ] OpenCode workflows can drive benchmark runs, finding explanations, and interactive fix proposals.
## Phase 12: MCP And OpenCode Commands (`opencode_product` prerequisite)
Implemented in `mcp.py` (zero-dep stdio JSON-RPC server) + the `ousast mcp` CLI
subcommand; tests in `tests/test_mcp.py`; README "MCP server" section documents it.
- [x] Add project-local opencode skills for scan, triage, fix audit, and implementation steering.
- [x] Implement narrow MCP server.
- [x] Add `openultrasast.scan`, `status`, `findings`, `get_finding`, `evidence`, `artifacts`, `benchmark`, `explain`, `propose_patch`, and `export_report` tools.
- [x] Add opencode command docs for quick, standard, deep, verify, and patch workflows. (README; deep/patch noted as roadmap.)
- [x] Ensure MCP tools never expose arbitrary shell execution. (Surface-safety test asserts no shell/exec/command tool.)
- [x] Add integration test for MCP tool listing and scan status.
## Phase 13: Fusion (`standard_security_harness` and `opencode_product` prerequisite)
Implemented in `fusion.py` (zero-dep deterministic engine + lazy LLM panels), wired
into the standard-mode CLI (`fusion.json` + manifest `fusion` block), config `[fusion]`.
Tests in `tests/test_fusion.py`.
- [x] Implement fusion trigger policy.
- [x] Trigger fusion whenever a task requires deeper reasoning, including difficult findings, contradictory evidence, high-impact decisions, and risky fixes.
- [x] Implement panel A, panel B, and decider role config.
- [x] Implement cross-critique and revised answer flow.
- [x] Implement vote/rank output schema.
- [x] Implement reconciliation dispositions.
- [x] Record model IDs, degradations, warnings, and final decision source.
- [x] Add tests for fusion output validation.
## Phase 14: Skill Index (`standard_security_harness` prerequisite)
Implemented in `skills.py` (descriptor inventory + scoring router + retrieval-namespace
chunking + budget-capped context); the HarnessX hunter prompt consumes the routed
skill context; tests in `tests/test_skills.py`.
- [x] Build Trail of Bits skill descriptor inventory.
- [x] Chunk selected skill operating guidance into retrieval namespace.
- [x] Implement skill router by language, tags, vulnerability class, and stage.
- [x] Add prompt budget caps for skill snippets.
- [x] Add tests for routing C parser targets to memory/fuzzing skills and crypto targets to constant-time/vector skills.
## Phase 15: Hardening And Release Readiness (`production` prerequisite)
Implemented: `redaction.py` (traces/reports), provider retry/backoff in
`provider/openrouter.py`, `[hardening]` config (redact_secrets + max_findings budget;
agentic spend bounded by `[harnessx]`), `docs/threat-model.md`, `docs/examples.md`.
Tests in `tests/test_redaction.py`, `tests/test_provider_retry.py`,
`tests/test_hardening_pipeline.py`. CI gates pre-existed (ruff/mypy/pytest/detection
gate/dagger).
- [x] Add cost budget enforcement.
- [x] Add model call retry and backoff policies.
- [x] Add redaction for secrets in traces and reports.
- [x] Add docs for threat model and sandbox limits.
- [x] Add end-to-end examples.
- [x] Add lint, typecheck, test, and CI gates.
+7 -7
View File
@@ -25,8 +25,8 @@ Fix the scorer and labels first (no engine change). Anchor harvest at the declar
## Scope
- **In**: scorer fidelity; label schema; harvest anchor and modes; corpus hygiene for sast; three new slices; dataset pointers; provenance fingerprint; per-profile and per-mechanism dashboard; per-profile improve-loop gate; offline tests.
- **Out**: any change to taint, facts, CST walker, or dispositions (engine specs); tolerating tree-sitter ERROR nodes (engine); BaxBench generation and exploit-labelled slices (needs deep mode; later); profile-specific fact tables; making any dashboard a merge gate; vendoring SecBench.js test files (no license).
- In: scorer fidelity; label schema; harvest anchor and modes; corpus hygiene for sast; three new slices; dataset pointers; provenance fingerprint; per-profile and per-mechanism dashboard; per-profile improve-loop gate; offline tests.
- Out: any change to taint, facts, CST walker, or dispositions (engine specs); tolerating tree-sitter ERROR nodes (engine); BaxBench generation and exploit-labelled slices (needs deep mode; later); profile-specific fact tables; making any dashboard a merge gate; vendoring SecBench.js test files (no license).
## Boundary Candidates
@@ -42,14 +42,14 @@ Fix the scorer and labels first (no engine change). Anchor harvest at the declar
## Upstream / Downstream
- **Upstream**: pair catalog schema; overlay eval path; `build_pair_signals`; `run_improvement` accept gate; preprocess tags.
- **Downstream**: `overlay-ir-completeness` and `guard-dominance-regime` measure against these slices and vocabularies; later `baxgen` slice reuses provenance and mechanism fields.
- Upstream: pair catalog schema; overlay eval path; `build_pair_signals`; `run_improvement` accept gate; preprocess tags.
- Downstream: `overlay-ir-completeness` and `guard-dominance-regime` measure against these slices and vocabularies; later `baxgen` slice reuses provenance and mechanism fields.
## Existing Spec Touchpoints
- **Supersedes**: `real-world-vfc-slice` (corpus/scorer plane). Its seed pairs, harvest script, and dual scoreboard are kept and extended.
- **Extends**: roadmap direct candidate "record inventory vs overlay Youden side-by-side in sast JSON".
- **Adjacent**: `propose-adjudicate-prove` (one additive field on `OverlayRecord`), `harnessx-self-improving-rulesets` (accept gate grows a per-profile clause, ledger format unchanged).
- Supersedes: `real-world-vfc-slice` (corpus/scorer plane). Its seed pairs, harvest script, and dual scoreboard are kept and extended.
- Extends: roadmap direct candidate "record inventory vs overlay Youden side-by-side in sast JSON".
- Adjacent: `propose-adjudicate-prove` (one additive field on `OverlayRecord`), `harnessx-self-improving-rulesets` (accept gate grows a per-profile clause, ledger format unchanged).
## Constraints
+5 -5
View File
@@ -172,11 +172,11 @@ tests/
### Modified Files
- `src/openultrasast/pairs.py` — see above; `OVERLAY_SLICES` grows; `PairCase` gains `provenance`, `mechanism` (on expected), `split`, `known_limit`.
- `src/openultrasast/semantic/overlay.py` — fill `function` from the `FunctionIR` whose range contains the record line; default `None`.
- `src/openultrasast/improve/evolve.py` — `_Eval` computes `per_profile` on holdout; new reject reason `profile_regression:<profile>`.
- `src/openultrasast/preprocess.py`, `reports.py`, `cli.py` — wiring only.
- `benchmarks/pairs/README.md`, `.kiro/steering/roadmap.md` — slice list and spec order.
- `src/openultrasast/pairs.py`: see above; `OVERLAY_SLICES` grows; `PairCase` gains `provenance`, `mechanism` (on expected), `split`, `known_limit`.
- `src/openultrasast/semantic/overlay.py`: fill `function` from the `FunctionIR` whose range contains the record line; default `None`.
- `src/openultrasast/improve/evolve.py`: `_Eval` computes `per_profile` on holdout; new reject reason `profile_regression:<profile>`.
- `src/openultrasast/preprocess.py`, `reports.py`, `cli.py`: wiring only.
- `benchmarks/pairs/README.md`, `.kiro/steering/roadmap.md`: slice list and spec order.
## System Flows
@@ -8,9 +8,9 @@ Engineers evolving OpenUltraSAST's overlay cannot trust the pair scoreboard: cov
## Boundary Context
- **In scope**: pair scorer fidelity; catalog label schema (function, mechanism, provenance, known limits, held-out split); harvest anchoring and extraction modes; sast corpus hygiene; `vibe-py`, `vfc-js`, `agent-vfc` slices; dataset pointers; deterministic provenance fingerprint at scan time; per-profile and per-mechanism dashboard; per-profile acceptance in the improve loop; offline tests.
- **Out of scope**: taint, facts, CST walker, dispositions, or tolerance of parser ERROR nodes (engine specs); BaxBench generation slices; profile-specific fact tables or thresholds; any new merge gate; vendoring files without a stated license; LLM-based classification.
- **Adjacent expectations**: `local` stays the 100% pair gate; the stage-1 smoke gate stays inventory-only; `OverlayRecord` may gain one optional additive field; the improve loop's existing accept gate (recall floor, FP ceiling, score, no matched regression) stays and grows one clause; ledger file formats do not change.
- In scope: pair scorer fidelity; catalog label schema (function, mechanism, provenance, known limits, held-out split); harvest anchoring and extraction modes; sast corpus hygiene; `vibe-py`, `vfc-js`, `agent-vfc` slices; dataset pointers; deterministic provenance fingerprint at scan time; per-profile and per-mechanism dashboard; per-profile acceptance in the improve loop; offline tests.
- Out of scope: taint, facts, CST walker, dispositions, or tolerance of parser ERROR nodes (engine specs); BaxBench generation slices; profile-specific fact tables or thresholds; any new merge gate; vendoring files without a stated license; LLM-based classification.
- Adjacent expectations: `local` stays the 100% pair gate; the stage-1 smoke gate stays inventory-only; `OverlayRecord` may gain one optional additive field; the improve loop's existing accept gate (recall floor, FP ceiling, score, no matched regression) stays and grows one clause; ledger file formats do not change.
## Requirements
@@ -23,7 +23,7 @@ Engineers evolving OpenUltraSAST's overlay cannot trust the pair scoreboard: cov
1. When an overlay slice pair is scored, the system shall count a `coverage` record that carries a source-to-sink flow as a detection on the vulnerable side and as a leak on the fixed side, symmetrically with `promote`.
2. When an expected row names a function, the system shall match a promotion or coverage record only if the record lies inside that function's line range.
3. When an expected row names neither a rule id nor a sink nor a function, the system shall not count a CWE-only match as a detection and shall flag the row as `weak_label` in the payload.
4. When any overlay slice is evaluated, the system shall report inventory and overlay metrics side by side for that slice, not only for `vfc`.
4. When any overlay slice is evaluated, the system shall report inventory and overlay metrics side by side for that slice as well as for `vfc`.
5. When a pair is evaluated, the system shall report how many files on each side were `parse_failed` and how many proposals were `unadjudicated`, so parser loss is visible on the dashboard.
6. The system shall keep `local` as the only slice whose result can fail CI.
7. Where a hunter model is configured, the system shall report a third scorer, `hunter`, that runs the tool hunter on each isolated pair function and scores it with the same pair rules, so the LLM plane is measured on the same corpus; absence of a model shall skip the scorer with a recorded degradation.
+47 -47
View File
@@ -2,9 +2,9 @@
## Summary
- **Feature**: `pair-corpus-honesty`
- **Discovery Scope**: Extension of the pair catalog, scorer, and harvest; supersedes `real-world-vfc-slice`
- **Key Findings**:
- Feature: `pair-corpus-honesty`
- Discovery Scope: Extension of the pair catalog, scorer, and harvest; supersedes `real-world-vfc-slice`
- Key Findings:
- The overlay scorer ignores `coverage`. The Java sqli vuln file already produces a coverage record on `prepareCall` from a servlet source; the scorer reports MISS.
- The overlay column equals the inventory column on all 176 vfc pairs because C facts have no memory sink, so every proposal is `flow_incomplete` and the scorer falls back to inventory. The dashboard cannot show engine progress until the scorer reports parser and adjudication loss explicitly.
- Harvest anchors on the first textual occurrence of the function name. For curl that is the doc comment mention (`Curl_follow()`), so 25 pairs start mid-comment and fail to parse.
@@ -18,23 +18,23 @@
### Scorer blind spots
- **Context**: Why the sast overlay number did not move after the tree-sitter extra.
- **Sources Consulted**: `src/openultrasast/pairs.py` (`_evaluate_overlay_pair`, `_overlay_matches_expected`, `_overlay_leaks`), `semantic/overlay.py`, direct runs of `adjudicate` on the sast files.
- **Findings**: only `promote` is counted; `coverage` is excluded on both sides. `_overlay_matches_expected` accepts a CWE-only match. `ExpectedFinding` already has `function` and `line`, and `benchmark._matches_expected` honors them, but the overlay path does not. `OverlayRecord` has no function field; `FunctionIR` has line ranges.
- **Implications**: count coverage symmetrically; add optional `function` to `OverlayRecord`; require function or sink or rule id for a match when present; flag CWE-only rows.
- Context: Why the sast overlay number did not move after the tree-sitter extra.
- Sources Consulted: `src/openultrasast/pairs.py` (`_evaluate_overlay_pair`, `_overlay_matches_expected`, `_overlay_leaks`), `semantic/overlay.py`, direct runs of `adjudicate` on the sast files.
- Findings: only `promote` is counted; `coverage` is excluded on both sides. `_overlay_matches_expected` accepts a CWE-only match. `ExpectedFinding` already has `function` and `line`, and `benchmark._matches_expected` honors them, but the overlay path does not. `OverlayRecord` has no function field; `FunctionIR` has line ranges.
- Implications: count coverage symmetrically; add optional `function` to `OverlayRecord`; require function or sink or rule id for a match when present; flag CWE-only rows.
### Harvest anchor defect
- **Context**: 50 vfc files `parse_failed`.
- **Sources Consulted**: `benchmarks/pairs/vfc/harvest.py` `extract_function`; tree-sitter ERROR nodes on `curl-cve-2022-27774/vuln.c` and `curl-cve-2023-38545-do_socks5/vuln.c`.
- **Findings**: the name search accepts a match followed by `(` even inside a comment, and the brace search then finds the real body. A second, smaller class is `#if`-split `else if` chains, which tree-sitter cannot attach; that is an engine tolerance question, not harvest.
- **Implications**: skip comment and string regions before searching; anchor on a line that starts a declarator; add line-range and hunk modes so Python and JS/TS are covered; report parse loss on the dashboard so the engine spec can measure its tolerance work.
- Context: 50 vfc files `parse_failed`.
- Sources Consulted: `benchmarks/pairs/vfc/harvest.py` `extract_function`; tree-sitter ERROR nodes on `curl-cve-2022-27774/vuln.c` and `curl-cve-2023-38545-do_socks5/vuln.c`.
- Findings: the name search accepts a match followed by `(` even inside a comment, and the brace search then finds the real body. A second, smaller class is `#if`-split `else if` chains, which tree-sitter cannot attach; that is an engine tolerance question, not harvest.
- Implications: skip comment and string regions before searching; anchor on a line that starts a declarator; add line-range and hunk modes so Python and JS/TS are covered; report parse loss on the dashboard so the engine spec can measure its tolerance work.
### Candidate corpora
- **Context**: User asked for vibe-coded and Node/TypeScript/Python web corpora.
- **Sources Consulted**: Real-Vuln-Benchmark repo and paper; SecBench.js repo and ICSE 2023 paper; AIDev dataset card and paper; BaxBench site, repo, and paper; SecurityEval, CodeSecEval, CWEval; CyberSecEval benchmarks README; VibeApps paper; TS-VulBench page; SecureVibeBench repo; GitHub commit search via `gh api search/commits`.
- **Findings**:
- Context: User asked for vibe-coded and Node/TypeScript/Python web corpora.
- Sources Consulted: Real-Vuln-Benchmark repo and paper; SecBench.js repo and ICSE 2023 paper; AIDev dataset card and paper; BaxBench site, repo, and paper; SecurityEval, CodeSecEval, CWEval; CyberSecEval benchmarks README; VibeApps paper; TS-VulBench page; SecureVibeBench repo; GitHub commit search via `gh api search/commits`.
- Findings:
| Corpus | Languages | Labels | Fixed twin | License | Verdict |
|---|---|---|---|---|---|
| Real-Vuln-Benchmark | Python web | file, line range, function, CWEs, authorship, FP traps | traps | Apache-2.0 | `vibe-py` |
@@ -46,14 +46,14 @@
| VibeApps/VibeVulns | TS, JS, React | not released | no | n/a | method only |
| TS-VulBench | TS synthetic | paywalled | no | unknown | skip |
| SecureVibeBench | C/C++ | agent tasks | n/a | MIT | skip for web |
- **Implications**: three slices now; BaxBench after deep mode is stable; CyberSecEval never as recall ground truth.
- Implications: three slices now; BaxBench after deep mode is stable; CyberSecEval never as recall ground truth.
### Classification and branching
- **Context**: User expects tuning to split into branches per corpus class.
- **Sources Consulted**: `improve/evolve.py` accept gate; `ruleset/store.py` ledger; `calibration.py` scope; `preprocess.detect_tags`; `build_pair_signals`.
- **Findings**: the accept gate already ANDs four clauses and reverts byte-for-byte. Pair signals carry pair name but no profile. Ledger is one file per target. Preprocess tags are deterministic and per file.
- **Implications**: classify on two axes (provenance for gating, mechanism for reporting). Branch the acceptance decision, not the ledger format: reject a change if any profile's holdout regresses. Profile-specific facts and thresholds are deferred to the engine specs where those data live.
- Context: User expects tuning to split into branches per corpus class.
- Sources Consulted: `improve/evolve.py` accept gate; `ruleset/store.py` ledger; `calibration.py` scope; `preprocess.detect_tags`; `build_pair_signals`.
- Findings: the accept gate already ANDs four clauses and reverts byte-for-byte. Pair signals carry pair name but no profile. Ledger is one file per target. Preprocess tags are deterministic and per file.
- Implications: classify on two axes (provenance for gating, mechanism for reporting). Branch the acceptance decision, not the ledger format: reject a change if any profile's holdout regresses. Profile-specific facts and thresholds are deferred to the engine specs where those data live.
## Architecture Pattern Evaluation
@@ -68,48 +68,48 @@
### Decision: Count coverage-with-flow as detection and as leak
- **Context**: Sink aliases will always land as coverage.
- **Alternatives Considered**: promote coverage to `promote` in the engine; count coverage only on the vulnerable side.
- **Selected Approach**: scorer treats `coverage` with a non-empty source the same as `promote` on both sides.
- **Rationale**: `propose-adjudicate-prove` already states coverage enters the same state machine. Asymmetry would inflate recall.
- **Trade-offs**: some pairs move from MISS to LEAK; that is the honest reading.
- Context: Sink aliases will always land as coverage.
- Alternatives Considered: promote coverage to `promote` in the engine; count coverage only on the vulnerable side.
- Selected Approach: scorer treats `coverage` with a non-empty source the same as `promote` on both sides.
- Rationale: `propose-adjudicate-prove` already states coverage enters the same state machine. Asymmetry would inflate recall.
- Trade-offs: some pairs move from MISS to LEAK; that is the honest reading.
### Decision: Function-scoped matching with weak-label flagging
- **Context**: CWE-only matching over-counts.
- **Selected Approach**: expected rows carry `function`; match requires the record inside that function's lines; rows with no function, sink, or rule id are `weak_label` and never match by CWE alone.
- **Trade-offs**: vfc recall may drop on first run; the drop is real.
- Context: CWE-only matching over-counts.
- Selected Approach: expected rows carry `function`; match requires the record inside that function's lines; rows with no function, sink, or rule id are `weak_label` and never match by CWE alone.
- Trade-offs: vfc recall may drop on first run; the drop is real.
### Decision: Provenance is deterministic and closed
- **Context**: No LLM in classification.
- **Selected Approach**: signals from `.claude/`, `CLAUDE.md`, `AGENTS.md`, `.cursor/`, `.github/copilot-instructions.md`, Lovable meta tag, "Generated with Claude Code", and co-author trailers for Claude, Codex, Copilot, Cursor, Devin, Jules when a git log is readable. Threshold for `agent` on a git tree: at least 85% of commits or lines in the last N commits carry a signal, following the VibeApps method; otherwise `mixed` when any signal exists; `human` when none; `synthetic` only from catalog declaration.
- **Trade-offs**: repos that scrub trailers read as human. Acceptable; the label is a profile, not a verdict.
- Context: No LLM in classification.
- Selected Approach: signals from `.claude/`, `CLAUDE.md`, `AGENTS.md`, `.cursor/`, `.github/copilot-instructions.md`, Lovable meta tag, "Generated with Claude Code", and co-author trailers for Claude, Codex, Copilot, Cursor, Devin, Jules when a git log is readable. Threshold for `agent` on a git tree: at least 85% of commits or lines in the last N commits carry a signal, following the VibeApps method; otherwise `mixed` when any signal exists; `human` when none; `synthetic` only from catalog declaration.
- Trade-offs: repos that scrub trailers read as human. Acceptable; the label is a profile, not a verdict.
### Decision: Branch the accept decision, not the data
- **Context**: Overfitting risk with per-slice tuning.
- **Selected Approach**: `run_improvement` evaluates holdout pairs per profile; any profile regression beyond tolerance rejects. Profiles capped at four. Minimum holdout size reported.
- **Follow-up**: profile-keyed facts land with the engine specs, gated by this clause.
- Context: Overfitting risk with per-slice tuning.
- Selected Approach: `run_improvement` evaluates holdout pairs per profile; any profile regression beyond tolerance rejects. Profiles capped at four. Minimum holdout size reported.
- Follow-up: profile-keyed facts land with the engine specs, gated by this clause.
## Risks & Mitigations
- Recall numbers drop after function-scoped matching and weak-label flags — expected; record before and after in the roadmap.
- SecBench.js has no license — never vendor its files; harvest from upstream package repos and record each package license.
- Agent-authored commits may be false labels (a "fix" that was not a vuln) — human review required; keep `agent-vfc` small.
- Real-Vuln-Benchmark apps are educational and CTF style — say so in the slice README; do not treat `vibe-py` as production.
- Held-out halves are small on day one — report minimum-size warnings; do not gate on a profile under the minimum.
- Recall numbers drop after function-scoped matching and weak-label flags; expected; record before and after in the roadmap.
- SecBench.js has no license; never vendor its files; harvest from upstream package repos and record each package license.
- Agent-authored commits may be false labels (a "fix" that was not a vuln); human review required; keep `agent-vfc` small.
- Real-Vuln-Benchmark apps are educational and CTF style; say so in the slice README; do not treat `vibe-py` as production.
- Held-out halves are small on day one; report minimum-size warnings; do not gate on a profile under the minimum.
## References
- [Real-Vuln-Benchmark](https://github.com/kolega-ai/Real-Vuln-Benchmark) — Apache-2.0, manifest with pinned SHAs, ground-truth schema
- [RealVuln paper](https://arxiv.org/abs/2604.13764) — 26 human plus 40 LLM-generated Python repos, FP traps
- [SecBench.js](https://github.com/cristianstaicu/SecBench.js) — 600 npm vulns, `fixCommit`, `sinkLocation`
- [Real-Vuln-Benchmark](https://github.com/kolega-ai/Real-Vuln-Benchmark): Apache-2.0, manifest with pinned SHAs, ground-truth schema
- [RealVuln paper](https://arxiv.org/abs/2604.13764): 26 human plus 40 LLM-generated Python repos, FP traps
- [SecBench.js](https://github.com/cristianstaicu/SecBench.js): 600 npm vulns, `fixCommit`, `sinkLocation`
- [SecBench.js paper](https://software-lab.org/publications/icse2023_SecBenchJS.pdf)
- [AIDev dataset](https://huggingface.co/datasets/hao-li/AIDev) — agent-authored PRs, CC-BY-4.0 index
- [BaxBench](https://github.com/logic-star-ai/baxbench) — MIT, scenarios and exploits, generation by CLI
- [AIDev dataset](https://huggingface.co/datasets/hao-li/AIDev): agent-authored PRs, CC-BY-4.0 index
- [BaxBench](https://github.com/logic-star-ai/baxbench): MIT, scenarios and exploits, generation by CLI
- [SecurityEval](https://github.com/s2e-lab/SecurityEval), [CodeSecEval](https://arxiv.org/pdf/2407.02395), [CWEval](https://github.com/Co1lin/CWEval)
- [CyberSecEval](https://github.com/meta-llama/PurpleLlama/tree/main/CybersecurityBenchmarks) — labels defined by ICD rules
- [VibeApps paper](https://arxiv.org/html/2606.23130v1) — identification method
- `.kiro/specs/real-world-vfc-slice/` — superseded corpus spec
- Memory note `overlay-gaps-diagnosis-2026-09` — measured state and engine gaps
- [CyberSecEval](https://github.com/meta-llama/PurpleLlama/tree/main/CybersecurityBenchmarks): labels defined by ICD rules
- [VibeApps paper](https://arxiv.org/html/2606.23130v1): identification method
- `.kiro/specs/real-world-vfc-slice/`: superseded corpus spec
- Memory note `overlay-gaps-diagnosis-2026-09`: measured state and engine gaps
+21 -21
View File
@@ -206,25 +206,25 @@ URL; the CLI mints them and polls the store. The URLs travel in the start body,
"inputs": {"CANDIDATES": "<presigned GET runs/<run>/<producer>/<artifact>>"}}}
```
- **Why the start body.** A presigned URL is a bearer capability, so the credentials-only-in-the-start-request
- Why the start body. A presigned URL is a bearer capability, so the credentials-only-in-the-start-request
rule applies; env would bake it into the ActorTemplate and hand it to the golden boot; and the body has no
32 KB ceiling (Substrate rejects env values over 32768 characters, `reconciler.py:141`). `StartGate.offer`
(`runner.py:250-275`) validates `delivery` the way it validates `credentials`: `https` URLs only, hostnames,
no echo. Expiry = `OUSAST_TASK_TIMEOUT` (7200 s) + 600 s; a re-start after a re-resume re-mints.
- **Why presigned rather than a scoped delivery credential.** A per-task S3 key needs a per-task policy on RustFS,
- Why presigned rather than a scoped delivery credential. A per-task S3 key needs a per-task policy on RustFS,
gives the sandbox `ListBucket` and reads beyond its inputs, and would travel in the same body. Presigned URLs
name exactly the objects a task may write and read, which is what the receiver enforces today
(`Receiver.input_file`, `egress.py:230-247`).
- **Why not an in-cluster receiver Service.** It is a server to run, holds state in a process, and is reachable
- Why not an in-cluster receiver Service. It is a server to run, holds state in a process, and is reachable
only from a pod; that is the gap remote mode has. **Rejected.**
- **Objects.** `runs/<run>/<task>/output.tar` (the runner's PUT); after the CLI extracts it, the declared
- Objects. `runs/<run>/<task>/output.tar` (the runner's PUT); after the CLI extracts it, the declared
`outputs[]` are re-put as `runs/<run>/<task>/<artifact>` so a consumer's presigned GET names one artifact, and
`runs/<run>/<task>/done.json` carries the summary's status. The 30-day `runs/` expiry already applies; memory
rows are ingested from the local extraction by `remember` as today (f: annotations and `memory_key` seeding
unchanged, `generate.py:_chain_inputs`, `memory.seed`).
- **Completion.** `_await`'s `delivered()` becomes `Delivery.delivered(task)`: a HEAD on `output.tar`, polled at
- Completion. `_await`'s `delivered()` becomes `Delivery.delivered(task)`: a HEAD on `output.tar`, polled at
`OUSAST_POLL_SECONDS` (3 s). The runner's completion marker logic (`runner.py`, `_run_once`) is unchanged.
- **Inputs no task produces** (`candidates.json`, `functions.json`, `case.json`) may also be store objects
- Inputs no task produces (`candidates.json`, `functions.json`, `case.json`) may also be store objects
declared as inputs, which lifts the ~20 KB inline-files limit (`ops/ax/README.md:117`); Workspace `files`
stay for the smoke Task and small cases.
@@ -251,7 +251,7 @@ shim and `/workspace`), so one *contract* in two images: `ousast-runner` (slim,
2.8 GB and every golden snapshot carry it; the engine runs for a minority of cases); Docker-in-pod (no socket on
containerd; `Dockerfile:1-7` already argues against it).
- **Task.** Template `plane/tasks/engine.yaml`: `command: ["engine"]`, env `OUSAST_WORKSPACE_DIR`,
- Task. Template `plane/tasks/engine.yaml`: `command: ["engine"]`, env `OUSAST_WORKSPACE_DIR`,
`OUSAST_FIXED_DIR`, `OUSAST_INPUT_CASE`, pins and `OUSAST_FAMILIES`, bound to the same two Workspaces the
`alerts` task binds (`generate.py:342-353`). The module lifts `benchmarks/push/finding_dump.py`'s scan into
`openultrasast.cpg.dump` (the image ships `src/`, not `benchmarks/`) and reuses `engine_alerts.engine_rows`,
@@ -259,7 +259,7 @@ containerd; `Dockerfile:1-7` already argues against it).
`alerts.jsonl` rows (`rule_id: engine:<family>`, `source: "engine"`) and `summary.json` (coverage with the
engine's languages, files and bytes read, questions, seconds, degradations per pin) as `alerts-engine`, with
`host: false`. The instrument rule stays: no `read N files` line or `questions == 0` fails the task loudly.
- **Sizing from the record** (`benchmarks/measurements/2026-09-30-php-engine-alerts/record.json`): container
- Sizing from the record (`benchmarks/measurements/2026-09-30-php-engine-alerts/record.json`): container
peak 2.129 GiB under a 3 GiB limit, 789 s for both pins of froxlor, deadline 1800 s per pin. Task resources:
requests 3 Gi / 1 CPU, limits 4 Gi / 2 CPU; `engine-pool` workers 4 Gi / 2 CPU, replicas 1 on kind (the 7 GB
host holds it only with `ousast-pool` at one worker), production replicas in `k3s.toml`. Task timeout: two pins
@@ -314,23 +314,23 @@ runbook: a Run with `--workers N` beyond the pool's replicas queues in ax as Sus
`plane/scan.py`, reusing `generate.py`'s render path with a new case source:
1. **Workspace generation** (`generate.scan_cases(url, base, head, cache)`): a shallow clone into the case cache
1. Workspace generation (`generate.scan_cases(url, base, head, cache)`): a shallow clone into the case cache
(`runner.find_cached_checkout` layout), two pinned Workspaces `<repo_dir>-<head7>` and `<repo_dir>-<base7>`
(`workspaces.py` shape, pins in `openultrasast.io/git-commits`), repository key by `memory.repo_key`.
2. **Candidates from changed functions**: `generate.fix_ranges(repo, base, head, side="new")` gives the head-side
2. Candidates from changed functions: `generate.fix_ranges(repo, base, head, side="new")` gives the head-side
hunks (the `push/` delta primitive `_diff_records` is the same `git diff -U0`), `repo_facts.enclosing` names
the function per hunk line, filtered to `repo_facts.product_files`. Quick rules on the changed files
(`tasks.alerts.scan`) add alerted functions; `roles` runs once per repository when memory has no `roles` rows
for it; `repo-facts` supplies callers. `candidates.json` and `functions.json` are store inputs (section 3).
3. **Run composition**: per changed-function set `facts -> va, vb -> agree -> vc -> final -> features ->
3. Run composition: per changed-function set `facts -> va, vb -> agree -> vc -> final -> features ->
remember`, plus `engine` when a changed file's language is engine-covered and not quick-covered, or PHP
(`ENGINE_ALONGSIDE_QUICK`). Budgets from `generate.verify_budget` (0.022 USD per candidate, headroom 1.5);
`--ceiling` caps the sum. `--all` (whole repository, secondary) takes every product function.
4. **Results** land in memory under `repos/<host>__<owner>__<name>/<head>.jsonl` through `remember` and
4. Results land in memory under `repos/<host>__<owner>__<name>/<head>.jsonl` through `remember` and
`ousast plane remember` (rows already carry `repo` and `pin`, `memory.py:77`); `runs/<run>/` holds the raw
artifacts for 30 days. The report prints the attribution table and the plane's agreement (`final`); the
decision engine's verdict only when a model is adopted (learned-decision-engine Req 6 unchanged).
5. **Latency split (e).** `ousast pre-push` stays the blocking hook: quick rules and the delta engine inside
5. Latency split (e). `ousast pre-push` stays the blocking hook: quick rules and the delta engine inside
`OUSAST_PUSH_DEADLINE` (30 s, `ops/README.md:322`). `ousast plane scan --exec remote --no-wait` submits and
prints the Run name in the hook's report; `--wait SECONDS` blocks for CI. `ousast plane status <run>` reports
later. The wrapper `ops/pre-push` gains an opt-in `OUSAST_PLANE_SCAN=1`; it never blocks on the plane.
@@ -378,7 +378,7 @@ template names `imagePullSecret` from the profile.
- `tests/test_plane_engine_task.py`: fixture record -> rows and summary equal `engine_alerts.produce`'s shape;
the instrument failures.
- `tests/test_plane_scan.py`: candidates from a two-commit fixture repository; Run composition; budgets.
- **Only live on kind:** placement on `engine-pool`; Substrate accepting the SA token for `egress-policy`;
- Only live on kind: placement on `engine-pool`; Substrate accepting the SA token for `egress-policy`;
the gateway passing TLS to the store host; GHCR pulls; the HPA (k3s only). These are the kind proofs above.
## Data Models
@@ -411,16 +411,16 @@ from `StartGate.offer`. The engine task fails loudly on an unread input, as `rea
## Open Questions
1. **Substrate authorization for the CI token.** `kubectl-ate` sends a ServiceAccount token; which audience and
1. Substrate authorization for the CI token. `kubectl-ate` sends a ServiceAccount token; which audience and
which Substrate-side grant let `ousast-ci` create `egress-policy` in `default` is not in the checkouts read
here. Proven or refuted in increment 3.
2. **Engine placement.** Whether Substrate places a 3 Gi actor only on workers that fit it (section 4). The
2. Engine placement. Whether Substrate places a 3 Gi actor only on workers that fit it (section 4). The
fallback is named.
3. **gVisor tarball reachability on k3s** (`gs://gvisor/...` in the SandboxConfig): mirror or egress needed.
4. **Store endpoint hostname** for any future in-cluster RustFS; today's external endpoint is fine.
5. **Two images under one contract** versus the maintainer's "one runner image for all tasks": the design
3. gVisor tarball reachability on k3s (`gs://gvisor/...` in the SandboxConfig): mirror or egress needed.
4. Store endpoint hostname for any future in-cluster RustFS; today's external endpoint is fine.
5. Two images under one contract versus the maintainer's "one runner image for all tasks": the design
reads it as one contract; confirm.
6. **GHCR visibility** (public packages need no pull secret) and whether Substrate's and ax's `ko` images go to
6. GHCR visibility (public packages need no pull secret) and whether Substrate's and ax's `ko` images go to
the same registry namespace.
7. **Reconciler size**: remote mode and `Delivery` must stay under the 500-line test; if they do not, the test's
7. Reconciler size: remote mode and `Delivery` must stay under the 500-line test; if they do not, the test's
bound moves with a reason, not silently.
@@ -9,24 +9,24 @@ production entry point (a delta Run for a push) is added. Context: `brief.md`, `
## Boundary Context
- **In scope**: two profiles of one code base, `kind` (local, kept) and `k3s` (production); a remote execution mode of the CLI; artifacts delivered to the store; configurable
- In scope: two profiles of one code base, `kind` (local, kept) and `k3s` (production); a remote execution mode of the CLI; artifacts delivered to the store; configurable
cluster context, registry and image pins; our own snapshot and memory buckets; the Joern engine as a plane task
on a larger worker pool; worker pool sizing and autoscaling; a delta Run command and Workspaces from URL +
commit; a deployment runbook with a proof on kind and a checklist for the target cluster.
- **Out of scope**: provisioning or operating the target cluster; modifying Agent Substrate or ax; any change to
- Out of scope: provisioning or operating the target cluster; modifying Agent Substrate or ax; any change to
detection, scoring, populations or the decision engine; replacing the `ax` CLI with a bespoke client beyond what
remote submission needs.
- **Adjacent expectations**: `ai-service-plane` requirements (one Model + budget per task, attribution,
- Adjacent expectations: `ai-service-plane` requirements (one Model + budget per task, attribution,
credentials only in the start request, egress policies, verify-at-startup memory store) keep holding;
`harnessx-removal` Req 6 (memory on the S3 store) keeps holding; nothing is deleted before its replacement
passes its test (the `harnessx-removal` sequencing rule).
## Target Environment (maintainer, 2026-10-02)
- **Cluster: k3s.** Consequences: containerd (no Docker socket; the engine's host-Docker path does not exist there),
- Cluster: k3s. Consequences: containerd (no Docker socket; the engine's host-Docker path does not exist there),
gVisor needs `runsc` and a `RuntimeClass` installed on the nodes Substrate's workers run on, k3s ships Traefik
and local-path storage, and single-node k3s is close enough to kind that the kind proof transfers.
- **Images and source: GitHub.** Images are published to GitHub Container Registry (`ghcr.io/norandom/...`),
- Images and source: GitHub. Images are published to GitHub Container Registry (`ghcr.io/norandom/...`),
built by GitHub Actions from tagged commits, digest-pinned in the manifests; private packages need an
`imagePullSecret` on the worker ServiceAccounts and the CI principal. Substrate's and ax's own images are built with
`ko` from their checkouts and pushed to the same registry.
+1 -1
View File
@@ -69,7 +69,7 @@ family semantics remain with their existing specs rather than being reimplemente
- No promise of all-language coverage; capabilities are admitted per language/framework/family.
- Initial work must establish feasibility and noise before investing in an LLM ranker.
## Cross-language amendment — 2026-09-12
## Cross-language amendment, 2026-09-12
Keep PMPro as the primary worked optimization example, and require a JavaScript/Express/Node API
transfer experiment before claiming the core improvement generalizes. Abstract evidence, ranking,
+4 -4
View File
@@ -24,7 +24,7 @@ an irrelevant old defect, or an unactionable witness can still produce noise.
- Do not build a second ranker/scope heuristic, an incremental CPG engine, a daemon, an automatic fixer,
or a mandatory model service. Do not scan intermediate pushed history in this feature.
## What this hook is, and is not — 2026-09-21
## What this hook is, and is not, 2026-09-21
A general-purpose pre-commit hook, not an application-specific one. It may be measured on WordPress
plugins and Node applications, and it may not become a tool that only works on them. The practical
@@ -63,8 +63,8 @@ The fourth and fifth cost the declared v0.1 target its entire admissibility: a W
PHP plus admin JavaScript, so the product could detect defects on its own target and never admit
one. Neither passing subject could reveal it, because both hold one language.
**In review:** every new degradation reason must state the scope it reaches — one question, one file,
one partition, or the whole read — and a reason that demotes an outcome must justify reaching that
**In review:** every new degradation reason must state the scope it reaches (one question, one file,
one partition, or the whole read), and a reason that demotes an outcome must justify reaching that
far. A gap that costs coverage without threatening correctness is a declared exclusion: it stays
published and it does not demote.
@@ -462,7 +462,7 @@ decision in the terminal; do not fabricate a successful cache entry or hang whil
separate from a future C/C++ positive control. See `evaluation.md` for selection and labeling gates.
Measure the accepted experimental acceptance profile in requirements on declared hardware and fixed workloads.
Warm means prior reusable work from the preceding state, not only repeating the identical tip.
Warm means prior reusable work from the preceding state. Repeating the identical tip alone is insufficient.
Report identical-tip hits separately from a one-function change and a dependency/configuration
change. Include first installs, cold graphs, growing repository size, multi-ref pushes and incomplete
outcomes. Freeze the applicable population before ranker selection; deferred questions count against
@@ -84,3 +84,15 @@ to remove Node from the evaluation or silently fall back to a PHP-only release c
## Task 1.2 input preparation
The versioned push manifest records upstream PMPro vulnerable/fixed revisions, NodeGoat with an explicitly authored eval-removal repair, and comment-only benign changes that retain vulnerable backlog. Ghost is reserved independently for production Node module/async API evaluation; its reviewed supported-family pair population remains missing. The validator verifies immutable Git blob identities and source/license byte counts offline, retains every unresolved case, and keeps all capabilities experimental. Independent PHP and Python admission populations remain prerequisites; VAmPI regressions and the libpng unsupported-property envelope retain their separate roles. See `benchmarks/push/README.md` for the reproducible command and provenance boundaries.
## Measurement schema and admission separation (amended 2026-10-30)
- Every detection measurement on this workload matrix publishes one fixed schema: population
and freeze digests, judgment vocabulary, recall / false alarms per 100 / precision / pair
correctness each with sample counts and Wilson 95%, coverage, instrument failures, and
runtime. A change of population or vocabulary starts a new baseline; cross-vocabulary
trend claims are invalid.
- M4 qualification reports per family scope (which families the capability claims) and per
untouched population, through the Req 12 admission workflow. No tuning on admission labels.
- Stop conditions per `steering/detection-quality.md`: two consecutive admission NO-GOs on
the same capability park it for a strategy review; no incremental patching of parked arms.
@@ -68,7 +68,7 @@ real capability; task 8.3 still owns actual eligibility. Task 4.2 brings progres
The agent service rejected the task 4.3 dispatch with `agent thread limit reached`.
The [kiro-impl skill](../../../.agents/skills/kiro-impl/SKILL.md) explicitly provides:
“If multi-agent is not available, fall back to manual mode execution for all tasks.”
"If multi-agent is not available, fall back to manual mode execution for all tasks."
Implementation, review and completion checks therefore ran in the main context.
This is a manual review record, not an independent-review claim.
@@ -1,6 +1,6 @@
# Implementation verification: group 5
Group 5 was authorized by the maintainer's “approved” after group 4 completion.
Group 5 was authorized by the maintainer's "approved" after group 4 completion.
Tasks 5.1–5.2 are implemented and verified (16/27 total); the measured feasibility decision is NO-GO.
Real capability eligibility and rollout readiness remain unproven.
@@ -8,7 +8,7 @@ Real capability eligibility and rollout readiness remain unproven.
Fresh implementer dispatch for 5.1 returned `agent thread limit reached`.
The [Kiro implementation skill](../../../.agents/skills/kiro-impl/SKILL.md) explicitly
states: “If multi-agent is not available, fall back to manual mode execution for all tasks.”
states: "If multi-agent is not available, fall back to manual mode execution for all tasks."
Its manual review rule permits review in the main context when a fresh reviewer is unavailable.
This run therefore uses manual implementation and adversarial review, not independent review.
@@ -300,15 +300,15 @@ and its [bundle](../../../benchmarks/measurements/2026-09-16-nodegoat-m1b-explai
Three evaluation findings were rendered on the vulnerable revision, one per eval site. For
line 32:
- **Location** app/routes/contributions.js:32, family injection, novelty new with
- Location: app/routes/contributions.js:32, family injection, novelty new with
`operation_absent_from_comparable_base`.
- **Witness** `eval(req.body.preTax) -> const preTax = eval(req.body.preTax)`.
- **Change attribution** `base:app/routes/contributions.js:32-34`.
- **Provenance** `file_scope`: the question named no function, the engine reported `<lambda>2`.
- **Consequence** "A request field reaches const preTax = eval(req.body.preTax), where it is
- Witness: `eval(req.body.preTax) -> const preTax = eval(req.body.preTax)`.
- Change attribution: `base:app/routes/contributions.js:32-34`.
- Provenance: `file_scope`: the question named no function, the engine reported `<lambda>2`.
- Consequence: "A request field reaches const preTax = eval(req.body.preTax), where it is
evaluated as JavaScript, so an attacker controlling that field can execute arbitrary code in
the server process."
- **Repair** "Replace the evaluation at const preTax = eval(req.body.preTax) with a numeric
- Repair: "Replace the evaluation at const preTax = eval(req.body.preTax) with a numeric
conversion such as Number(...) or parseInt(...), then validate the result before use."
Both sentences come from the declaration's templates through the grounded-template path, not from
@@ -64,9 +64,9 @@ code the precision estimate will move, against a bar of 95% with zero false aler
benign sides. Expect the first M4 evaluation to fail and budget for a second. That is the strongest
argument for decision 2.
## M1 — One real security change works end to end
## M1: One real security change works end to end
Status: **NEXT — not verified**. Dependencies: existing immutable replay and scan machinery.
Status: **NEXT, not verified**. Dependencies: existing immutable replay and scan machinery.
M1 is split into M1a (diagnose with vetoes recorded, not applied) and M1b (repair only
the vetoes M1a showed to matter). M1 is verified only when M1b passes.
@@ -80,7 +80,7 @@ a global completion veto, line ambiguity inherited by every question, and query
differing from witness identity. Requiring all four repaired before the first visible
result is a full production pass, not one detection.
### M1a — Show the detection with every veto recorded
### M1a: Show the detection with every veto recorded
Status: **MET 2026-09-16**, inside its one-week box. Evidence:
[M1a record](m1a-recorded-vetoes.md) and
@@ -112,9 +112,9 @@ source and census receipts. **Kill criterion:** if the fixed twin is not quiet u
recorded-veto run, M1 has a detector problem, and M1b does not start until that is
diagnosed and recorded.
### M1b — Repair only the vetoes M1a showed to matter
### M1b: Repair only the vetoes M1a showed to matter
Status: **NEXT — not verified**. M1a supplied the veto list: per-family change context,
Status: **NEXT, not verified**. M1a supplied the veto list: per-family change context,
question-owned vendor relevance, operation correspondence for an operation on a changed line,
file-wide correspondence ambiguity scope, and query-to-operation provenance. The owner
diagnosis expected the first and the last two; operation correspondence is the addition M1a
@@ -144,7 +144,7 @@ Owners: contributor-scan for query/operation/context evidence; pre-push-safety-n
comparison, admission, the recorded-veto flag and reporting. Requirements: 1.1–1.4,
2.1–2.4, 3.1–3.5, 6.3, 7.1–7.3.
## M2 — The same useful analysis fits the push budget
## M2: The same useful analysis fits the push budget
Status: **MEASURED AND UNMET, 2026-09-17.** Two attacks on engine startup were measured and
neither reaches the gate. A transaction-owned session is implemented and proven equivalent for
@@ -179,7 +179,7 @@ completion at least 95%, under the predeclared profile. A fast census alone does
Owner: contributor-scan runtime, with pre-push-safety-net deadline/cache integration.
Requirements: 4.1–4.4, 6.2–6.5, 7.3.
## M3 — The shared design transfers across languages
## M3: The shared design transfers across languages
Status: **PARTIAL, 2026-09-17. Not met.** Python is established and PHP is blocked by runtime.
@@ -218,7 +218,7 @@ remain a separate unsupported abstraction; adding a new engine is not a prerequi
Owners: contributor-scan shared evidence and framework adapters; pre-push-safety-net evaluation.
Requirements: 6.2–6.4, 8.1–8.4.
## M4 — Independent examples establish useful detection and low noise
## M4: Independent examples establish useful detection and low noise
Status: **PENDING M3**. Population preparation may proceed earlier, but evaluation follows
the frozen candidate. Reserve untouched cases before examining their scan results.
@@ -241,7 +241,7 @@ do not require every possible language or family to become eligible for a narrow
Owner: pre-push-safety-net task 8.3, consuming verified contributor-scan semantics.
Requirements: 6.1–6.5, 8.1–8.4 and the accepted experimental acceptance profile.
## M5 — Enable a narrow advisory rollout
## M5: Enable a narrow advisory rollout
Status: **PENDING M4**.
@@ -196,6 +196,6 @@ failures without lowering the bar after inspecting holdout results.
- >= 95% observed actionable-alert precision with counts and confidence interval; zero known false
alerts on the fixed/benign regression suite. Small samples do not establish broad precision.
- >= 90% recall on the predeclared supported security-change suite; unsupported classes remain in
coverage reporting, not reclassified after a miss. Publish per-capability results, not only an aggregate.
coverage reporting, not reclassified after a miss. Publish per-capability results; an aggregate alone is insufficient.
- No capability is admitted with zero positive controls, zero reviewed emitted alerts, or no untouched
evaluation repository. Failed profiles remain experimental and silent in the normal hook.
+1 -1
View File
@@ -89,7 +89,7 @@ feasibility gate. [Official CPG slicing documentation](https://docs.joern.io/cpg
2. **Separate claim from interruption.** Existing rungs describe analysis evidence. Hook admission
additionally requires evaluated capability, change attribution and actionability. This meets the
user's low-noise requirement without pretending an LLM confidence score proves a bug.
3. **Compare evidence, not just finding lists.** A missing base finding can mean truncated analysis.
3. **Compare evidence as well as finding lists.** A missing base finding can mean truncated analysis.
Targeted comparable base checks are required for novelty, including removed-guard cases.
4. **Adopt Git and the existing engine.** Build only the push transaction, cache and admission adapters.
Do not add an IR, database or service merely to describe future scale.

Some files were not shown because too many files have changed in this diff Show More