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]>
This commit is contained in:
Marius Ciepluch
2026-10-13 09:55:21 +02:00
co-authored by Claude Fable 5.1
parent e979fb0d95
commit d2010d0693
15 changed files with 1779 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Feature inventory, src/openultrasast (2026-10-13)
199 Python files, 51,860 lines (ast count; `wc -l` with blank lines says 60k). Discovery used an ast import
resolver over every module (about 690 internal edges). The four active lines come from
`.kiro/steering/overview.md`, section 2026-10-13: hunter-learning-loop, pre-push-safety-net,
search-with-proof, engine-change-scoped-analysis.
## Map units (what the flowcharts cover)
| Unit | Features | Purpose | Entry points | Core files | Lines | Active line |
|---|---|---|---|---|---|---|
| U1 Scan and reports | F1 | `ousast scan` quick/standard/deep, run artifacts, SARIF and markdown | cli.py:226 parser, cli.py:613 `_scan`, cli.py:619 `_run_scan`; mcp.py:62 | cli.py (613-1075), stages.py, run.py, harness.py, verification.py, fusion.py, reports.py, scoring/, calibration.py, provenance.py, profiling.py | ~2,000 + part of cli.py | none directly (feeds the pre-push quick tier) |
| U2 Shared core | F2 | file enumeration, regex quick rules, CWE policy, ranking, config, contracts, mapping | quick-rule pool `run_hunter_pool` cli.py:670 | config.py, contracts.py, redaction.py, preprocess.py, findings.py, rank.py, policy/, ruleset/, mapping.py, hunter.py (regex pool, not the LLM hunter) | ~3,030 | all, indirectly |
| U3 Pre-push hook | F3, F3b | advisory head-vs-base check on pushed commits; evaluation instruments | cli.py:143 parser, cli.py:523 `_hook_main`, cli.py:529 `_pre_push`; push/install.py; push_inputs.py:143, push_scoring.py:280 | push/*.py, push_inputs.py, push_scoring.py | 5,536 | pre-push-safety-net |
| U4 Engine (CPG + model layer) | F4, F6 | Joern graph build and session; arbitration of detector claims (taint, dominance, config, evidence ladder) | `ousast model candidates` cli.py:306; cli.py:1286 `_run_model_layer`; cli.py:1304 lazy | cpg/*.py, model/*.py (minus endpoint.py) | 7,764 | engine-change-scoped-analysis, pre-push-safety-net |
| U5 Semantic layer | F5, F13 | tree-sitter CST/IR, fact tables, overlay, taint, obligations; complexity hotspots for MAP | cli.py:82, 90, 1403-1442 (lazy) | semantic/*.py, semantic/obligations/*.py, complexity/*.py | 4,301 | engine-change-scoped-analysis |
| U6 LLM hunter and model client | F7 (+ model/endpoint.py, plane/budget.py) | tool-calling LLM hunter over hotspots, its tools, the chat client, metering | cli.py:962 `_attach_tool_hunter`, cli.py:982; plane/tasks/verify.py:29; benchmarks/valid/hunter_arm.py:245 | tool_hunter.py, hunter_tools.py, provider/openrouter.py, model/endpoint.py, plane/budget.py | ~1,700 | hunter-learning-loop |
| U7 Learn harness, memory store, plane fact tasks | F8, F10, F9b | labels, examples, features, roles, compile, evaluate, experiments, leak audit; the row/blob store; fact-producing tasks | learn/cli.py:18-89; cli.py:315 `learn labels`; plane/tasks/*.py `__main__` | learn/*.py, plane/memory.py, plane/tasks/*.py | 11,350 | hunter-learning-loop |
| U8 Plane executor | F9 | run manifests on the ax cluster: reconciler, router, runner, egress, harvest | cli.py:326-379 `plane ...`, cli.py:1530; plane/runner.py:989 (container PID 1); plane/profile.py:257 | plane/reconciler.py, runner.py, generate.py, manifests.py, harvest.py, egress.py, delivery.py, router.py, doctor.py, profile.py, engine_alerts.py, workspaces.py | 4,885 | hunter-learning-loop (runs its tasks) |
| U9 Search with proof, sandbox, regress | F11, F12 | coordinator, worker, executor, differential verification in a sandbox; deep-mode regress verdicts | `__main__` in search/coordinator.py:487, worker.py:530, executor.py:691, verify.py:714; benchmarks/search/pilot_run.py:39-47; cli.py:66, 80 | search/*.py, sandbox/*.py, regress/*.py | 5,352 | search-with-proof |
| U10 Periphery | F14, F15, F16, F17 | improve lever; pair corpus, benchmark manifests, pinned repos; CI gates; MCP server and skill router | `ousast improve` cli.py:237; `benchmark`/`pairs`/`repos` cli.py:232/283/296; gate.py:184, map_gate.py:118, pair_gate.py:80; `ousast mcp` cli.py:305 | improve/*.py, pairs.py, benchmark.py, repos.py, gate.py, map_gate.py, pair_gate.py, mcp.py, skills.py, index.py | 4,429 | pairs feed hunter-learning-loop labels; the rest none |
## Edges that matter (feature → feature, heaviest sites)
- Pre-push → Model: push/policy.py:18-23, push/runner.py:23-27, push/vetoes.py:24-25. Pre-push → CPG: push/runner.py:20-21, push/cache.py:21-22. Pre-push → LLM client: push/assistance.py:18.
- Model → CPG: model/scan.py:30, model/pipeline.py:30, model/taint.py:17. **CPG → Model (cycle)**: cpg/backend.py:37, cpg/session.py:44 import `model.contracts`.
- Core → Model (cycle, lazy): findings.py:86 → model.partitions; preprocess.py:255 → model.layout.
- Learn → Memory and budget: learn/compile.py:39-40, learn/program.py:35-36. Learn → plane tasks: learn/features.py:27, learn/labels.py:48, learn/roles.py:35. **Plane tasks → Learn (cycle)**: plane/tasks/features.py:34, plane/tasks/roles.py:33-34, plane/tasks/remember.py:42.
- **Memory → Plane (layering inversion, lazy)**: plane/memory.py:933, 989, 1002 import the reconciler and tasks.loop.
- Search → Memory and budget: search/coordinator.py:22-23, search/board.py:18, search/worker.py:17. Search → sandbox: search/_sandbox.py:24, search/executor.py:32.
- Improve → Memory: improve/memory.py:49, 464. Improve → pairs: improve/evolve.py:21, 71.
- MCP → scan: mcp.py:63, 164 (lazy import of cli). skills.py is imported only by hunter.py:139 (lazy).
## Ambiguous ownership (discovery's findings, kept for phase 3)
1. model/contracts.py is shared by cpg, push and model and causes the CPG↔Model cycle; contracts.py and push/contracts.py are siblings.
2. model/endpoint.py is an LLM client that imports tool_hunter; used by learn, plane/budget, search/worker, push/assistance.
3. plane/budget.py is metering used by learn, search and plane tasks.
4. plane/memory.py is a shared store that imports the plane reconciler and loop lazily.
5. plane/tasks/{repo_facts,roles,features,alerts}.py are fact producers that learn imports and that import learn.
6. mapping.py (1,087 lines) serves scan, push, gates, pairs, hunter_tools, learn and model.
7. Name collisions: hunter.py (regex pool) vs tool_hunter.py (LLM); harness.py (scan trace config) vs the learn harness; calibration.py (scan feedback) vs learn/calibrate.py.
8. push_scoring.py and push_inputs.py sit at top level but belong to pre-push evaluation.
9. regress/ (deep mode) and search/ (proof arm) both run code in the sandbox for a triggerable verdict.
## Standalone modules (no internal importer except cli.py or tests)
No importer at all: model/audit.py, push_inputs.py, map_gate.py, plane/runner.py (container entry), plane/tasks/agree.py, plane/tasks/features.py, search/worker.py, search/probe.py (driven by benchmarks/search/pilot_run.py:43, 47).
Only via package `__init__`: policy/verycode.py, semantic/prove_filter.py, scoring/project_score.py, regress/candidate.py, sandbox/probe.py.
Imported only by cli.py: calibration, fusion, harness, hunter, improve, learn.cli, mcp, model/report, pair_gate, plane/engine_alerts, plane/harvest, plane/tasks/remember, plane/workspaces, profiling, provenance, regress, reports, stages, semantic/obligations/intent.
## Gaps
Discovery did not read tests or the spec directories; cli.py line ranges per feature are approximate; where the
measured "LLM hunter" runs in the plane (likely plane/tasks/verify.py:29) is unconfirmed.
@@ -0,0 +1,86 @@
# U1 Scan orchestration and reports (`ousast scan`)
Traced read-only on 2026-10-13. Three premises from discovery were wrong and are corrected here:
`run_hunter_pool` (cli.py:670) is the **standard** branch and is the same regex matcher as quick, with no model
call; the tool hunter runs in standard as well as deep whenever MAP runs; scoring and fusion run **before** MAP,
so model-layer, overlay, obligation and tool-hunter findings never reach `score.json` or `fusion.json`.
## Happy path, `--mode standard`
1. `main` → `_main` (cli.py:126, 134); `load_dotenv()` (cli.py:139); parser `scan` (cli.py:226-230).
2. `_scan` (cli.py:613) → `_run_scan` (cli.py:619) → `plan_for_mode` standard = (STATIC, MAP) (stages.py:13-17, 28); `load_config` (cli.py:624).
3. `create_scan_run` makes the run dir (run.py:18-26); `HarnessRuntime` with no processors, `write_harness_config` (cli.py:627-633); stages run through `run_stage` (harness.py:103-111).
4. `load_policy`, `load_ruleset`, `assert_rules_resolve` (cli.py:634-638); `_load_static_hints` (cli.py:1037); `preprocess_repository` (cli.py:641-644).
5. `analyze_entry_points`, `attach_reachability_hints` (cli.py:656-659); `provenance.fingerprint` (cli.py:660, provenance.py:66).
6. `rank_targets`, `calibrate_rankings` (cli.py:661-666).
7. Standard: `run_hunter_pool` (cli.py:667-673) = thread pool of `quick_scan_findings` (hunter.py:103-160). Quick/deep: `quick_scan_findings` directly (cli.py:674-687).
8. `write_findings` (cli.py:708); `verify_findings` deterministic (verification.py:73-123, cli.py:711); `_persist_calibration_feedback` (cli.py:1011).
9. Fusion only when standard and enabled: `fuse_findings` (cli.py:720-726, fusion.py:191); decisions do not change findings.
10. Scoring: `build_score_artifact` → `score.json` (cli.py:731-746); `record_completed(STATIC)` (cli.py:747).
11. MAP (cli.py:757-835): `build_complexity_map` (758), `load_facts` (769), overlay `adjudicate` (774), `_check_obligations` (789, 1388), `_run_model_layer` (804, 1286-1347), `_attach_tool_hunter` if `hunter_model` set (816, 962-1008).
12. Deep adds REGRESS (cli.py:839-880): sandbox probe → `run_regression` → `write_verdicts`, `persist_verdicts`. Deep skips fusion (cli.py:720).
13. `write_markdown_report` (886), `write_sarif_report` (905), `write_manifest` (915); `scan_exit_code` (reports.py:281-296).
```mermaid
flowchart TD
A[main/_main<br/>cli.py:126] --> B[parse scan args<br/>cli.py:226]
B --> C[_scan<br/>cli.py:613] --> D[_run_scan<br/>cli.py:619] --> E[plan_for_mode<br/>stages.py:28]
E --> F[create_scan_run<br/>run.py:18] --> G[HarnessRuntime + write_harness_config<br/>cli.py:627]
G --> H[load_policy / load_ruleset<br/>cli.py:634] --> I[_load_static_hints<br/>cli.py:1037]
I --> J[preprocess_repository<br/>cli.py:641] --> K[analyze_entry_points<br/>cli.py:656]
K --> L[fingerprint<br/>provenance.py:66] --> M[rank_targets + calibrate_rankings<br/>cli.py:661]
M --> N{mode == standard<br/>cli.py:667}
N -- standard --> O[run_hunter_pool<br/>hunter.py:103]
N -- quick/deep --> P[quick_scan_findings<br/>findings.py:45]
O --> Q[write_findings<br/>cli.py:708]
P --> Q
Q --> R[verify_findings<br/>verification.py:117] --> S[_persist_calibration_feedback<br/>cli.py:1011]
S --> T{standard and fusion.enabled<br/>cli.py:720}
T -- yes --> U[fuse_findings<br/>fusion.py:191] --> V
T -- no --> V[build_score_artifact<br/>cli.py:732]
V --> W{MAP requested<br/>cli.py:757}
W -- quick --> AC
W -- standard/deep --> X[build_complexity_map<br/>cli.py:758] --> Y[adjudicate overlay<br/>cli.py:774]
Y --> Z[_check_obligations<br/>cli.py:1388] --> AA[_run_model_layer<br/>cli.py:1286] --> AB[_attach_tool_hunter<br/>cli.py:962]
AB --> AR{REGRESS requested<br/>cli.py:839}
AR -- deep, sandbox ok --> AS[run_regression<br/>cli.py:856] --> AC
AR -- no --> AC[write_markdown_report<br/>cli.py:886]
AC --> AD[write_sarif_report<br/>cli.py:905] --> AE[write_manifest<br/>cli.py:915] --> AF[scan_exit_code<br/>reports.py:281]
```
## Side effects
Run dir `<target>/<runs_dir>/<scan_id>/`: resolved_config.json (run.py:24), harness.json (cli.py:632),
trace/events.jsonl (harness.py:76), mapping/static_hints.json and entry_points.json (cli.py:640, 657),
preprocess/file_targets.json, rank/ranking.json, calibration/*.json, shadow_findings.json, findings.json
(rewritten up to 6 times: cli.py:708, 788, 801, 814, 877, 1005), traces/hunter_trajectories.jsonl,
verification.json, fusion.json, score.json, complexity_map.json, overlay.json, verdicts.json, report.md,
report.sarif, manifest.json. Into the scanned repo itself: `.openultrasast/calibration/false_positive_learnings.json`
(cli.py:1021) and `complexity_ledger.json` (cli.py:765, 872). Subprocesses: `git log` (provenance.py:102-108),
Joern via `JoernBackend` (cpg/backend.py:1817), Docker in deep mode (sandbox/runner.py:64). Model calls:
`scan_repository` with `resolve_chat_endpoint` (cli.py:1321-1336), `adjudicate_intent` with
`resolve_hunter_client` (cli.py:1450), `run_tool_hunter` (cli.py:976-989).
## External dependencies
Core: preprocess_repository (cli.py:643), load_ruleset (637), load_policy (634), analyze_entry_points (656),
rank_targets (661), quick_scan_findings (677). Engine: resolve_cpg_backend (1309), regions_for (1315),
coverage_rows (1323), scan_repository (1333). Semantic: load_facts (770), adjudicate (777), check_obligations
(1426-1450), filter_promoted_hotspots (847). LLM hunter: run_tool_hunter (976-982). Complexity:
build_complexity_map (760). Sandbox/regress: resolve_sandbox_runner (845), run_regression (858).
## Never touched by the scan path (removal candidates)
- `calibration.compute_ranking_metrics` (calibration.py:108): only tests/test_calibration.py.
- `findings.build_quick_hunter_prompt` (findings.py:102): only tests/test_findings.py.
- `verification.validate_evidence_transition` (verification.py:39): only tests/test_verification.py.
- `fusion.should_fuse` (fusion.py:94): only in `__all__`.
- Processor machinery in harness.py (Processor, ProcessorSpec, _run_processor, _contract_violation,
harness.py:41-55, 125-166): the runtime is always built with no processors (cli.py:627-632).
- `fusion.json` decisions never feed findings or score; the quick-rule fallback to PATTERN_RULES
(findings.py:55-56) never triggers from the CLI.
- Kept: profiling.py is opt-in by env var (profiling.py:40-42).
## Likely bugs (statement order, unconfirmed)
Scoring (cli.py:732) and verification (cli.py:711) run before MAP (cli.py:757), so MAP findings are absent from
score.json and unverified, which affects `--fail-on verified`. Deep skips fusion (cli.py:720). A stage
exception aborts with no report (harness.py:107-109).
## Confidence
High for cli.py:613-959 and stages.py; medium for side effects inside other units (not read here).
@@ -0,0 +1,111 @@
# U10 Periphery: improve, pairs/benchmark/repos, gates, MCP and skills
Traced read-only 2026-10-13.
## (a) improve/ (1,175 lines: evolve 416, memory 549, validator 125, journal 53)
`ousast improve` (cli.py:237-281, 433) → `_improve` (1106): holdout split of the pair catalog (1129), manifest
and target (1130-1131), optional `MemorySession` (1133, 1170) → `run_improvement` (evolve.py:259) loops
`run_round` (150): `_scan`/`_evaluate` (307, 319; `benchmark.evaluate_benchmark`), propose status edits (126)
plus memory proposals (176-183), `EvolveValidator` (validator.py:69), replay boot `_load_with` (333), rescan;
accept only if recall ≥ floor, FP < ceiling, score and matched count do not drop (212-214) plus `evaluate_profiles`
(59, 220-228); write the ledger on accept (231), always the journal (`_record` 378 → journal.py:21/45).
Writes `<target>/.openultrasast/calibration/rule_policy.json`, `improve_journal.json`, `rule_signals.json`,
and with `--memory` `memory_proposals.jsonl`, `signals.json`, outcome rows to the store (memory.py:355-380,
508, 519). Callers: cli.py:45, 1170; lazily plane/tasks/loop.py:69, 80, 194-195, 219-221; plane/memory.py:1002
→ loop.write_snapshot → improve.memory Guard; `loop-improve` task registered at plane/generate.py:380,
loop.py:269. Nothing under benchmarks/, dagger/, .github imports it. No active spec names the command; only
.kiro/steering/overview.md:65 and docs (README.md:220, 314; docs/memory.md, examples.md, index.md,
threat-model.md; ops/ax/README.md:101). Tests: test_improve, test_improve_memory, test_cli_improve,
test_subtraction_complete (:67-89), test_gate.py:135-136.
```mermaid
flowchart TD
A[cli improve<br/>cli.py:433] --> B[_improve<br/>cli.py:1106]
B --> M[MemorySession.open<br/>improve/memory.py:460] --> F[MemorySession.finish<br/>improve/memory.py:506]
B --> R[run_improvement<br/>improve/evolve.py:259] --> RR[run_round<br/>improve/evolve.py:150]
RR --> S[_scan / _evaluate<br/>improve/evolve.py:307,319]
RR --> V[EvolveValidator<br/>improve/validator.py:69]
RR --> P[evaluate_profiles<br/>improve/evolve.py:59]
RR --> J[write_journal<br/>improve/journal.py:21]
```
## (b) pairs.py (1,207), benchmark.py (504), repos.py (285)
`ousast pairs` (cli.py:283-294) → `_pairs` (1175) → `load_pair_catalog` (pairs.py:229) → `select_slice`/
`select_profile`/`select_split` (649/661/667) → `evaluate_catalog` (438) → `_evaluate_overlay_pair` (973) or the
inventory path (876) → `result_payload` (673) or `pair_gate.print_pair_metrics` (pair_gate.py:52, cli.py:49).
`ousast benchmark` (cli.py:232) → `_benchmark` (1076) → `create_benchmark_run` (benchmark.py:185) → `_run_scan` →
`evaluate_benchmark` (224) → `write_benchmark_artifacts` (283). `ousast repos` (cli.py:296) → `_repos` (1199) →
`load_repo_recipes` (repos.py:92) → `select` (282) → `resolve` (172; `_fetch` 188 only with `--fetch` or
OPENULTRASAST_REPOS_NETWORK=1) → `verify` (224).
The hunter-learning-loop label path needs only `load_pair_catalog` (with `_load_catalog_file`, `PairCase`,
CatalogError) and `pair_cache_dir` (pairs.py:328): learn/labels.py:142, 615; learn/examples.py:292;
benchmarks/pairs/advisory-fixes/materialize.py:51 (loaded by benchmarks/valid/catalog.py:41). Other non-CLI
users: plane/harvest.py:131, loop.py:70, 222 (`load_pair_catalog`, `select_split`); improve/evolve.py:71
(`evaluate_catalog`, `select_tier`, `select_vendored`, `GATING_TIERS`); model/candidates.py:268
(`_materialize_side`, `_overlay_scan`, `_quick_scan`); benchmarks/learn/* (`_materialize_side`, `_targets`).
Only CLI/pair_gate/tests: `select_profile`, `result_payload`, `duplicate_groups`, `split_by_repository`,
`_header_lines`, `_body_digest`, `is_weak_label`, `pointer_recipe`, `load_mechanisms`, `load_datasets` (no
non-test caller), `build_pair_signals` (re-exported by improve only). benchmark.py is core (cli.py:18,
gate.py:22, map_gate.py:17, pairs.py:28, mcp.py:163, improve/evolve.py:21, plane/tasks/loop.py:68, 175).
repos.py callers: cli.py:68-77, push_inputs.py:18, benchmarks/{ranking/position.py:26, push/*.py, repos/measure.py:34}.
```mermaid
flowchart TD
P[_pairs<br/>cli.py:1175] --> L[load_pair_catalog<br/>pairs.py:229]
P --> E[evaluate_catalog<br/>pairs.py:438] --> O[_evaluate_overlay_pair<br/>pairs.py:973]
P --> PM[print_pair_metrics<br/>pair_gate.py:52]
B[_benchmark<br/>cli.py:1076] --> CR[create_benchmark_run<br/>benchmark.py:185]
B --> EB[evaluate_benchmark<br/>benchmark.py:224]
B --> W[write_benchmark_artifacts<br/>benchmark.py:283]
R[_repos<br/>cli.py:1199] --> RS[resolve<br/>repos.py:172]
R --> VF[verify<br/>repos.py:224]
```
## (c) Gates (gate 205, map_gate 130, pair_gate 93)
`gate.main` (gate.py:182): for each of eight `LANGUAGE_MANIFESTS` (30-35) load `benchmarks/manifests/<name>.toml`
→ `benchmarks/fixtures/<name>`, quick scan (80), `evaluate_benchmark`; recall ≥ 0.90 and FP < 0.10 per language
and overall (114-146). `map_gate.main` (118): scans split-sink-{c,java,javascript,python} (24-29); planted sink
must be in the top hotspot band (69); Python map order must differ from hit-count order (96-99).
`pair_gate.main` (80): vendored `local` slice of benchmarks/pairs/catalog.toml must have pair_pass_rate 100%
(34-47). Callers: gate: ci.yml:28, dagger/ci.py:53, cli.py:42, loop.py:218 (constants), measure_php_quick.py:129,
many tests via `LANGUAGE_MANIFESTS`; map_gate: ci.yml:30, dagger/ci.py:54, model/audit.py:30, test_map_gate,
test_degradation_matrix; pair_gate: ci.yml:32, dagger/ci.py:55, **cli.py:49 (`print_pair_metrics`, module
level)**, model/audit.py:30, test_pair_corpus.py:11, 138, test_degradation_matrix. Deleting map_gate/pair_gate
breaks ci.yml:29-32, dagger/ci.py:54-55, every `ousast` command (cli.py:49), test_degradation_matrix.py:34, 41
(diffs against benchmarks/measurements/2026-09-06-gate-baseline.txt), test_map_gate, test_pair_corpus.py:138,
model/audit.py:30 `ROOTS`.
```mermaid
flowchart TD
CI[ci.yml checks<br/>.github/workflows/ci.yml:27-32] --> G[gate.main<br/>gate.py:182] --> DG[detection_gate<br/>gate.py:114]
CI --> MG[map_gate.main<br/>map_gate.py:118] --> SS[split_sink_map_gate<br/>map_gate.py:102]
CI --> PG[pair_gate.main<br/>pair_gate.py:80] --> PGF[pair_gate<br/>pair_gate.py:34] --> EC[evaluate_catalog<br/>pairs.py:438]
```
## (d) mcp.py (384), skills.py (372), index.py (74)
`ousast mcp` (cli.py:305, 467) → lazy `serve` (mcp.py:362) → `McpServer.handle` (306) → tools (238): `scan`
(62, quick/standard only, into `cli._run_scan`); `benchmark` (162, into benchmark + `_run_scan`); read-only
over run_dir JSON: `status` (81), `findings` (101), `get_finding` (128), `evidence` (143), `artifacts` (157),
`explain` (188), `export_report` (226); `propose_patch` (215) returns `not_implemented`. Only importer
cli.py:468. skills: cli.py:667-670 standard mode `run_hunter_pool` → `schedule_hunter_tasks` (hunter.py:70) →
`select_skill_snippets` (137) → `skills.route_skill_ids` (skills.py:318); ids stored as `selected_skills` on
the task and trajectory (hunter.py:96, 157) and end up in `traces/hunter_trajectories.jsonl` (cli.py:707-710);
detection is plain `quick_scan_findings` (151) and never reads the skills. `select_skill_context`, `skill_chunks`
(322, 339) only test_skills; index.py imported only by skills.py:15 for `skill_chunks`, otherwise test-only.
The `skills`/`mcp` strings in plane/manifests.py:556 and reconciler.py:130-132 are Workspace fields, not these
modules.
```mermaid
flowchart TD
C[cli mcp<br/>cli.py:467] --> SV[serve<br/>mcp.py:362] --> T[_build_tools<br/>mcp.py:238]
T --> SC[_scan<br/>mcp.py:62] --> RS[_run_scan<br/>cli.py:619]
T --> BM[_benchmark<br/>mcp.py:162] --> RS
H[run_hunter_pool standard<br/>cli.py:670] --> SK[select_skill_snippets<br/>hunter.py:137] --> RT[route_skill_ids<br/>skills.py:318]
CH[skill_chunks tests only<br/>skills.py:339] --> IX[chunk_text_namespace<br/>index.py:28]
```
## Confidence
High on caller lists for gates, mcp, skills, index, the improve read/write sets and the CI dependencies;
medium on the "CLI or tests only" list for pairs.py. Not read: `propose_from_memory` (improve/memory.py:282);
whether ops/ runs `loop-improve`.
@@ -0,0 +1,63 @@
# U2 Shared core: the quick-rule path
Traced read-only 2026-10-13. `hunter.py` is the regex quick-rule pool, not the LLM hunter. No model call
anywhere on this path: `rank.py:32 complete_json` is a method on a Protocol used only by
`rank_targets_with_model` (rank.py:46), whose only caller is tests/test_rank.py:82.
## Happy path (`_run_scan`, cli.py:619)
1. `load_config` (cli.py:624 → config.py:225): loads `.env` into os.environ (config.py:77-92), reads TOML, rejects retired keys (config.py:236).
2. `load_policy` reads CWE_Score.tsv (policy/verycode.py:30-37); `load_ruleset` parses every rules `*.toml` by rglob, filters framework priors, applies the ledger overlay (ruleset/store.py:44-71); `assert_rules_resolve` (verycode.py:73).
3. `ingest_sarif` (mapping.py:101) → `write_static_hints` (mapping.py:240).
4. `preprocess_repository` (preprocess.py:61): `enumerate_source_files` with `git ls-files` (preprocess.py:96, 240), `build_file_target` + `detect_language` + `detect_tags` (114, 130, 146), `attach_static_hints` (mapping.py:116), `_git_commit` (285), `write_preprocess_artifact` (90).
5. `uncovered_languages` (findings.py:78); `analyze_entry_points` (mapping.py:123-130) → `attach_reachability_hints` (mapping.py:181).
6. `rank_targets` without a model → `heuristic_rank` (rank.py:36, 78); reachability from hints (rank.py:112, 227); `calibrate_rankings` (calibration.py:161); `write_rankings` (rank.py:72).
7. Standard: `run_hunter_pool` (hunter.py:103) → `schedule_hunter_tasks` (70) → ThreadPool of 8 × `_run_hunter_task` → `quick_scan_findings` (151). Other modes: `quick_scan_findings` directly with emit floors (cli.py:675-685).
8. `quick_scan_findings` (findings.py:45) compiles rules (60-62) → `_finding_from_match` (119) → `resolve_severity` (verycode.py:56), `_reachability_for_line` (154) → `StaticFinding` (16).
9. `write_findings` (findings.py:113), `write_hunter_trajectories` (hunter.py:123), then score (`build_score_artifact`, scoring/project_score.py:98).
```mermaid
flowchart TD
CFG[load_config<br/>config.py:225] --> POL[load_policy<br/>policy/verycode.py:30] --> RS[load_ruleset<br/>ruleset/store.py:44]
RS --> CHK[assert_rules_resolve<br/>policy/verycode.py:73] --> SARIF[ingest_sarif<br/>mapping.py:101] --> PRE[preprocess_repository<br/>preprocess.py:61]
PRE --> ENUM[enumerate_source_files<br/>preprocess.py:96]
PRE --> ASH[attach_static_hints<br/>mapping.py:116]
PRE --> EP[analyze_entry_points<br/>mapping.py:123] --> ARH[attach_reachability_hints<br/>mapping.py:181]
ARH --> RANK[rank_targets / heuristic_rank<br/>rank.py:36 / rank.py:78] --> CAL[calibrate_rankings<br/>calibration.py:161]
CAL -->|standard| POOL[run_hunter_pool<br/>hunter.py:103] --> TASK[_run_hunter_task<br/>hunter.py:144] --> QS
CAL -->|other modes| QS[quick_scan_findings<br/>findings.py:45]
QS --> FM[_finding_from_match<br/>findings.py:119] --> SEV[resolve_severity<br/>policy/verycode.py:56]
FM --> SF[StaticFinding<br/>findings.py:16] --> SCORE[build_score_artifact<br/>scoring/project_score.py:98]
```
## Side effects
Subprocesses: `git ls-files -z` (preprocess.py:240), git commit hash (287). Writes: preprocess.py:93,
findings.py:116, rank.py:75, hunter.py:125, mapping.py:242/247, config.py:258; ruleset/store.py:102, 113 are
written by the learning loop, not scan. config.py:92 mutates os.environ. findings.py:42 loads PATTERN_RULES at
import time. The hunter pool computes `selected_skills` and `retrieval_context` (hunter.py:96-97, 112) but only
logs their ids; they never affect findings. **Bug:** the standard-mode pool skips
`min_emit_priority`/`min_emit_precision` (compare cli.py:670 with cli.py:683-684).
## Dependencies on other units
hunter.py:139 → skills.route_skill_ids (skills.py:318); preprocess.py:255 → model.layout.is_vendored (lazy);
findings.py:89 → model.partitions.FRONTENDS (lazy); config.py:11 → contracts.Contract. Same pipeline is also
called by gate.py:81-84, map_gate.py:52-54, pairs.py:794-815, plane/tasks/alerts.py:120-122, push/quick.py:87-90,
improve/evolve.py:311-314, hunter_tools.py:232/307.
## mapping.py (1,087 lines): who imports what
| Function | Importers |
|---|---|
| ingest_sarif :101, attach_static_hints :116, write_static_hints :240, write_entry_points :245 | cli.py / preprocess.py (+ tests) |
| analyze_entry_points :123 | cli, gate, map_gate, pairs, hunter_tools, plane/tasks/alerts, learn/features, improve/evolve, push/quick, push/runner; 7 tests; 6 benchmarks |
| attach_reachability_hints :181 | cli, gate, map_gate, pairs, plane/tasks/alerts, push/quick, improve/evolve |
| php_hook_callbacks :134 | model/scan.py, model/taint.py; 2 benchmarks |
| verifier_evidence_candidates :188, semgrep_mapping_tasks :204, codeql_mapping_tasks :218, differential_mapping_record :232, sharp_edge_record :236 | **tests only** (test_mapping.py); their records MappingTaskRecord, DifferentialMappingRecord, SharpEdgeRecord (:39, :48, :56) are produced nowhere else |
## Overlap check
- policy/ (CWE severity table) vs push/policy.py (revision/capability admission): different things.
- scoring/ `REACH_MULT` (project_score.py:15) duplicates `_BASE_REACH_MULT` (calibration.py:255).
- calibration.py: only cli.py:27 and tests import it.
- contracts.py (base `Contract`), model/contracts.py (question/scope schemas), push/contracts.py (push schemas, builds on model/contracts): layered, not duplicates.
## Confidence
High on the call chain; medium on importer lists (name greps with -w). Not traced: parsers inside
mapping.py:250-1087, skills routing internals, families.toml consumers beyond model/taxonomy.py:32.
@@ -0,0 +1,101 @@
# U3 Pre-push hook
Traced read-only 2026-10-13. Key finding for the hunter spec: **in the shipped configuration admission can never
produce a finding.** `eligibility.json` has no `"enabled": true` entry (grep count 0), verdicts are NO-GO or
experimental, so every candidate gets `capability_unavailable` or `capability_disabled` (policy.py:913-919).
Engine hits appear only as advisory lines (background.py:113-145, report.py:241). The hook default deadline is
30 s (hook/pre-push:24, cli.py:182, config.py:196); 300 s is the replay benchmark's `long_deadline` arm
(benchmarks/unseen/replay.py:25); the background engine defaults to 900 s (cli.py:210).
## Happy path
1. Git runs `push/hook/pre-push:6-38` → `ousast pre-push . --artifact $dir/push-$$.json --deadline ${OUSAST_PUSH_DEADLINE:-30} --mode advisory --remote NAME URL`; docker wrapper if present (:10-12); prior hook chained (:34-36).
2. `_advisory_hook` (cli.py:513) → `_hook_main` (523); argument errors become exit 0 "check did not run" (412-415). `_pre_push` (529) builds `PushConfig` (config.py:196), duplicates stdin (570), calls `runner.push` (584).
3. `push()` (runner.py:609) creates one `ExecutionBudget(started + deadline, allowance=2)` (632); bounded work runs in a forked child via `_prepare` (56-115).
4. Refs: `_prepare(updates)` 1 MiB cap (638-640) → `SnapshotAdapter.resolve_updates` (snapshot.py:200) → `parse_updates` (124), `_resolve` with merge-base (167, 191). No base → `_last_commit_quick`, quick only (runner.py:646-654, 503-523).
5. `_analyze` (runner.py:257): provenance (322-327, 131); optional `ArtifactCache` only with `--cache-dir` (328-341).
6. Snapshots: `materialize(head)`, `materialize(base)` (344-351, snapshot.py:462) into `mkdtemp("ousast-snapshot-")` (485).
7. Discovery: `reuse.discovery(..., discover=_discover)` for head and base (356-363); `_discover` (118) uses preprocess, mapping, `regions_for`; `ReusingBackend` (365-385).
8. Attribution: `adapter.compare` → `ChangeContext` (386, snapshot.py:226, diff-tree 270); `_uncovered_languages` (212, 389).
9. Quick tier: `head_spans` → `_prepare(run_quick_tier)` (385-391; quick.py:47, 60); matches kept only on a changed line or in a changed function (quick.py:121-130); "advisory, not admitted, never blocking" (quick.py:35).
10. Engine tier (`--engine inline` default): head `scan_repository(..., ScanBudget(max_model_calls=0))` (288, 396-406); base `policy.compare_targeted_base` (410-423, policy.py:731) → `compare_evidence` (policy.py:489) → `CandidateDelta`s.
11. Admission: `AdmissionCandidate` with `CapabilityKey`, `dependency_gaps` (428-446) → `_prepare(_admit)` (447) → `_admit` (169) → `load_registry` (eligibility.py:152) + `admit_candidates` (policy.py:889).
12. Vetoes only with explicit replay + `--experimental-record-vetoes` (449-457, 180-209; vetoes.py:412); never on the hook path (cli.py:550-551).
13. `deadline_exhausted`, `analysis_incomplete` appended (465-477); `admission_push_result` (488, policy.py:1013); merge across comparisons (676-686) → `PushReport` (707); remote URL stored as sha256 only (700).
14. `_background` (526): `background.previous()` always; `background.start()` only with `--engine background` (719-727).
15. `assist(report, config=model_config)` (730-732) returns at once without `--model-config` (assistance.py:31-32); the hook never passes it.
16. `_deliver` (560) → `deliver_report` (report.py:423) → `_save` forked, atomic, 0o600 (158, 120) → `render_report` (340); CLI prints (cli.py:595); chained hook (596-605); exit = `int(push_disposition == "block")` (report.py:104-105), only possible in `mode == "blocking"` (policy.py:1029-1031).
Replay: `replay.py:19-50` packages `hook_flags=["--deadline", N]`; `replay_entry.py:141-156` runs
`ousast pre-push <case> --base B --head H --artifact out/push.json --deadline N` through `runner.replay`
(runner.py:567); subprocess timeout N+30; exit 0 or 1 accepted (:159).
```mermaid
flowchart TD
G[git push<br/>hook/pre-push:38] --> W{docker wrapper?<br/>hook/pre-push:10}
W -->|yes| D[docker run --network none<br/>hook/ousast-docker:46] --> M
W -->|no| M[_main / _advisory_hook<br/>cli.py:405-411]
M --> HM[_hook_main<br/>cli.py:523] --> PP[_pre_push<br/>cli.py:529] --> PU[push<br/>runner.py:609]
RP[replay_entry --base/--head<br/>replay_entry.py:141] --> RE[replay<br/>runner.py:567] --> AN
PU --> B[ExecutionBudget<br/>runner.py:632] --> RU[resolve_updates<br/>snapshot.py:200]
RU -->|no base| LC[_last_commit_quick<br/>runner.py:503] --> APR
RU --> AN[_analyze<br/>runner.py:257] --> MAT[materialize head/base<br/>snapshot.py:462]
MAT --> DIS[discovery/_discover<br/>reuse.py:160 runner.py:118] --> CMP[compare -> ChangeContext<br/>snapshot.py:226]
CMP --> QT[run_quick_tier<br/>quick.py:60] --> ENG{engine mode<br/>runner.py:392}
ENG -->|inline| HS[scan_repository head<br/>runner.py:396] --> BS[compare_targeted_base<br/>policy.py:731]
BS --> ADM[_admit / admit_candidates<br/>runner.py:169 policy.py:889]
ADM --> VR[record_vetoes replay-only<br/>runner.py:449]
ADM --> APR[admission_push_result<br/>policy.py:1013]
ENG -->|off/background| APR
APR --> BG[_background<br/>runner.py:526] --> BGS[background.start Popen<br/>background.py:47]
BG --> AS[assist witness index only<br/>assistance.py:29] --> DL[deliver_report/_save<br/>report.py:423 report.py:158] --> EX[exit_code<br/>report.py:104 cli.py:610]
```
## Side effects
Artifact JSON at `${OUSAST_ARTIFACT_DIR:-$XDG_CACHE_HOME|~/.cache}/openultrasast/push/push-<pid>.json`, atomic
`.partial-<uuid>`, 0o600 (report.py:120-150), refused inside the repo (runner.py:561). Snapshots in
`$TMPDIR/ousast-snapshot-*`, removed after (snapshot.py:480-485, 648). Cache only with `--cache-dir` outside the
repo (runner.py:310, 328). Background results under `<artifact dir>/engine/<sha16(repo)>/` (background.py:29-33,
60-104). Git: rev-parse, cat-file, check-ref-format, merge-base, diff-tree, ls-tree, cat-file blob, all with
`-c protocol.allow=never -c core.hooksPath=/dev/null` (snapshot.py:63-570); no fetch. Subprocesses: forked
children (`_prepare` runner.py:66, `_save` report.py:168), Joern (cpg/backend.py:641, 1303, 1428), detached
background CLI (background.py:69-101), chained hook (cli.py:600). Model calls: none by default
(`max_model_calls=0` runner.py:288; `assist` at most one call, only with `--model-config`, assistance.py:89-110).
## External dependencies
Core: `enumerate_source_files`/`build_file_target` (runner.py:28, 121-122), `detect_language` (quick.py:69),
`analyze_entry_points` (runner.py:22, 123), `attach_reachability_hints` (quick.py:68), `PATTERN_RULES` /
`quick_scan_findings` (quick.py:67, 89), `rank_targets`, `load_policy` (quick.py:70-71). Engine: `JoernBackend`,
`engine_runtime_identity`, `joern_version` (runner.py:21), `scan_repository` (26), `regions_for`,
`declared_sources`, `layout_facts` (24-27); policy.py:19-22 imports `model.pipeline._arbitrate_all` and
`model.ladder.Rung`. LLM client: `model.endpoint.ChatEndpoint`/`resolve_chat_endpoint` (assistance.py:18).
push_scoring: `_metrics`, `score` (eligibility.py:18, 163-167) on every admission.
## Where an LLM detection tier plugs in
Defects enter admission at runner.py:428-447: the tuple of `AdmissionCandidate`s from `delta.candidates` goes
to `_admit` (169) → `admit_candidates(candidates, capabilities=...)` (policy.py:889, origin-agnostic per its
docstring :890). A hunter tier adds its candidates before runner.py:447. A candidate must carry (policy.py:61-75,
837-845; gates 906-967): a `CandidateDelta` with family, `site` = `"{path}:{line}:"`, `defect_id`, a non-null
`witness`; `rung == Rung.ENTAILED` (else `insufficient_rung`; :933-935 says a non-engine producer needs its own
protocol); `novelty`/`reason` from four allowed pairs with non-empty `change_evidence` (:941-946); matching
base/head OIDs and semantics; a `head_operation: EvidenceOperation` whose (language, family, mechanism) matches
the `CapabilityKey` and whose operation symbol is in the capability's `operation_symbols`; and an enabled PASS
`CapabilityAdmission` from eligibility.json (none exists today) with empty `dependency_gaps`. Hunter output that
cannot meet that bar can mirror the quick tier: an advisory, never-admitted payload on `PushReport`
(runner.py:499, report.py:143). `assistance.py` cannot add a finding: it asks for an index only (22-26), accepts
exactly `{"selections":[{defect_id, witness_index}]}` over existing admitted ids (120-140), replaces only
`model_assistance` (153).
## Off the happy path
- `vetoes.py`: only via `_record` under `--experimental-record-vetoes`, rejected outside replay (cli.py:550); importer runner.py only; `benchmarks/push/m1a_record_vetoes.py` needs it.
- `install.py`: install-time only (cli.py:476).
- `cache.py` / `reuse.ReusingBackend`: inactive by default (no `--cache-dir` in the hook); `reuse.discovery` still runs.
- `background.py` start: off unless `OUSAST_PUSH_ENGINE=background`; does not work through Docker (ousast-docker:17).
- `assistance.py`: no-op on the hook path.
- `eligibility.py`: always runs, yields no enabled capability; admission is structurally empty.
- `push_inputs.py`: only `benchmarks/push/{measure,m1a_record_vetoes,transfer_replay}.py` and one test.
- `push_scoring.py`: production (eligibility.py:18) plus benchmarks/push/measure.py:20 and tests.
## Confidence
High for runner, CLI, assistance, quick, background, admission, report. Medium for snapshot.compare internals
(snapshot.py:226-460), `compare_evidence` (policy.py:489-730), cache/reuse bodies. Not verified: eligibility
bindings vs current provenance (`eligibility_provenance_mismatch`, eligibility.py:159-160); ops/install-pre-push.
@@ -0,0 +1,86 @@
# U4 Engine: CPG backend and model layer
Traced read-only 2026-10-13.
## Happy path A, standard scan (no deadline)
1. cli.py:804 → `_run_model_layer` (1286), early return if disabled (1300).
2. `resolve_cpg_backend` (cpg/backend.py:1813) → `has_cpg` (cpg/capability.py:14) → `semantic.engines.joern_available` (engines.py:25); `JoernBackend` or `NullBackend`; `cpg_unavailable` degradation (cli.py:1310-1312).
3. `regions_for(..., shipped=declared_sources(root))` (model/regions.py:78, model/shipped.py:45; cli.py:1315); `coverage_rows` (model/report.py:87; cli.py:1324).
4. `run_stage("model", scan_repository(...))` (cli.py:1332); no `execution_budget`, so no deadline.
5. `scan_repository` (model/scan.py:143): drop vendored regions (164-165), reject duplicate question ids (167), `_scan_repository_impl` (213) inside a finally that disposes graphs and closes the session (187-198).
6. `load_families` (model/taxonomy.py:201; scan.py:233).
7. `build_partitions` (model/partitions.py:168; scan.py:244): `os.fwalk` no-follow, bucket by `LANGUAGE_BY_EXTENSION`, copy each language into `mkdtemp("ousast-input-")` (249), `build(scratch, frontend)` (280) via `scan._build` (scan.py:1444) with `vendored_directories` as exclude.
8. `JoernBackend.build` (backend.py:419) → `_build_impl` (573): `mkdtemp("ousast-cpg-")` (584), readability precondition (600), then sharded frontend (609) / JS-TS direct frontend + `_apply_overlays` (630-638) / `joern-parse --output cpg.bin` (642-643) with frontend retry (648). Returns `CpgResult` with run, run_batch, run_batch_once, cleanup (655-663).
9. `_evidence_pass` (scan.py:1275; called 345): taint requests `evidenceOnly` via `_portioned_evidence` (1252) and `cpg.run_batch("taint")` → `evidence_from_rows` (model/evidence.py:130; scan.py:1338); `order_by_evidence` (1399; 359).
10. `_scope_work` (751; 401): scoping and tier-0 pruning.
11. Query phase per kind (434-524): `_grouped` (1479) with each arbiter's `request_params` (taint.py:22, dominance.py:30, config_value.py:22); `_batched` (969) → `run_batch_once` → `JoernBackend._batch_once` (backend.py:1193): writes `<scratch>/<kind>-requests-*.json` (1222-1224), tries the session (1229), else `joern --script queries/<kind>.sc --param cpgFile=… --param requestsFile=…` (1231-1242) via `_run` (1514, Popen, process group killed on timeout); `extract_payload` (1250). `__census__` checks empty graph/shards (scan.py:490-509).
12. Arbitration (540-586): prefetched `CpgResult` per question (548) → `scan_region` (model/pipeline.py:124) → `_arbitrate_all` with taint/dominance/config verdicts; rungs on the ladder (model/ladder.py:29 `Rung`, 45 `Verdict`): ENTAILED = graph decided (pipeline.py:139-146); CORROBORATED and SUSPICION only from a judge (151-181); `enumerate_candidates` only with a judge (scan.py:556-558).
13. Graph-gap demotion (591-655), `_deduplicated`/`_ordered` (659), `ModelScanResult` (662); CLI `_finding_from_model` (cli.py:1361).
## Happy path B, pre-push (with deadline)
`push()` (runner.py:609) → `ExecutionBudget` (632; replay 586) → `_analyze` (257) with `JoernBackend()` (290) and
optional `ArtifactCache` (315); `discovery` wraps `_discover` (118); `ReusingBackend.build` (push/reuse.py:32) →
`graph_identity` (backend.py:447), cache hit `load_graph` (522) else `build` + `describe_graph` (494, seals a
`GraphArtifact` from `census.sc`) + `cache.publish`. `scan_repository(..., execution_budget, ranking_mode=
"evidence", change_context)` (runner.py:396), client withheld (scan.py:177), `max_model_calls=0` (runner.py:288),
`affected_context` (regions.py:258; scan.py:363). Base via `compare_targeted_base` (policy.py:731, 774).
Deadline enforcement: `_within_deadline` (scan.py:1435) with `_arbitration_reserve` (884), `_build` pre-checks
(1449-1462), partition walk checks (partitions.py:182-273), `_time_available` (backend.py:1377), `_run_bounded`
(1414-1471: timeout capped at deadline, cancel window, partial stdout harvested), `_cleanup_limit` (1399);
`deadline_exhausted` appended (scan.py:203, runner.py:458).
```mermaid
flowchart TD
CLI[_run_model_layer<br/>cli.py:1286] --> RES[resolve_cpg_backend<br/>cpg/backend.py:1813] --> CAP[has_cpg / joern_available<br/>cpg/capability.py:14]
CLI --> REG[regions_for + declared_sources<br/>model/regions.py:78]
PUSH[push / _analyze<br/>push/runner.py:609 / :257] --> BUD[ExecutionBudget<br/>model/contracts.py:19]
PUSH --> REU[ReusingBackend.build<br/>push/reuse.py:32] --> ART[graph_identity/load_graph/describe_graph<br/>cpg/backend.py:447-570]
CLI --> SR[scan_repository<br/>model/scan.py:143]
PUSH --> SR
PUSH --> BASE[compare_targeted_base<br/>push/policy.py:731] --> SR
SR --> IMPL[_scan_repository_impl<br/>model/scan.py:213] --> PART[build_partitions<br/>model/partitions.py:168] --> BLD[_build<br/>model/scan.py:1444]
BLD --> JB[JoernBackend.build/_build_impl<br/>cpg/backend.py:419/573] --> JP[joern-parse / frontend subprocess<br/>cpg/backend.py:642]
IMPL --> EVP[_evidence_pass<br/>model/scan.py:1275] --> EVR[evidence_from_rows<br/>model/evidence.py:130]
IMPL --> SCOPE[_scope_work<br/>model/scan.py:751] --> GRP[_grouped request_params<br/>model/scan.py:1479] --> BO[_batch_once<br/>cpg/backend.py:1193]
EVP --> BO
BO --> SES[EngineSession opt-in<br/>cpg/session.py:68]
BO --> JS[joern --script queries/*.sc<br/>cpg/backend.py:1231]
IMPL --> SREG[scan_region<br/>model/pipeline.py:124] --> VER[taint/dominance/config verdict<br/>model/taint.py:72] --> LAD[Rung/Verdict<br/>model/ladder.py:29]
IMPL --> RESULT[ModelScanResult<br/>model/scan.py:662] --> SF[_finding_from_model<br/>cli.py:1361]
```
## Side effects
JVMs: `joern-parse` or a frontend, then `joern --script` once per query kind, killed as process groups
(backend.py:1514-1550, 1659); optional `joern --server` under `OUSAST_ENGINE_SESSION=1` with a budget
(session.py:132-137; backend.py:65, 405, 1319). No Docker inside U4; the container boundary is the hook's
`ousast-docker` wrapper. Files: `$TMPDIR/ousast-input-*` (partitions.py:249), `$TMPDIR/ousast-cpg-*` with
cpg.bin, `*-requests-*.json`, `engine-session/` (backend.py:584, 1222, 1324), removed via cleanup (scan.py:1425);
stale trees swept only without a budget (backend.py:586-587, 1638). Push-only `ArtifactCache` 0700 outside the
repo (push/cache.py:35-44; runner.py:286). Standard scan has no graph cache.
## The CPG↔Model cycle
cpg/backend.py:37 and cpg/session.py:44 import exactly one name, `ExecutionBudget` (model/contracts.py:19-34): a
frozen dataclass over `openultrasast.contracts.Contract` with two floats and a `__post_init__`. No model logic,
so it can move to a shared contracts module. Reverse couplings: model/partitions.py:14 imports the private
`_DeadlineExpired`, `_remove_owned_tree` from cpg; cpg/artifact.py:151 imports the same two lazily.
## Dependencies
Out: semantic (`joern_available` cpg/capability.py:11; `load_facts` scan.py:31, layout.py:14, specs.py:24;
obligations facts specs.py:25; `parse_file`/obligations candidates.py:107, 215-217); core (`Contract`
model/contracts.py:15, cpg/artifact.py:13; `LANGUAGE_BY_EXTENSION` partitions.py:15; `php_hook_callbacks`
scan.py:1521; redaction pipeline.py:31; `StaticFinding` report.py:20; pairs candidates.py:268 `ceiling` only).
In: cli.py:1302-1306, 1322, 1705; push/runner.py:20-27, 218; push/policy.py:22; push/reuse.py; push/cache.py;
learn/slice.py:18; learn/features.py:26, learn/labels.py:47, plane/tasks/features.py:55 (taxonomy);
plane/tasks/repo_facts.py:28, preprocess.py:255 (layout); findings.py:86 (`partitions.FRONTENDS`).
## Removal candidates
- `model/audit.py`: no src importer; maintainer `__main__` tool (audit.py:45, 255); tests/test_module_audit.py, tests/test_search_demo_replay.py:200, measurement 2026-09-08-module-audit.json.
- `model/report.py`: only `coverage_rows` live (cli.py:1322); `SliceRow`, `overfitting_gap`, `render`, `render_findings` (24-75) only tests/test_model_report.py.
- `cpg/queries/vocabulary.sc`: only tests/test_cpg_queries_compile.py:32 and benchmarks/push/ontology_gap.py:102.
- `model/candidates.py`: only with a judge (scan.py:556); push never reaches it; `ceiling`/`labeled_*` serve cli.py:1705 only.
- Conditional, not dead: cpg/session.py (opt-in), cpg/artifact.py sealing (push with cache_dir). `model/shipped.py` is live (cli.py:1306, runner.py:27, 123).
## Confidence
High on the chain, subprocess boundary, deadline plumbing; medium on unused-module claims. Not read in detail:
`_batched`/`_PortionSizer` (scan.py:969-1232), `_build_sharded` (backend.py:765-1115), session internals,
`.sc` bodies, `taint.verdicts` ranking (taint.py:131-312).
@@ -0,0 +1,87 @@
# U5 Semantic layer (tree-sitter) and complexity map
Traced read-only 2026-10-13. Correction: `prove_filter` acts on hotspots, not findings, and runs only in deep
mode before regress (cli.py:847). MAP runs in standard and deep (stages.py:15-16).
## (a) Overlay path, under Stage.MAP (cli.py:758)
1. `load_facts` (cli.py:770 → facts.py:412) reads `ruleset/semantic/*.toml` (facts.py:16, 179-213), memoised (420-422); FactLoadError → degradation (cli.py:771-773).
2. `adjudicate` (overlay.py:65) → `_flows_for` (161): per target read text (170), `parse_file` (ir.py:52) → `parse_with_cst` (cst.py:65) with `grammar_for` (extra.py:36) → `file_ir_from_tree` (cst.py:86) → FileIR of FunctionIR with Binds and CallSites (ir.py:10-50). Python falls back to `parse_python_ast` (ir.py:59-60); other languages `language_unsupported` (61).
3. `named_ranges_from_ir` (functions.py:96); `analyze_ir` (taint.py:60) → `_analyze_function` (76): parameters start TAINTED, up to 8 fixpoint passes (`_bind_state` 101), `_analyze_call` (123) emits TaintPath, Demotion or incomplete.
4. `_joern_paths` (overlay.py:192) probes `joern_available` but `joern_flow` always returns `()` (engines.py:42-47).
5. `_record_for_finding` (overlay.py:203) → promote / demote / unadjudicated; unmatched taint paths become coverage records (94-121). `write_overlay` (126); `finding_from_coverage` (132) → findings.
6. Deep + sandbox: `filter_promoted_hotspots` (prove_filter.py:13) → `run_regression` (cli.py:854).
Obligations (default on, config.py:181): `_check_obligations` (cli.py:1388) → `parse_file`, `load_declared_policy` (policy.py:59) → `check_obligations` (check.py:55: find_operations, find_discharges, sibling_sets, consistency_anomalies, OrderDominance dominance.py:27) → `adjudicate_intent` **LLM call** (intent.py:26, 47) → `findings_to_static` (check.py:145) → `rank_obligations` (cli.py:800).
```mermaid
flowchart TD
A[run scan MAP<br/>cli.py:758] --> B[load_facts<br/>facts.py:412] --> C[adjudicate<br/>overlay.py:65] --> D[_flows_for<br/>overlay.py:161]
D --> E[parse_file<br/>ir.py:52] --> F[parse_with_cst<br/>cst.py:65] --> G[grammar_for<br/>extra.py:36]
F --> H[file_ir_from_tree<br/>cst.py:86]
E --> E2[parse_python_ast fallback<br/>ir.py:63]
H --> I[named_ranges_from_ir<br/>functions.py:96]
H --> J[analyze_ir<br/>taint.py:60] --> K[_analyze_function<br/>taint.py:76] --> L[_analyze_call<br/>taint.py:123]
D --> M[_joern_paths stub<br/>overlay.py:192]
C --> N[_record_for_finding<br/>overlay.py:203] --> O[write_overlay<br/>overlay.py:126]
O --> P[finding_from_coverage<br/>overlay.py:132]
O --> Q[filter_promoted_hotspots<br/>prove_filter.py:13] --> R[run_regression<br/>cli.py:854]
```
## (b) Complexity MAP path
`build_complexity_map` (cli.py:760 → map.py:37) → `collect_signals` (signals.py:44: nesting via
`measure_nesting` 86, adjacent tests, tags, reachability hints, `_score` per (path, function)) →
`_hotspot_from_signals` bands high ≥ 6, medium ≥ 3 (map.py:62-102) → `attach_test_hints` (hints.py:52, 79) →
`apply_overlay(load_ledger)` (map.py:47-48, ledger.py:37, 103) → `write_complexity_map` (map.py:57).
Consumers: `_attach_tool_hunter` passes the top `max_hunter_hotspots` to `run_tool_hunter` (cli.py:964,
tool_hunter.py:323) and rewrites the map (cli.py:1007); deep mode `filter_promoted_hotspots` →
`regress/candidate.select_candidates` (candidate.py:63) with `hotspot_key`, `select_forced_candidates`
(ledger.py:33, 142); `plane/tasks/verify.py:179-186` builds synthetic Hotspots for the hunter; map_gate.py:18, 96.
After regress `persist_verdicts` (cli.py:872 → ledger.py:60, 81, 49) writes the ledger for the next scan.
```mermaid
flowchart TD
A[MAP stage<br/>cli.py:760] --> B[build_complexity_map<br/>map.py:37] --> C[collect_signals<br/>signals.py:44]
C --> D[_hotspot_from_signals<br/>map.py:62] --> E[attach_test_hints<br/>hints.py:52] --> F[load_ledger + apply_overlay<br/>ledger.py:37,103]
F --> G[write_complexity_map<br/>map.py:57]
G --> H[_attach_tool_hunter<br/>cli.py:964] --> I[run_tool_hunter<br/>tool_hunter.py:323]
G --> J[filter_promoted_hotspots<br/>prove_filter.py:13] --> K[select_candidates<br/>regress/candidate.py:63] --> L[persist_verdicts<br/>ledger.py:60] --> M[write_ledger<br/>ledger.py:49]
N[plane hunt<br/>plane/tasks/verify.py:179] --> I
```
## Side effects
`grammar_for` builds a new `Language(...)` on every call, no cache, once per parsed file (extra.py:46-53,
cst.py:68). `joern_available` runs `joern --help` (5 s timeout) once per target from `_joern_paths` and again in
`joern_flow`, uncached and pure overhead since `joern_flow` returns `()` (engines.py:33-47, overlay.py:193).
Writes `<run>/overlay.json`, `<run>/complexity_map.json`, and into the scanned repo
`.openultrasast/calibration/complexity_ledger.json` (ledger.py:49-57 via cli.py:872). Reads ruleset TOMLs and
the target's `.openultrasast/obligations.toml` (config.py:183). Network: `adjudicate_intent` (intent.py:47).
## Facts the hunter annotation model can reuse (hunter-learning-loop R3)
| Fact | Producer | Without Joern? |
|---|---|---|
| Enclosing function at a line | `function_at` (functions.py:136) over `named_function_ranges` (120) / `named_ranges_from_ir` (96) | yes (regex scan; Python uses ast, functions.py:126-133) |
| Callers of a function | not in U5; `learn/features.py:371` (`callers`), `plane/tasks/verify.py:128` (`callers_of`) | n/a, U5 IR has CallSites per function (ir.py:22-40) but no caller index |
| Entry points | `mapping.analyze_entry_points` (consumed by check.py:84, 87) | yes |
| Source/sink/sanitizer roles | `load_facts` SourceFact/SinkFact/SanitizerFact (facts.py:37-80, 412); matched by `_source_in`, `_sink_for`, `_sanitizer_for` (taint.py:222-238); already reused by learn/slice.py:21, learn/roles.py:38 | yes |
| Def-use within a function | Bind/CallSite (ir.py:11-30), taint state (taint.py:76-98), TaintPath.names (taint.py:16) | yes, needs tree-sitter or Python ast |
| Guards/obligations | `find_operations`, `find_discharges` (operations.py:47, 76), `OrderDominance.dominates` (dominance.py:32), `check_obligations` (check.py:55); sanitizer demotions with `dominating_fact` (taint.py:136-146) | yes, order-based only |
## Dependencies
U5 imports findings.StaticFinding, policy, preprocess.FileTarget, ruleset.frameworks. Imported by cli.py:37-38,
82-90, 1403-1442; hunter_tools.py:224, 254-257, 319; pairs.py:41-44, 838-840; model/candidates.py:107, 215-217;
model/specs.py:24-25; model/layout.py:14; model/scan.py:31; learn/roles.py:38; learn/slice.py:19-21;
learn/features.py:164, 487; mapping.py:154; cpg/capability.py:10; map_gate.py:18; tool_hunter.py:13;
regress/candidate.py:10-11; plane/tasks/verify.py:179; benchmarks/valid/hunter_arm.py:15.
## Removal candidates (no non-test caller)
- `prove_filter.promoted_findings` (prove_filter.py:28): test-only; cli.py:848-852 reimplements it inline.
- `engines.joern_flow` stub (engines.py:42-47) and `taint.merge_joern_paths` (taint.py:277): Joern is never used on the semantic path.
- `engines.tree_sitter_available`: export + tests only. `taint.analyze_file` (taint.py:53): test-only.
- `obligations/shapes.py` public API (`derive_obligation` 111, `explain_skip` 139, `obligation_mechanisms` 406, `is_obligation_row` 401, `ObligationShape` 41): test-only; private helpers live via operations.py:19.
- `check_obligations(store_shapes=())` is always empty (cli.py:1438, "the store was deleted"), so `_known_fix`, `_usable_shape` (check.py:252-258) are dead in practice.
- obligations package as a whole is **live** (cli.py:1403-1444, hunter_tools.py:251-285, pairs.py:834-862, model/candidates.py:217-222). All public complexity/* functions are live.
## Confidence
High on call chains; medium on dead-symbol claims (greps over src, tests, benchmarks). Not read: cst.py `_KIND`
tables, signals._score internals, find_discharges, siblings, policy, run_regression, map_gate beyond :96; whether
`hunter_tools.flows` is registered as a hunter tool.
@@ -0,0 +1,103 @@
# U6 LLM hunter and model client
Traced read-only 2026-10-13. The hunt path is clean. Below it: seven chat-client types or wrappers with two call
methods, four separate usage-to-cost paths, four curated tools that no caller ever enables, and a prompt that
names only the three default tools.
## Happy path of one hunt
1. `cli.py:976` `_attach_tool_hunter` → `resolve_hunter_client` (tool_hunter.py:239): `OPENULTRASAST_HUNTER_CLIENT` scripted/dump/unsafe → `scripted_hunter_client` (246-252); `DEEPSEEK_API_KEY` → `DeepSeekChatClient(OpenRouterChatClient(base_url=DeepSeek), endpoint=ChatEndpoint(prices=price_of("deepseek-flash")))` (253-259); `OPENROUTER_API_KEY` → `OpenRouterHunterClient.from_env()` (260-262); else None and the CLI skips (cli.py:977-978).
2. `run_stage("tool_hunter", run_tool_hunter(root, hotspots, client, model, max_steps=4))` (cli.py:980-988; DEFAULT_MAX_STEPS tool_hunter.py:30).
3. `run_tool_hunter` (323): `[]` if max_steps<=0 (339); `_tool_schemas(tools)` (341, 417); `DEFAULT_TOOL_NAMES = ("read_file","grep_repo","find_refs")` (33).
4. `_SYSTEM_PROMPT` (140-145) names only those three and asks for path, line, title, rationale (no family).
5. `_user_message` (423): context files first via `read_file` (429-434) for prefix caching; question last, `user_prompt` or `_hotspot_prompt` (439-454: hotspot JSON with path, function_name, score, band, rationale).
6. Tool loop (347-356): `client.complete(model, messages, tools=schemas)` (348); `_assistant_tool_message` replays `reasoning_content` (457, 473-475); `_tool_result_message` per call (479) → `_run_tool` (488, errors in-band, output cut at 16*max_chars 496-497) → `_dispatch_tool` (500-529) → hunter_tools `read_file` (28), `grep_repo` (69), `find_refs(root, symbol, None)` (83; mapping index always None, tool_hunter.py:528), optional `read_definition` (174), `entry_points` (194), `flows` (220), `obligations` (251). Paths clamped by `clamp_repo_path` (hunter_tools.py:15); `_source_lines` memo shared (41-59).
7. Ending: content with no tool used, or empty → `[]` (358-360); JSON → `_findings_from_content` (361-363); prose or out of steps after tools → `_final_answer` (364-368): one extra `complete(..., tools=[], json_object=False)` asking for path, line, title, rationale **and family** (388-405).
8. `_findings_from_content` (557) accepts a bare array, `{"findings":[...]}` or `{"value":[...]}` (561-569) via `_load_json` (581).
9. `_finding_from_item` (593): clamp path (600); `StaticFinding` with `evidence_level="suspicion"`, `confidence="low"`, id `tool-hunter:<path>:<line>` (615-631); `tags=[*tags, "family:<family>"]` when given (611-614, 628); `ranking_priority` from hotspot score (629); sorted by id (578). CLI dedups and caps (cli.py:989-1000).
```mermaid
flowchart TD
A[_attach_tool_hunter<br/>cli.py:962] --> B[resolve_hunter_client<br/>tool_hunter.py:239] --> C[run_tool_hunter<br/>tool_hunter.py:323]
C --> D[_SYSTEM_PROMPT<br/>tool_hunter.py:140]
C --> E[_user_message<br/>tool_hunter.py:423] --> E2[_hotspot_prompt<br/>tool_hunter.py:439]
E --> RF[read_file<br/>hunter_tools.py:28]
C --> L[client.complete loop<br/>tool_hunter.py:347-356] --> T[_dispatch_tool<br/>tool_hunter.py:500]
T --> RF
T --> G[grep_repo<br/>hunter_tools.py:69] --> SL[_source_lines memo<br/>hunter_tools.py:41]
T --> FR[find_refs<br/>hunter_tools.py:83] --> SL
T -.never enabled.-> RD[read_definition<br/>hunter_tools.py:174]
T -.never enabled.-> EP[entry_points<br/>hunter_tools.py:194]
T -.never enabled.-> FL[flows<br/>hunter_tools.py:220]
T -.never enabled.-> OB[obligations<br/>hunter_tools.py:251]
L --> FA[_final_answer<br/>tool_hunter.py:368] --> FC
L --> FC[_findings_from_content<br/>tool_hunter.py:557] --> FI[_finding_from_item<br/>tool_hunter.py:593] --> SF[StaticFinding + family: tag<br/>tool_hunter.py:615]
```
```mermaid
flowchart TD
CP[ChatClient Protocol .complete<br/>tool_hunter.py:163]
CA[Caller.ask cache/replay<br/>learn/program.py:369] --> MC[MeteredClient .complete<br/>plane/budget.py:68]
FM[FillMeter subclass<br/>learn/fill.py:44] -.is-a.-> MC
REC[_Recording<br/>plane/tasks/verify.py:66] --> MC
MC --> DS[DeepSeekChatClient .complete<br/>model/endpoint.py:91]
MC --> ORH[OpenRouterHunterClient .complete<br/>tool_hunter.py:203]
MC --> SC[ScriptedChatClient<br/>tool_hunter.py:176]
DS --> EPC[ChatEndpoint.cost / Prices<br/>model/endpoint.py:70,43]
DS --> OR[OpenRouterChatClient .complete_chat_raw<br/>provider/openrouter.py:84]
ORH --> ORC[OpenRouterChatClient .complete_chat<br/>provider/openrouter.py:69] --> OR
OR --> RT[call_with_retry<br/>provider/openrouter.py:34]
W[search Worker._call + SpendBudget<br/>search/worker.py:218] --> OR
DS -.implements.-> CP
MC -.implements.-> CP
```
`Caller` wraps `MeteredClient` (program.py:371-372; hunter_arm.py:65); verify.py's `_Recording` wraps it (181,
267, 378). The CLI hunt (cli.py:976) passes the bare client: no meter.
## Side effects
Every call POSTs `{base_url}/chat/completions` (openrouter.py:100-112), `temperature: 0` (94), 3 retries with
backoff on 429/5xx (34-52, 115). DeepSeek adds `thinking: disabled`, `response_format`, `logprobs`
(endpoint.py:164-175) and re-calls once on empty JSON (135-144): one `complete` = up to 2×3 HTTP attempts. A hunt
costs at most max_steps+1 calls. Tools read only inside the repo root; `entry_points`, `flows`, `obligations`
re-run `preprocess_repository` and quick-scan/adjudication or IR parsing over every target, uncached
(hunter_tools.py:304-308, 232-234, 268-274, 170-171). The hunter writes nothing; `Caller.ask` writes
`responses/<sha>.json` (program.py:412-413). Usage: `DeepSeekChatClient.usage` one row per raw call
(endpoint.py:98, 181-183); `cost_usd` via `ChatEndpoint.cost` (147-151, 78-88); `MeteredClient._absorb_rows`
diffs and prices (budget.py:154-167), raises `BudgetExhausted`/`AccountError` (107-116, 141-145). The
`OpenRouterHunterClient` path records no usage (tool_hunter.py:223 uses `complete_chat`): a meter sees zero.
## Duplication inside the unit
Chat-client types: `ChatClient` Protocol `.complete` (tool_hunter.py:163); `ScriptedChatClient` (176);
`OpenRouterHunterClient` adapter over `complete_chat` (203; drops `json_object`, records no usage 222-223);
`DeepSeekChatClient` `.complete/.bounded/.cost_usd` over `complete_chat_raw` (endpoint.py:91);
`OpenRouterChatClient` `.complete_chat/.complete_chat_raw/.complete_json` (openrouter.py:56, 69, 84, 128);
`MeteredClient` (budget.py:68) + `FillMeter` (learn/fill.py:44); `Caller.ask` (program.py:369, 397). Thin
wrappers: `_Recording` (verify.py:66), `_CountingClient` (model/scan.py:1628). Related: `RankerClient.complete_json`
(rank.py:31), `StubModel.complete_chat_raw` (search/worker.py:153-163), `_NoModel`/`_RefusingChat`
(verify.py:121, experiments.py:1003). Two resolvers: `resolve_hunter_client` (tool_hunter.py:239) and
`resolve_chat_endpoint` (endpoint.py:192); the hunter ignores config models and prices against
`DEFAULT_DETECTOR_MODEL` not the model it runs (tool_hunter.py:258); the OpenRouter branches build different
wrappers (tool_hunter.py:262 vs endpoint.py:221). Three "scripted flags" sets (tool_hunter.py:36-39,
endpoint.py:39). Four cost paths: `ChatEndpoint.cost`→`cost_usd` (endpoint.py:78, 147; push/assistance.py:103-110);
`cost_of` + `_absorb_rows` (budget.py:59, 158, arithmetic copied from the first per budget.py:60; `Caller.usd`
program.py:404-406, 420-423; `verify._sum_usage` 232-244); `SpendBudget`/`SpendReservation` (budget.py:260-322)
driven by search/worker.py:218-270 which repeats the DeepSeek extra_body logic (249-259); `_CountingClient.cost_usd`
(scan.py:1628-1640). Shape mismatch: `DeepSeekChatClient.usage` is a list (endpoint.py:98), `MeteredClient.usage`
a dict (budget.py:95); `Caller.ask` reads a dict (program.py:405), hence the shim in hunter_arm.py:56-65.
Dead tools: no `run_tool_hunter(` call in src or benchmarks passes `tools=` (cli.py:982, verify.py:189,
hunter_arm.py:245, benchmarks/archive/*). Only tests/test_hunter_curated_tools.py:143 enables `read_definition`,
`obligations`. So `read_definition`, `entry_points`, `flows`, `obligations` plus their schemas (tool_hunter.py:94-137)
and dispatch branches (505-521) are dead in production; `find_refs` mapping-record branch (hunter_tools.py:86-101,
118-164) too. Prompt mismatch: `_SYSTEM_PROMPT` names only 3 tools (141) and asks for no `family` (143) while
`_final_answer` asks for `family` (394); verify.py overrides the prompt (190).
## Dependencies
`complexity.map.Hotspot` (tool_hunter.py:13; inline at verify.py:178-186); `findings.StaticFinding` (14),
`quick_scan_findings` (hunter_tools.py:222, 232); preprocess (hunter_tools.py:8, 113, 305, 318);
`mapping.analyze_entry_points` (196, 202, 275, 279); `rank.rank_targets` (223, 232); semantic adjudicate, facts,
ir, obligations, dominance, functions (224-322); `config.ObligationsConfig`/`ResolvedConfig`. Cycle:
model/endpoint.py:22 imports tool_hunter; tool_hunter.py:254 lazily imports model.endpoint; plane/budget.py:16-17
imports both.
## Confidence
High on the happy path, client stack and dead tools. Not read in full: learn/fill.py, learn/experiments.py,
model/scan.py; callers of `complete_json` and `resolve_chat_endpoint` beyond push/assistance.py:84, learn/cli.py:171, 204.
@@ -0,0 +1,107 @@
# U7 Learn harness, memory store, plane fact tasks
Traced read-only 2026-10-13. Corrections to the brief: `learn memory build` does not call features, roles or
slice; it reads feature records from `--features` JSONL and `kind=features` store rows (produced by
benchmarks/learn/build_features.py or the plane `features`/`remember` tasks); slice.py runs when a prompt is
built (program.py:41). `learn/fill.py` (learn/cli.py:167) and `plane/tasks/agree.py` (runner.py:133 via
agree.yaml) are live. The lazy `plane.tasks.alerts` import at learn/features.py:484-486 is inside
`build_for_scan`, which only tests call.
## Happy path
1. `ousast learn labels` (cli.py:317-324, 466) → `_learn_labels` (1503) → `build_labels` (labels.py:938) → `load_sources(sources.toml)` (101; refuses unknown kinds, duplicates, excluded files) → `LabelBuilder.run` (501) reading every file through `SourceGuard` (126-167) → `Clone` cache `~/.cache/openultrasast` (374, 53) → `snapshot` (871), `write_labels` → `OUT/labels-<sha16>.jsonl` (956; cli.py:1516-1517).
2. Feature records: static profile `benchmarks/learn/build_features.py:58-79` (learn.features, learn.roles); plane profile: `features` task then `remember` writes `kind=features` rows.
3. `learn memory build` (learn/cli.py:102 → `_memory_build` 371): `load_sources`, `read_jsonl` labels + features, store `features` rows (383), `SideReader` (examples.py:274) → `build_examples` (146): refuses `origin: user` and unlisted sources (167-171); `excerpt`/`delta_diff`/`function_bounds` (excerpt.py:339, 383, 302) with `identities_of` redaction (173); `put_blob("excerpts")` (213); `example` rows (216); `put_rows` (229).
4. `learn memory embed` (optional): `_memory_embed` (342) → `embed_excerpts`, vectors at `embeddings/<slug>/<sha>` (embeddings.py:67-80).
5. `learn compile`: `_program_command` (274): `open_store`, `load_spec(compile.toml)` (compile.py:80), `load_examples` (examples.py:265), `EmbeddingCache`, `_caller` (241) = `MeteredClient` + `Caller`. `compile_program` (compile.py:347): `compile_split` + `outer_folds` + `assert_disjoint` (364-367); `bootstrap` via `Program.decide` (166) + `draw_demo_sets` (212); `propose` one call at temperature 0.7 (257); `score` with `balanced_brier` (296); `best` (315); `program_id` = sha256 of canonical JSON (328); `put_blob("programs", name=pid)` (479).
6. `learn evaluate`: `load_program` hash-checked (compile.py:484), `Program(spec_of(...))`; `compile_split`, `outer_folds(exclude=split.groups)`, `targets`/`unevaluable_families` (learn/cli.py:312-314); `predict` (evaluate.py:398), `evaluate` (457): `cross_fit` (calibrate.py:148), `check`/`offered` (197, 221), `nested_points` (evaluate.py:113), `summary`/`strata` cluster bootstrap (318-363). Report to stdout and `--out`; nothing to the store.
7. `learn experiment`: `_experiment` (learn/cli.py:114); `register` (experiments.py:364, put_row 403); `units` → `freeze_units` (466); `run` (899): `check_registered`, `read_units`, `program_folds`, seeded coin for arm order, one `Program.decide` per arm, `put_row(outcome_row)` (975), optional early stop (766), summary row (987); `analyse` (815): `paired_units`, `paired_bootstrap` (673), `exact_mcnemar` (722), `sequential_looks`, optional TOST, `decide` (795); `--record` writes `result_row` and `benchmarks/experiments/<id>/result.json` (learn/cli.py:141-144, experiments.py:67).
```mermaid
flowchart TD
CLI[ousast learn labels<br/>cli.py:317] --> LL[_learn_labels<br/>cli.py:1503] --> BL[build_labels<br/>learn/labels.py:938]
BL --> LS[load_sources sources.toml<br/>learn/labels.py:101]
BL --> SG[SourceGuard.check<br/>learn/labels.py:150]
BL --> WL[write_labels labels-sha.jsonl<br/>learn/labels.py:956] --> MB
BF[build_features static<br/>benchmarks/learn/build_features.py:1] --> MB
REM[remember features rows<br/>plane/tasks/remember.py:101] --> MB[_memory_build<br/>learn/cli.py:371]
MB --> BE[build_examples<br/>learn/examples.py:146] --> EX[excerpt / delta_diff<br/>learn/excerpt.py:339]
BE --> ST[(put_blob excerpts + put_rows example<br/>plane/memory.py:364)] --> PC[_program_command<br/>learn/cli.py:274]
PC --> LE[load_examples<br/>learn/examples.py:265]
PC --> CP[compile_program<br/>learn/compile.py:347]
CP --> SP[compile_split / outer_folds<br/>learn/folds.py:95]
CP --> BS[bootstrap + draw_demo_sets<br/>learn/compile.py:166] --> DEC
CP --> PR[propose<br/>learn/compile.py:257]
CP --> SC[score + best<br/>learn/compile.py:296] --> DEC[Program.decide<br/>learn/program.py:528]
DEC --> SL[function_slice<br/>learn/slice.py:41]
DEC --> CA[Caller.ask responses cache<br/>learn/program.py:397]
CP --> ART[(programs/pid blob<br/>learn/compile.py:479)]
ART --> EV[evaluate<br/>learn/evaluate.py:457] --> PD[predict<br/>learn/evaluate.py:398]
EV --> CF[cross_fit / check<br/>learn/calibrate.py:148]
EV --> NP[nested_points<br/>learn/evaluate.py:113]
ART --> XR[experiments.run<br/>learn/experiments.py:899] --> OR[(arm_outcome rows<br/>learn/experiments.py:975)] --> XA[analyse<br/>learn/experiments.py:815]
XA --> PB[paired_bootstrap<br/>learn/experiments.py:673]
XA --> MN[exact_mcnemar<br/>learn/experiments.py:722]
```
```mermaid
flowchart TD
OS[open_store<br/>plane/memory.py:929] --> RR[reconciler.results_root<br/>plane/reconciler.py:64]
OS --> FS[FileStore<br/>plane/memory.py:493]
OS --> S3[S3Store + verify_bucket<br/>plane/memory.py:612] --> S3C[S3Client boto3<br/>plane/memory.py:762]
MS[MemoryStore put_rows/rows/put_blob/get_blob<br/>plane/memory.py:267] --> FS
MS --> S3
RF[repo_facts facts.json<br/>plane/tasks/repo_facts.py:264] --> VE[verify units.jsonl<br/>plane/tasks/verify.py:247] --> AG[agree agreed.json<br/>plane/tasks/agree.py:153] --> FE
AL[alerts alerts.jsonl<br/>plane/tasks/alerts.py:117] --> FE
RO[roles roles.json<br/>plane/tasks/roles.py:141] --> FE[features features.jsonl<br/>plane/tasks/features.py:114]
RF --> FE
FE --> RE[remember memory.jsonl<br/>plane/tasks/remember.py:267] --> ING[ingest / ingest_rows<br/>plane/memory.py:968] --> MS
SEED[seed<br/>plane/memory.py:984] --> LR[reconciler.load_run/mark_done<br/>plane/reconciler.py:102]
SEED --> WS[loop.write_snapshot<br/>plane/tasks/loop.py:77] --> GU[improve.memory Guard<br/>plane/tasks/loop.py:67]
FE -.imports.-> LF[learn.features parts<br/>learn/features.py:53]
FE -.imports.-> LRO[learn.roles infer/from_model_roles<br/>learn/roles.py:426]
LF -.imports.-> RF
LRO -.imports.-> RF
```
## Side effects
Chat calls through `Caller.ask` (program.py:397), cached as `responses/<request_key>` (413; key = model, params
digest, messages, temperature, salted sample 356); `--replay-only` raises `ReplayMiss` (404); endpoint from
`resolve_chat_endpoint` (learn/cli.py:254); `--budget-usd` enforced by `MeteredClient`, exit 3 when hit
(321-323). Embeddings via OpenRouter (356), cached. Plane tasks `verify` (verify.py:336) and `roles`
(roles.py:95) also call models, metered, resumable through units.jsonl. Files: labels jsonl + snapshot, clones in
`~/.cache/openultrasast`, `--out` reports, `benchmarks/experiments/<id>/result.json`, units file; git subprocesses
(experiments.py:327-329). Default store `$OUSAST_RESULTS|~/ousast-results/plane/memory` (memory.py:938,
reconciler.py:64-65); layout `repos/<repo>/<pin>.jsonl` (389), `facts/<sha>.json`, excerpts, programs,
responses, embeddings, `index.jsonl` (432); FileStore atomic writes (529-533), 1 GiB free check (106). S3 via
`OUSAST_MEMORY=s3://…`, boto3 lazy (771), `verify_bucket` once per store (620-624), S3 Select reads (733).
`seed` writes `<run>/<task>/facts.json`, `summary.json`, snapshot `rows.jsonl`/`index.json` (1016-1019;
loop.py:94-102).
## The Learn↔PlaneTasks cycle
learn → plane.tasks: labels.py:48, features.py:27, roles.py:35, excerpt.py:35 import `repo_facts` tables and
helpers (`_EXCLUDED_DIRS`, `DECLARATION`, `GLOBAL`, `_declared_name`, `declared_functions`, `enclosing`,
`product_files`, `KEYWORDS`); features.py:484-486 imports `alerts.covers`, `engine_languages`, `quick_languages`
(test-only path). plane.tasks → learn: features.py:34-54 (18 `learn.features` parts + `learn.roles`),
roles.py:33-34 (`learn.roles.MODEL_ROLE_KINDS`, `learn.schema`), remember.py:42 (`learn.schema.validate_record`).
What learn takes is pure tables and helpers (repo_facts.py:30-146, alerts.py:56-74). `features` and `roles`
task logic can move into learn/ (case_features :114, recorded_case; chunks/parse_roles/classify/assemble
roles.py:61-127), leaving `main()` wrappers because the runner resolves `openultrasast.plane.tasks.<command>`
by name (runner.py:24, 133). `repo_facts` and `alerts` tables are also used by plane/generate.py:72,
plane/harvest.py:46, remember.py:44, plane/engine_alerts.py:39-41 and benchmarks/learn/*: they belong in a
neutral module both sides import.
## The Memory→Plane inversion
memory.py:933 (`open_store`) imports `reconciler.results_root` only for the default path, lazily because
reconciler.py:51 imports `open_store` at top. memory.py:989 (`seed`) imports `load_run`, `run_dir`,
`_read_json`, `mark_done`; memory.py:1002 imports `tasks.loop.write_snapshot` (which pulls improve.memory,
loop.py:67-70). `seed` belongs with orchestration (callers cli.py:1645): move it to reconciler.py or
plane/seed.py; `results_root` to a leaf module. `ingest` (968) has no plane dependency.
## Test-only code
`folds.leave_one_source_out` (129), `leave_one_framework_out` (138): tests only. `roles.load_roles` (539): no
caller at all. `features.build_for_scan` (467): only tests/test_learn_schema.py:15. `plane/tasks/features.py:202
recorded_case`: only tests/test_plane_features.py. Live: fill.py, schema.py (11 importers), agree.py, slice.py.
## Confidence
High on dispatch, compile/evaluate/experiment flow, store API, the lazy imports and cycle edges. Not read in
detail: `LabelBuilder.run` (labels.py:501-838), retrieve.py, bootstrap internals, `run_rule`/`analyse_rule`,
S3 `verify_bucket`/`_select`, verify/agree/loop internals.
@@ -0,0 +1,89 @@
# U8 Plane executor (ax)
Traced read-only 2026-10-13. Correction: `ousast plane run` never calls generate.py; it reads Run/Task/Model/
Workspace YAML written earlier by `plane workspaces --validation-set` (increment) or `plane harvest`.
Dockerfile.runner is at repo-root `plane/Dockerfile.runner`.
## Happy path of `ousast plane run <manifest>`
1. cli.py:464 → `_plane` (1530) → `load_profile` (1534, profile.py:203).
2. `_plane_memory("seed")` (1542, 1633) → `memory.seed` (memory.py:984) → `reconciler.mark_done` (280) for reusable facts.
3. `reconciler.run` (384): `open_store` before the lock, no fallback (396, memory.py:929); `Delivery(store, run, base)` (397); `state.json` seeded on rerun (398-401); `_acquire_lock` (269, 402).
4. `load_run` (102) + sibling `tasks/`, `models/`, `workspaces/` (105-107) via `load_manifests` (manifests.py:593) → `parse_manifest` (562) → `check_run_tasks` (545).
5. Optional legacy receiver if `OUSAST_DELIVERY=receiver` (409-412, egress.py:233).
6. `open_router` (router.py:147): `kubectl port-forward svc/atenet-router :80` in `ate-system` (43, 168-169) unless `router_url`/`OUSAST_ROUTER_URL` (160-163); reconciler.py:431.
7. Scheduling loop (432-457): producers, `serialize` labels, `--workers`; one `worker` thread per task (419-427).
8. `_execute` (335): `render_task` (145) builds Model/Workspace/Task docs with `OUSAST_RUN/TASK/OUTPUT_DIR`, `OUSAST_BUDGET_*`, `OUSAST_INPUTS`, `OUSAST_GIT_PINS`, writes `<run>/<task>/task.yaml` (348); `delivery.body` (delivery.py:107) presigns PUT `runs/<run>/<task>/output.tar` and GET per input; `delivery.reset` (114); `credentials(model)` (router.py:58) reads `secretKey.key` from host env, `StartError` names the variable never the value; `policy_for` (egress.py:87) grants TLS passthrough 443 to store, git hosts, model hosts; `ax.apply` (reconciler.py:197) → `egress.apply` (egress.py:194, `kubectl-ate create/update egress-policy`) → `egress.settle` sleeps 11 s (229).
9. `_await` (292): `ax resume task` (202), `ax get task` (207); on Running calls `Router.start_task` (router.py:100): `POST START_PATH` with header `ate-target-actor: <atespace>/<task>` and body `{run, task, credentials, delivery}` (107-110); done when `delivery.delivered` (delivery.py:120) sees the tar.
10. Runner in the container: `/usr/local/bin/ax-task-runner` → `python -m openultrasast.plane.runner` (Dockerfile.runner:27); `main` (runner.py:989) → health server (999-1003) → `_run_once` (906): `_prepare` parses `AX_TASK_YAML` (860); `gate.arm/wait` (924-930), `StartGate.offer` (303) checks credential names and delivery URLs (`parse_delivery` 257, https hostname only) → 202; `_materialise_once` (972) → `materialise` (546) git clone/fetch at pins; `fetch_inputs` (785); `run_command` (671) → `Child` (610) runs `python -m openultrasast.plane.tasks.<cmd>` (`task_module` 600), credentials only in the child env (684); `summary_after` (650) → `_finish` (887) → `_deliver_output` (821) → `deliver` (719) PUTs the tar (`build_tar` 694); marker `<state>/<task>.done.json` (900); serves until SIGTERM (1013-1015).
11. `delivery.collect` (125): downloads, extracts after rejecting symlinks, hardlinks, traversal (136-138); `publish` (155); `done.json` (150); then `ax.delete`, `egress.delete` (reconciler.py:371-374); `outcome_of` (258) reads summary.json.
12. `_State.set/_flush` (237-251) → local state.json mirrored via `delivery.save_state` (173); final status (470-475).
13. `_plane_memory("ingest")` (1549) → `remember_run` (tasks/remember.py:229); failure reported, not fatal (1660-1662).
14. `plane status` → `reconciler.status` (531) → `attribution` (510) → attribution.json.
Credentials: read on the host only (router.py:58-72), travel only in the start POST body (107), never in
ActorTemplate env (reconciler.py:358); router errors redacted (router.py:113-116, 126, 130, `redact` 50); the
runner logs status and reply text only (runner.py:357), child stderr redacted (621); delivery logs use
`safe_url` (713, 743). No log line prints a key.
```mermaid
flowchart TD
CLI[_plane<br/>cli.py:1530] --> PROF[load_profile<br/>profile.py:203]
CLI --> SEED[memory.seed<br/>memory.py:984] --> MD[mark_done<br/>reconciler.py:280]
CLI --> RUN[reconciler.run<br/>reconciler.py:384]
RUN --> STORE[open_store<br/>memory.py:929]
RUN --> LR[load_run<br/>reconciler.py:102] --> LM[load_manifests<br/>manifests.py:593]
RUN --> DEL[Delivery<br/>delivery.py:78]
RUN --> OR[open_router atenet-router<br/>router.py:147]
RUN --> EX[_execute<br/>reconciler.py:335]
EX --> RT[render_task<br/>reconciler.py:145]
EX --> BODY[Delivery.body presign<br/>delivery.py:107]
EX --> CRED[credentials<br/>router.py:58]
EX --> POL[policy_for<br/>egress.py:87]
EX --> AXA[Ax.apply<br/>reconciler.py:197] --> EGA[Egress.apply kubectl-ate<br/>egress.py:194]
EX --> AW[_await resume/poll<br/>reconciler.py:292] --> ST[Router.start_task POST<br/>router.py:100] --> RM[runner.main<br/>runner.py:989]
RM --> GATE[StartGate.offer<br/>runner.py:303] --> MAT[materialise<br/>runner.py:546] --> FI[fetch_inputs<br/>runner.py:785] --> RC[run_command tasks.X<br/>runner.py:671] --> FIN[_finish / PUT tar<br/>runner.py:887]
FIN --> DLV[Delivery.delivered<br/>delivery.py:120] --> COL[Delivery.collect<br/>delivery.py:125] --> OUT[outcome_of<br/>reconciler.py:258] --> STATE[_State.set + save_state<br/>reconciler.py:237]
STATE --> ING[remember_run ingest<br/>tasks/remember.py:229] --> STAT[status/attribution<br/>reconciler.py:531]
```
## Side effects
Host subprocesses: `ax apply/resume/get/delete` (reconciler.py:191-228), `kubectl-ate get/create/update
egress-policy` (egress.py:174-216), `kubectl port-forward` (router.py:168-169), receiver-mode `kubectl get svc`
(egress.py:133). Store: `runs/<run>/{state.json, <task>/output.tar, <task>/<artifact>, <task>/done.json}`
(delivery.py:11-16), presigned URLs (96-105), seed/ingest rows. Local `$OUSAST_RESULTS/plane/<run>/` (lock,
state.json, task.yaml, outputs, attribution.json; reconciler.py:64-69). Container: git clone/fetch
(runner.py:422-530), child `python -m`, HTTP on 80, marker in `/workspace/.ousast-state` (134). Sleeps: 11 s
egress settle, 3 s poll (reconciler.py:296).
## Dependencies and callers
Memory: `open_store` (929), `RUNS_PREFIX` (593), presign (547-555, 751-755). Budget: budget.py:16-17 imports
`Prices` (model.endpoint) and `ChatClient` (tool_hunter); the reconciler only exports `OUSAST_BUDGET_*`
(158-159). generate.py:67 is `price_of`, a price lookup, plus tasks.verify/repo_facts (72-73) and reconciler
helpers (71). learn: harvest.py:119 (`learn.excerpt.function_bounds`), 198 (`learn.labels.Clone`). Callers of
`reconciler.run`: only cli.py:1544 and ops/ax/smoke-run.sh:17. benchmarks/ax/batch.py:28 imports plane.memory
and runs its own `ax` CLI calls (62, 246), not the reconciler; search/coordinator.py:22-23 imports budget and
memory only; benchmarks/unseen/{draft.py:842, eligibility.py:332} import `reconciler.results_root`.
## What the replay and the hunter arm use
benchmarks/unseen/replay.py has no plane import (uses benchmarks.ax.batch → plane.memory only);
benchmarks/valid/hunter_arm.py:21 uses `plane.budget.MeteredClient` only. None of the executor modules are on
their path.
| Module | Callers |
|---|---|
| engine_alerts.py | cli.py:1597; benchmarks/learn/engine_pairs.py:37 (`docker_command`, `frozen_source`, `read_record`); benchmarks/learn/engine_trace.py:37 (`export_pin`); tests; model/audit.py:69 |
| harvest.py | cli.py:1579; `declares` used by benchmarks/learn/build_features.py:75 and engine_trace.py:635; tests |
| workspaces.py | cli.py:1570; tests/test_plane_reconciler.py:35; model/audit.py:80 |
| profile.py | live: cli.py:1532, reconciler.py:52, 390, doctor.py, ops/ax/*.sh, tests |
| generate.py | cli.py:1578 (`repin_templates`), 1668 (`increment`); harvest.py:43; tests |
## Removal candidates (never on `run`)
engine_alerts.py (its helpers would move to benchmarks/learn first), harvest.py (`declares` needs a home),
workspaces.py (no benchmark or ops caller), generate.py (manifest producer for harvest/increment only),
receiver mode in egress.py:128-160, 233-324 and runner.py:829-840 ("task 2.6 removes it", reconciler.py:409,
egress.py:106; ops/ax/up.sh:121-123 still creates the Service). doctor.py is live via `plane doctor`.
## Confidence
High on run/execute/await/start/runner/collect and credential handling. Medium on caller lists. Not read:
`memory.seed` body beyond 984-1020, tasks/remember.py, `materialise` internals, profile/doctor/engine_alerts/
harvest bodies, ops/ax/*.tmpl, prepare-substrate.py.
@@ -0,0 +1,98 @@
# U9 Search with proof, sandbox, regress
Traced read-only 2026-10-13. fossil-mcp's "unreachable Coordinator" is half wrong: `Coordinator` is live
(benchmarks/search/pilot_run.py:39, 378); `BatchExecutor` really has no src or benchmarks caller.
## Happy path A, search with proof (pilot)
1. `pilot_select.main` (pilot_select.py:302) writes `benchmarks/search/private/pilot.json` (310, 330) and a public record (311, 349).
2. `pilot_run.main` (pilot_run.py:411): validates manifest (466-505), `OpenRouterChatClient(max_attempts=1)` (509), `FileStore`/`open_store` (520), `AXLane` (522); dry run uses `probe.materialise` and `StubModel` (529-531, 60).
3. `Pilot.__init__` (79): `Board(store, "pilot-…")` + candidate hint commit (105-116), `Worker(...)` (117).
4. `Pilot.run` (376) → `Coordinator(self.board, self.dispatch, budget=SearchBudget(retries=0))` (378) `.run()`.
5. `Coordinator.run` (coordinator.py:210) seeds `bootstrap` (214-229); explore intent → `_call("explore")` (285).
6. `Coordinator._call` (125): reserve spend, `SearchTask` (28/149), checkpoint `_save` (85) → `Board.commit` (board.py:51) → `put_blob` (63); `executor(task)` (160) = `Pilot.dispatch`.
7. `Pilot.dispatch` (290) explore (327): `_executor` (178) opens an AX `SearchExecutorTask` (189) running `python -m openultrasast.search.executor` in the pod (batch.py:735) with the repo cloned (pilot_run.py:193); the worker's executor is `remote.client` (`ObjectStoreExecutor`, batch.py:752).
8. `Worker.__call__` (worker.py:471) → `explore` (351) → `_call` (218) reserves budget, `client.complete_chat_raw` (251); tools via `Worker.tool` (283): read_file/grep/run → `executor.submit` → pod `serve` → `InProcessExecutor.execute` (executor.py:622, 120, 197).
9. `write_demo` → `validate_demo` (demo.py:350) writes `demo/demo.json` (worker.py:294-310); `finish` → `load_demo` + `validate_oracle` (440-446) returns a fact with `demo`.
10. Coordinator stores the fact (coordinator.py:300-313); next loop `_call("verify")` (246).
11. `Pilot.dispatch` verify (297) → `Pilot.verification` (230): VM lane per revision `prepare` (217) → `SearchExecutorTask.prepare` → `prepare_verification` (batch.py:758-760, executor.py:206) → `extract_checkout` (verify_task.py:161), `validate_spec` (219), `verify(...)` (pilot_run.py:282); AX lane `verify(..., task_dispatcher=AXSideDispatcher)` (252-271) → `python -m openultrasast.search.verify_task` (search_verify.py:94) → `task_main`/`run_side`/`verify_side_task` (verify_task.py:279, 273; verify.py:632).
12. `verify` (verify.py:648): `_sandbox.isolation_check` (673); `_side` for affected and safe (690); `_side` (280) runs 3 fresh runs (327): `oracle_for` (399), `Canary.fresh` (402; oracles.py:307, 30), app launched via `_sandbox.run` (bwrap, _sandbox.py:105, 125), `oracle.observe` (verify.py:556).
13. `demonstrated` only if 3/3 affected observe and 0/3 safe do (697-698) → `VerificationRecord`.
14. `dispatch` returns outcome, `evidence_refs=["verify:owned"]` (318-324); coordinator appends `verified-…` and `_end("goal_met")` (256-268); `Pilot.run` fills spend and metrics (396-403) → `output.write` (540), summary (547-567).
## Happy path B, deep-mode regress
cli.py:66 imports `run_regression`; at 839 if REGRESS requested, `resolve_sandbox_probe().available()` (840)
runs `docker info` (sandbox/probe.py:22-33); `resolve_sandbox_runner()` (845) → `DockerCliRunner`
(runner.py:59-64); `run_stage("regress", run_regression(...))` (856-870) → `candidate.run_regression`
(candidate.py:177) → `select_candidates` (63) with `complexity.ledger.select_forced_candidates` (81); per
hotspot `_proposed_snippet`/`snippet_for` (203, 153) → `RegressionRunner.run_recipe` (126) → `recipe_for`
(recipes.py:16) → `run_snippet` (141) → `check_snippet_safety` (safety.py:63) → `DockerCliRunner.run`
(runner.py:72: `docker run --rm --network none --read-only …` 113-137); `verdict_from_result` (verdict.py:64):
`triggerable` on non-zero exit or oracle, `not_triggerable` on exit 0 (91-99); `write_verdicts` → verdicts.json
(cli.py:871, candidate.py:222); `persist_verdicts` (872); obligations re-ranked on TRIGGERABLE (874-877).
```mermaid
flowchart TD
A[pilot_select.main<br/>pilot_select.py:302] --> B[pilot_run.main<br/>pilot_run.py:411] --> C[Pilot.__init__ Board+Worker<br/>pilot_run.py:79] --> D[Pilot.run<br/>pilot_run.py:376]
D --> E[Coordinator.run<br/>coordinator.py:210] --> F[Coordinator._call / SearchTask<br/>coordinator.py:125]
F --> G[Board.commit<br/>board.py:51]
F --> H[Pilot.dispatch<br/>pilot_run.py:290]
H --> I[SearchExecutorTask AX pod<br/>batch.py:675] --> J[executor.serve/InProcessExecutor<br/>executor.py:622]
H --> K[Worker.explore<br/>worker.py:351] --> L[Worker._call LLM<br/>worker.py:218]
K --> M[write_demo validate_demo<br/>demo.py:350]
H --> N[Pilot.verification<br/>pilot_run.py:230] --> O[prepare_verification<br/>executor.py:390]
N --> P[verify<br/>verify.py:648] --> Q[_side x3 runs<br/>verify.py:280]
Q --> R[_sandbox.run bwrap<br/>_sandbox.py:105]
Q --> S[oracle_for/Canary<br/>oracles.py:307]
P --> T[demonstrated<br/>verify.py:698] --> U[_end goal_met<br/>coordinator.py:268] --> V[record jsonl<br/>pilot_run.py:540]
```
```mermaid
flowchart TD
A[cli scan deep REGRESS<br/>cli.py:839] --> B[SandboxProbe.available<br/>sandbox/probe.py:22] --> C[resolve_sandbox_runner<br/>sandbox/runner.py:59] --> D[run_regression<br/>regress/candidate.py:177]
D --> E[select_candidates<br/>regress/candidate.py:63]
D --> F[RegressionRunner.run_recipe<br/>regress/candidate.py:126]
F --> G[recipe_for<br/>regress/recipes.py:16]
F --> H[check_snippet_safety<br/>regress/safety.py:63]
F --> I[DockerCliRunner.run<br/>sandbox/runner.py:72] --> J[verdict_from_result<br/>regress/verdict.py:64] --> K[write_verdicts<br/>regress/candidate.py:222] --> L[rank_obligations on TRIGGERABLE<br/>cli.py:874]
```
## Side effects
Model: `complete_chat_raw` with DeepSeek settings (worker.py:251; client pilot_run.py:509), reserved against
`SpendBudget` (228, 238). AX: an executor pod per explore/prepare (pilot_run.py:189, batch.py:735), verify pods
in AX lane (search_verify.py:94), cap 32 tasks (pilot_run.py:213). Local: bwrap (_sandbox.py:125, 194), Chromium
`BrowserExecutor` for xss (pilot_run.py:286). Files: `benchmarks/search/private/runs/<uuid>/…` (demo.json,
built-*, verify-demo, boards/), `--out` jsonl opened with "x" (517). Regress: `docker info`, `docker run`,
scratch seed dir (runner.py:74), verdicts.json, complexity_ledger.json.
## Overlap between regress/ and search/
| | search/ | regress/ |
|---|---|---|
| Sandbox | `_sandbox.run` bwrap+seccomp (_sandbox.py:105); `_run_task` (verify.py:576); AX pods | `DockerCliRunner.run` (sandbox/runner.py:72) |
| Shared types | `SandboxJob`/`SandboxResult` from `..sandbox` (_sandbox.py:24, verify.py:29) | same (candidate.py:15) |
| Verdicts | demonstrated / inconclusive / no_oracle / could_not_run / could_not_build; no negative (verify.py:660, 692-701) | triggerable, not_triggerable, already_covered, inconclusive (verdict.py:10-26) |
| Oracle | owned canary oracles per family (oracles.py:30, 307), differential 3 vs 3 | exit code or optional callable (verdict.py:91-99), single run, not differential |
The only production caller of regress/ is cli.py:66, 858; all other references are tests. `run_recipe(oracle=…)`
is never passed an oracle in production (candidate.py:205).
## Coordinator and BatchExecutor
`Coordinator` is reachable (pilot_run.py:378; tests). `BatchExecutor` (coordinator.py:426-484): only
tests/test_search_coordinator.py:177, 189; the pilot uses `SearchExecutorTask` and `AXSideDispatcher`.
`coordinator.main` (487), `worker.main` (worker.py:530), `verify.main` (verify.py:714): shims with no in-repo
invoker (the `-m` launches target only search.executor and search.verify_task). Retry path `lane="docker"`
(coordinator.py:202-205) unreachable from the pilot (retries=0). `probe.probe`/`main` (probe.py:157, 239) used by
tests and benchmarks/ax/search_probe.py; only `materialise` (39) in dry runs. Fake sandbox runner/probe are
env-flag test doubles.
## Dependencies
Memory: `FileStore` (plane/memory.py:493), `open_store` (929); `Board` uses put_blob/get_blob (board.py:63, 42).
Budget: `BudgetExhausted` (budget.py:23), `SpendBudget` (260); `search/budget.py:8` `SearchBudget`, 49
`verification_timeout`. LLM: provider/openrouter.py:56, 84; `price_of` (pilot_run.py:26-33, 425).
benchmarks/ax: batch.py:675-762 (`SearchExecutorTask`, `prepare`), 498 (`dispatch`, BatchExecutor only),
search_verify.py:56 (`AXSideDispatcher`), 48. plane/tasks/verify.py:179 uses `complexity.map.Hotspot`, the
same type regress uses (candidate.py:11); search/ does not. Regress also uses complexity/ledger.py and
`config.SandboxConfig`.
## Confidence
High on call chains, verdict sets, Coordinator/BatchExecutor callers (benchmarks/search/private excluded). Not
read in full: `_side` build/readiness/HTTP driver (verify.py:345-560), `_prepare_verification`
(executor.py:390-488), recipes/safety bodies.
@@ -0,0 +1,222 @@
# Duplication report (2026-10-13)
Every claim was checked against the source by the phase-2 agents, not taken from the flowcharts. Paths are
relative to `src/openultrasast/` unless they start with `benchmarks/`. "First" dates are from
`git log --diff-filter=A` and give rough order only.
Verdict in one line: of ten candidate concerns, seven are real duplication (D1 to D5, D7, D10), two are one
owner missing (D8, D9), one is a misplaced type (D6). The three that matter most: D1 (LLM call and cost),
D4+D5 (enclosing function, callers, declaration tables), D3 (the quick-rule pipeline).
## Part A: concerns duplicated across features
### D1. Chat client, resolver and cost metering (accidental, highest value)
Where: `ChatClient` Protocol `.complete` (tool_hunter.py:163); `RankerClient.complete_json` (rank.py:31, tests
only). Clients: `ScriptedChatClient` (tool_hunter.py:176); `OpenRouterHunterClient` (203) drops `json_object`,
calls `complete_chat`, records no usage (222-223); `DeepSeekChatClient` (model/endpoint.py:91) uses
`complete_chat_raw`, keeps `usage` as a list (98), has its own retry on empty JSON (135-144);
`OpenRouterChatClient` (provider/openrouter.py:56, 69, 84). Wrappers: `MeteredClient` (plane/budget.py:68,
`usage` is a dict 95); `FillMeter(MeteredClient)` (learn/fill.py:44) reserves before each call; `Caller`
(learn/program.py:369) caches and replays, diffs `client.usage` as a dict (404-406); `_Recording`
(plane/tasks/verify.py:66); `_CountingClient` (model/scan.py:1628). Two resolvers: `resolve_hunter_client`
(tool_hunter.py:239) ignores config and prices against `DEFAULT_DETECTOR_MODEL` whatever model runs (258);
`resolve_chat_endpoint(config)` (endpoint.py:192) honours `[models]`; their OpenRouter branches build different
wrappers (tool_hunter.py:262 unmetered vs endpoint.py:221 metered). Duplicate env-flag sets tool_hunter.py:36-39
and endpoint.py:39. Four cost paths: `ChatEndpoint.cost` (endpoint.py:70-80; `cost_usd` 147; push/assistance.py:
103-110 inline); `cost_of` (budget.py:59, docstring: "the arithmetic of ChatEndpoint.cost") used by
`_absorb_rows` (158-167), `Caller.usd` (program.py:420-423), `verify._sum_usage` (232-244); `SpendBudget`/
`SpendReservation` (budget.py:260-322) with search/worker.py:218-270 repeating the DeepSeek `extra_body`;
`_CountingClient.cost_usd` (scan.py:1639). `_tokens` copied (budget.py:170, endpoint.py:262). Fallout: the
list-vs-dict `usage` mismatch needs the shim in benchmarks/valid/hunter_arm.py:56-65 ("made every live
classification fail while stubbed tests passed"); the CLI hunt passes the bare client (cli.py:976) so the
shipped scan's hunter spend is never metered.
Why: tool_hunter 09-03, endpoint 09-08, budget 09-29, program 10-01, fill 10-03, worker 10-05; each consumer
added the wrapper it needed (tool_hunter.py:240-244, 257; budget.py:263-266 "existing MeteredClient callers
retain their historical post-call metering").
Legitimate parts: the worker's hard reservation (fail-closed, `max_attempts=1` worker.py:188-189); `Caller`'s
replay cache; `ScriptedChatClient` as a test double. Accidental: two resolvers, two adapters, four cost
formulas, two usage shapes.
Unification: one `resolve_chat_client(config|None) -> (client, endpoint)`; one client class (today's
`DeepSeekChatClient`, renamed) with `usage: list[row]`; one `Meter` with `reserve(bound)`/`settle(row)` so
`MeteredClient`, `FillMeter` and the worker's `_call` are configurations (post-call vs reserve-first); one
`cost(row, prices)`; `Caller` stays as the cache layer; `_CountingClient`, `_Recording` become counters read
off the meter. Lost: calling OpenRouter unmetered; the hunter prices by its configured model (recorded dollars change).
### D2. Sandboxed execution for a verdict (runtime split legitimate, verdict split not)
Shared `SandboxJob`/`SandboxResult` (sandbox/runner.py:29-45; search/_sandbox.py:24; regress/candidate.py:15).
Runners: `DockerCliRunner.run` (sandbox/runner.py:67, 107-137) implements `SandboxRunner` (24);
`search/_sandbox.run` (105) is bwrap+seccomp with extra keyword-only args and does not satisfy the Protocol.
Probes: `resolve_sandbox_probe` (`docker info`, sandbox/probe.py:22-49) vs `_sandbox.isolation_check` (68).
Verdicts: regress `triggerable/not_triggerable/already_covered/inconclusive` + 14 reasons (regress/verdict.py:
10-26); search `demonstrated/inconclusive/no_oracle/could_not_run/could_not_build`, no negative
(search/verify.py:350-701); learn `vulnerable/not_vulnerable/unsure` (program.py:43, classification). Gap:
neither path produces `Rung.EXECUTION_CONFIRMED` (model/ladder.py:33; consumers push_scoring.py:84, mcp.py:98,
learn/schema.py:77), so the evidence ladder never receives execution evidence.
Why: regress/sandbox 09-03; _sandbox.py 10-05 for AX pods without Docker (docstring :1-8).
Unification: make `_sandbox.run` a `SandboxRunner` (options into `SandboxJob`) selected by
`resolve_sandbox_runner`; one outcome record `{outcome ∈ confirmed|refuted|inconclusive|unavailable, reason}`
that emits `Rung.EXECUTION_CONFIRMED`; keep two oracle strategies. Lost: nothing.
### D3. The quick-rule pipeline, re-implemented by each caller (accidental)
Each site runs preprocess → `analyze_entry_points` → `attach_reachability_hints` → `rank_targets` →
`quick_scan_findings` itself:
| Site | Ruleset | Shadow findings | Emit floors |
|---|---|---|---|
| cli.py:643-687 | `load_ruleset` + target ledger (634-638) | filtered (691) | quick/deep only; the standard pool skips them (670 vs 683-684) |
| gate.py:81-84 | None → `PATTERN_RULES` | not filtered here | no |
| map_gate.py:52-54 | `PATTERN_RULES` | not filtered | no |
| pairs.py:794-801, 815 | param | filtered | no |
| plane/tasks/alerts.py:120-122 | `PATTERN_RULES` | kept on purpose (docstring :5) | no |
| improve/evolve.py:311-315 | param | filtered | no |
| push/quick.py:87-92 | `PATTERN_RULES` | filtered; no preprocess, targets from changed files | no |
| hunter_tools.py:232 (`flows`) | None | filtered | no; re-runs preprocess per tool call (307), uncached |
| hunter.py:151 | pool | per task | no |
Plus benchmarks/learn/build_features.py and benchmarks/measurements/2026-09-30-php-quick-rules/measure_php_quick.py.
Legitimate differences: push's changed-files target set; alerts keeping shadow findings. The divergent ledger
and floors mean a gate measures different rules from what `ousast scan` ships.
Unification: one `quick_pipeline(root, targets=None, *, ruleset, policy, floors, include_shadow=False) ->
(targets, entries, rankings, findings)`; cli wraps steps in `run_stage` through a callback. Lost: nothing;
gates pass `ruleset=PATTERN_RULES` explicitly to keep today's numbers.
### D4. Enclosing function, function spans and callers (two naming systems, accidental)
Tree-sitter path: semantic/functions.py `_DECL` (56-69), `RESERVED_FUNCTION_LABELS` (23-51), sentinel
`<module>` (20), `function_at` (136); docstring (1-9): "the single source of that naming. Every consumer must
therefore name ranges the same way, or function-scoped matching silently fails". Used by hunter_tools
`_named_span` (317-323), overlay, pairs, model/candidates. Regex path: plane/tasks/repo_facts.py `DECLARATION`
(48-63), `KEYWORDS` (64-88), sentinel `<global>` (34), `enclosing` (131), `declared_functions` (118),
`call_sites` (154); used by plane/tasks/alerts.`function_at` (108-114); learn/features
`FeatureContext.function_at` (365-370), line-for-line the same as the alerts copy; learn/excerpt
`function_bounds` (302) with its own brace matcher; learn/labels.py:48, learn/roles.py:35, plane/harvest.py:46.
Two caller indexes: `repo_facts.call_sites` (154) written to facts.json (301), read by `verify.callers_of` (128)
accepting two facts shapes (131-141); `learn/features.callers()` (372-395) with a separate `_CALL` regex (49).
`hunter_tools.find_refs` (83) is a plain `\bsymbol\b` grep. `mapping.analyze_entry_points` (123) is the single
source for entry points, recomputed per caller (nine D3 sites plus learn/features.py:415, push/runner.py:123).
Why: functions.py 09-05 for the pair scorer; repo_facts.py 09-29 as a pod task avoiding tree-sitter, so learn
imported it. functions.py already has a regex fallback (120-133). Two sentinels and two keyword sets mean labels
(`<global>`) and scorer ranges (`<module>`) can name the same function differently, the exact failure
functions.py:6-8 warns about.
Unification: one `functions` module with `ranges(path, text, lang)` (tree-sitter, regex fallback),
`enclosing(line)`, `bounds(name)`, `call_sites(names)`, one sentinel, one reserved set; `repo_facts` re-exports
it so the runner still finds the task by name. Lost: Java/Go/Ruby/PHP patterns in DECLARATION must merge into
`_DECL` or labels change; re-label and diff before switching.
### D5. Declaration tables, product-file filter, language detection (accidental)
Product-file filters: `repo_facts.product_files` (96-115, rglob, own `_EXCLUDED_DIRS`/`_NOT_PRODUCT` 36-43,
`LANGUAGES` 33) vs `preprocess.enumerate_source_files` (96-100, 240, `git ls-files`); excluded-dir lists also at
preprocess.py:9 and model/partitions.py:194. Language detection: `preprocess.detect_language`/
`LANGUAGE_BY_EXTENSION` (11, 130) is the one extension map (push/runner.py:219-226, regress/candidate.py:14);
`learn/excerpt.resolve_language` (200) sniffs; alias maps at semantic/facts.py:18, semantic/obligations/facts.py:21,
model/candidates.py:49; language enumerations at learn/schema.py:54, repo_facts.py:33. The tables are imported
by learn/labels, features, roles, excerpt; plane/harvest, generate (`_digest` 72), remember (44); plane/tasks/
alerts, features: this is the Learn↔PlaneTasks cycle.
Unification: move `DECLARATION`/`KEYWORDS`/`product_files`/`_digest` into a neutral `source.py` or D4's module;
one `LANGUAGE_ALIASES` beside `LANGUAGE_BY_EXTENSION`. Lost: nothing; the cycle goes away.
### D6. Contracts and the CPG↔Model cycle (layered by design, one type misplaced)
contracts.py:1-93 generic `Contract`; model/contracts.py 12 classes (19-205); push/contracts.py 9 classes
(18-161) building on `QuestionIdentity`/`ScopeDecision` (14). Layered, not duplicated. The cycle is
`ExecutionBudget` only (model/contracts.py:19-33; cpg/backend.py:37, cpg/session.py:44); reverse direction
private names `_DeadlineExpired`/`_remove_owned_tree` (cpg/backend.py:1554) imported by model/partitions.py:14,
cpg/artifact.py:151 (lazy) and push/cache.py:22 (missed by the flowcharts). Budget types `ScanBudget`
(scan.py:79), `HunterBudget` (hunter.py:19), `SearchBudget` (search/budget.py:8), `SpendBudget` (budget.py:260),
manifest `Budget` (plane/manifests.py:185) cover different quantities: legitimate.
Unification: move `ExecutionBudget` and public `DeadlineExpired`/`remove_owned_tree` into contracts.py or a
`deadline.py`. Lost: nothing.
### D7. Reachability multipliers and severity (accidental constants, legitimate severity split)
Same dict at scoring/project_score.py:15 `REACH_MULT` and calibration.py:255 `_BASE_REACH_MULT` (both
06-17/22). The set `{"reachable","inferred-file-surface"}` at regress/verdict.py:28 and complexity/ledger.py:23;
produced once at findings.py:164. Severity: policy-derived via `SEVERITY_LABEL` (findings.py:39) for quick rules
(132) and overlay (overlay.py:134); hard-coded "medium" for model-layer findings (cli.py:1373); LLM's own or
"medium" for the hunter (tool_hunter.py:35, 619); sensitivity-mapped for obligations (check.py:162).
Unification: calibration imports `REACH_MULT`; one `REACHABLE_STATUSES`; `resolve_severity` whenever a family
maps to a CWE. Lost: nothing.
### D8. Hotspot construction for the hunter (legitimate inputs, no constructor)
`Hotspot` (complexity/map.py:20-28, `band` documented "high | medium | low") built by `_hotspot_from_signals`
(62-102), plane/tasks/verify.py:180-186 with `band="candidate"` (outside the vocabulary), and
benchmarks/valid/hunter_arm.py:233-242 with `band="low"`. Unification: `Hotspot.for_file(path, function=None,
rationale, band="low")` or let `run_tool_hunter` take `(path, function)` targets. Lost: nothing.
### D9. Result and record writers (accidental sequencing, one owner missing)
`findings.json` written at cli.py:708, 788, 801, 814, 877, 1005 after each proposer; merge-by-id dedup repeated
(784-787, 989-1000). Writes into the scanned repo all come from `CALIBRATION_DIR` (cli.py:94):
`false_positive_learnings.json` (635, 1021-1022), `rule_policy.json` (636; improve 1137),
`complexity_ledger.json` (765, 872), `rule_signals.json` (improve/evolve.py:173-174), `improve_journal.json`
(1141-1144); plane/tasks/loop.py:43 re-declares the path. The plane `MemoryStore` (memory.py:267) is a separate
store that knows nothing of these. Unification: accumulate findings per proposer, write once in a `finally`
(and move scoring after the merge, fixing the U1 bug); one `CalibrationLedger(target)` owns the five files.
Lost: partial findings.json on a crash unless the finally write stays.
### D10. Other
- JSON-in-fences parsers: provider/openrouter.py:205 `parse_json_content` (raises; used by program.py:326,
compile.py:264, worker.py:329); tool_hunter.py:581 `_load_json` (returns None); tool_hunter.py:409
`_looks_like_json`. `cpg/backend.extract_payload` (203) is not a duplicate (Joern BEGIN/END markers, partial
bodies). Fix: tool_hunter wraps `parse_json_content`.
- Retry: one helper `call_with_retry` (openrouter.py:34, used 115, 168); the duplication is in policy:
DeepSeek's empty-JSON re-call (endpoint.py:135-144) makes up to 2×3 attempts; `bounded()` (103-113) and the
worker (worker.py:188) force one attempt. One policy on the client.
- `resolve_*` factories (tool_hunter.py:239, endpoint.py:192, sandbox/runner.py:59, sandbox/probe.py:49,
cpg/backend.py:1813): legitimate pattern; only the first two overlap (D1).
- Two evidence vocabularies on one record: `StaticFinding.evidence_level` (findings.py:139, overlay.py:115,
tool_hunter.py:621, check.py:164) and `rung` (findings.py:34-35: "the older overlay pipeline's word"). Fold into `rung`.
- Excerpt helpers `hunter_tools.read_definition` (174) and `learn/excerpt.function_bounds` (302): covered by D4.
Confidence: high on D1, D3, D4, D6, D7, D9; medium-high on D2, D5, D8. Unverified: whether
`evaluate_benchmark` filters shadow findings for the gates; `MeteredEmbeddingClient`/`UnmeteredUsage`
(budget.py) as a fifth metering path; callers of `resolve_chat_endpoint` beyond push/assistance.py:84 and
learn/cli.py:254.
## Part B: repetition inside the big units (22 items, about 650 to 800 lines; four are correctness risks)
Correctness risks first: an unbounded Joern path drops partial answers on timeout (W4.3); plane uploads follow
redirects while search uploads refuse them (W8.2); the advisory engine findings are deduplicated differently in
the background summary and the terminal report (W3.3); evaluate builds folds differently from experiments (W7.2).
### U3 push/
- **W3.1 Git command prefix twice.** `["git","-c","protocol.allow=never","-c","core.hooksPath=…","-C",repo,*args]` at snapshot.py:94 (`_git`, subprocess.run, 10 s) and :570 (`_bounded_git`, Popen+selector); `_git` already routes through `_bounded_git` when a budget exists (83-91) and `_repository_path` (74-79) fakes a 10 s budget. Fix: one argv constant, then always `_bounded_git`. ~20 lines.
- **W3.2 Two forked-child helpers.** `_prepare` (runner.py:56-115: os.pipe, selector, pickle, 32 MiB cap) vs `_save`/`_write_artifact` (report.py:120-199: multiprocessing.Pipe); identical finally blocks (runner.py:107-115, report.py:190-199). Fix: `run_forked(fn, *, until, output_cap)`. ~35 lines.
- **W3.3 Advisory engine filter twice, different dedup keys.** `background._summary` (background.py:113-145) dedups on (family, path, line); `report._advisory_engine_lines` (report.py:241-265) on `defect_id`. Counts can differ for one push. Fix: `is_advisory(disposition)` + one key. ~15 lines.
- **W3.4 Two "scan complete" checks that disagree.** `report._completed_scan` (108-117) vs `policy.compare_evidence.complete` (policy.py:502-512; allows tier_zero deferrals); push/contracts.py:64 a third partial copy. Fix: one `scan_complete(scan, *, allow_tier_zero)`. ~15 lines.
- **W3.5 Budget arithmetic written out 66 times** across 14 files (push 47, model 13, cpg 16); `ExecutionBudget` (model/contracts.py:19-32) has no methods. Existing copies: snapshot.py:635, 642; scan.py:1435; backend.py:1377, 1399; report.py:160; partitions.py:117, 183, 188, 205, 253, 267; regions.py:313; scan.py:203. Fix: `expired()`, `remaining()`, `cleanup_end`, `check()` on `ExecutionBudget`, landing with the D6 move. ~40 lines.
- Unreachable: none at def level. vetoes.py (533 lines) only under `--experimental-record-vetoes` in replay; callers runner.py:49, benchmarks/push/m1a_record_vetoes.py:32, transfer_replay.py:37, tests. `GraphLease` Protocol (cpg/artifact.py:70) only tests/test_push_contracts.py.
### U4 model/ + cpg/
- **W4.1 Arbiter chosen by spec type three times**: `scan._grouped` (scan.py:1491-1506), `pipeline._arbitrate_all` (93-98), `pipeline._arbitrate` (117-121); identical `request_params(spec, *, function, file)` in taint.py:22, dominance.py:30, config_value.py:22; three row normalisers (taint._flows 178, dominance._operations 144, config_value._settings 125) and three `_location` builders (taint.py:297, dominance.py:124, config_value.py:96). Fix: `ARBITERS = {TaintSpec: Arbiter(...)}` table + shared `_rows`, `_site`. ~45 lines. Divergence: `_grouped` passes `hook_callbacks`, `field_parameter_sources` (1498-1505) but `taint.verdicts` (taint.py:124-127) does not, so the single-query fallback (scan.py:519) asks a different question from the batch.
- **W4.2 Three build routes in `_build_impl`** (backend.py:573-664): "frontend → None ⇒ dispose; `_apply_overlays`" twice (630-638, 648-653); unparsed warning twice (618-619, 654-655); `CpgResult(...)` twice (622-630, 656-664). Fix: `_frontend_or_none()`, `_result(paths, unparsed, dispose)`. ~30 lines. Bug candidate: the joern-parse retry (648) calls `_build_with_frontend` without `exclude`, so vendored trees are included on retry.
- **W4.3 `_run` vs `_run_bounded`** (backend.py:1514-1555 vs 1414-1484): same Popen/`_jvm_env`/communicate/group kill; the unbounded `_run` returns None on timeout and never calls `_partial_stdout`, so the partial-answer harvest at `_batch_once` (1247) works only under a budget. Fix: one `_run(command, timeout, cwd)` with deadline `min(timeout, budget.remaining())`. ~35 lines.
- **W4.4 `query` (1285-1316) vs `_batch_once` (1193-1281)**: same script check, session-or-`joern --script`, `_run`, `_engine_failure`, `extract_payload`, `_time_available`. Fix: `_invoke(cpg_path, query, params, *, harvest)`. ~25 lines.
- **W4.5 Shard merges** `query_across` (933) / `query_batch_across` (951). ~8 lines.
- Unreachable, new: `pipeline._arbitrate` config and taint branches (119-121; only caller 96 is inside `isinstance(spec, DominanceSpec)`), so `config_value.verdict` (90) and `taint.verdict` (72) are test-only; `taxonomy.hierarchical` (76) no caller at all; `taxonomy.related` (80) tests only. Confirmed: model/report.py `SliceRow`, `overfitting_gap`, `render_findings` tests only; model/audit.py no src importer.
### U7 learn/
- **W7.1 Repository-cluster bootstrap four times**: `evaluate.cluster_bootstrap` (318-337), `experiments._group_bootstrap_precision` (1107-1122), `experiments.paired_bootstrap` (673-706), `calibrate.check` (198-213); percentile rule inconsistent (`int(q*(n-1))` thrice vs `round` in calibrate._percentile 192). Fix: `cluster_resample(groups, rng)` + `percentile_ci(values, alpha)`. ~45 lines, one quantile rule.
- **W7.2 Compile split and folds three times**: learn/cli.py:312-313, compile.py:367-370, experiments.program_folds (458-463); cli and compile ignore a configured `k`, only compile calls `assert_disjoint`, experiments reads `k`. Fix: `folds.evaluation_folds(memory, cfg) -> (split, folds, digest)`. ~10 lines, closes an evaluate/experiment mismatch.
- **W7.3 JSONL readers five times**: `examples.read_jsonl` (333), plane/tasks/features._jsonl (188), remember._jsonl (156) byte-identical; loop.py:227 inline; typed `experiments.read_units` (440), `memory.read_memory` (956). Fix: one `read_jsonl`. ~15 lines.
- **W7.4 `run` (899-996) vs `run_rule` (1035-1096)**: `_checked_units` copied at 921-930 vs 1021-1032; summary `put_row` duplicated 989-995 vs 1088-1094; the row header repeated at 393, 521, 862-866, 990, 1093, 1305-1309. Fix: call `_checked_units`; `_experiment_row(manifest, pin, task, **extra)`. ~35 lines.
- **W7.5 SourceGuard vs improve.memory.Guard: not duplicates** (path allowlist vs row exclusion).
- Unreachable: `roles.load_roles` (539) no caller; `features.language_of` (65) no caller (new); `folds.leave_one_*` (129, 138) only tests/test_learn_retrieve.py:169-170; `features.build_for_scan` (467) only tests/test_learn_roles.py:180; `plane/tasks/features.recorded_case` (202) tests only. `program.prepare_ensemble` is live (program.py:521).
### U8 + U9 plane/, search/, benchmarks/ax
- **W8.1 Safe tar extraction five times**: `delivery.collect` (delivery.py:134-140) and `egress.Receiver.accept` (egress.py:278-284) byte-identical; `runner._export` (runner.py:500-504, `filter="data"`); `executor.unpack_checkout` (executor.py:658-690, size/count/.git caps); `verify_task.extract_checkout` (161-215, gzip cap). The plane copies have no size cap and no `filter="data"`. Fix: one `safe_extract(data, dest, *, max_bytes, max_files, allow_dirs)`. ~50 lines.
- **W8.2 Presigned PUT/GET with different redirect handling**: plane `runner.deliver`/`fetch` (719-790) follow redirects; `search/executor.URLTransport` (508-535) no proxy + NoRedirect; `verify_task.upload` (262-270) its own `_NoRedirect`. Fix: one `ObjectURL` client (no proxy, no redirect, bounded read, backoff). ~60 lines.
- **W8.3 Presigning: one store API, two layouts**: `Delivery.body` (delivery.py:95-111, `runs/<run>/<task>/`) vs batch.py:289-304, 716-723 (`engine-queue/...`). Part of W8.4.
- **W8.4 Two task-dispatch stacks**: plane (reconciler `Ax` 185-228 → Router POST → plane/runner 1,037 lines → tar PUT → `Delivery.collect`) vs search (batch.py `lane.ax` 241-262 → `SearchExecutorTask` 675-800 → `search/executor.serve` 622 → part PUTs). Four CLI wrappers: `reconciler.Ax._run` (188; no timeout, no KUBECONFIG, no `--atespace`), `batch.ax` (241-262), `egress.Egress._run` (174), `batch.check_cluster_health` (221); three error tails. First step: one `Cli(executable, base_args, env, timeout).run(*args)` + `error_tail(proc)` in plane/ (~40 lines). Merging the stacks is a phase-3 decision.
- **W8.5 Redaction three ways**: `router.redact` (router.py:50, exact values), `redaction.redact_secrets` (redaction.py:38, patterns, `***REDACTED***`), `search/task_storage.diagnostic` (100-120, own `sk-`/`gh*_`/`Bearer` regexes, `[redacted]`); `runner.safe_url` (713) URL-only. runner.py:621 imports `router.redact` (not a copy). Fix: `redaction.py` takes `values=`, `urls=True`. ~20 lines.
- Unreachable: receiver delivery mode (reconciler.py:409-412, egress.py:233-324, runner.py:829-840; `OUSAST_DELIVERY` set by no script, ops/ax/up.sh:121-124 still creates the Service); `delivery.put_input` (167) tests only; `coordinator.BatchExecutor` (426) tests only; `memory.purge` (836) + `delete_version` (833) tests only; `memory.drop_rows` (399) used by benchmarks/learn/build_features.py.
### U1 + U2 cli.py and core
- **W1.1 Argument plumbing**: `_main` (cli.py:426-470) unpacks the namespace for `_scan` (4), `_benchmark` (3), `_improve` (14, 432-447), `_pairs` (6), `_repos` (4). Fix: `set_defaults(func=…)`, handlers take `args`. ~45 lines.
- **W1.2 "Is a file" guards** at 1077-1078, 1123-1124, 1128, 1184-1185. Fix: `type=existing_file`. ~10 lines.
- **W1.3 `_benchmark` vs `mcp._benchmark`** (mcp.py:160-183): the MCP copy skips `runtime_seconds`, `baseline_findings`, `write_benchmark_artifacts`, so MCP benchmark runs leave no artifacts. Fix: `benchmark.run_benchmark(manifest, config, mode)` used by both; inline `_load_scan_findings` (1499). ~20 lines.
- **W1.4 Degradation recording 13 times** (cli.py:647, 698, 773, 817, 843, 996, 1312, 1330, 1343-1344, 1411, 1427, 1445-1446), mixing raw dicts and `stages.skip_as_degradation` (stages.py:32). Fix: `runtime.degrade(stage, reason, detail)`. ~15 lines.
- **W1.5 `write_*` wrappers of one json.dumps** (24 sites in 19 files: findings.py:113, verification.py:121, mapping.py:240, 245, config.py:256, rank.py:72, calibration.py:185, 190, overlay.py:126, preprocess.py:90, regress/candidate.py:222, complexity/map.py:57; inline cli.py:726, 746, 1713; `_write_json` in reconciler.py:76 and repo_facts.py:211). Fix: `run.write_json`, `write_records`. ~35 lines.
- **W1.6 Run-dir creators** run.py:18-26 vs benchmark.py:185-190 (~6 lines). **W1.7 findings.json six writes** (~5 lines).
- Unreachable, each only one test file: `calibration.compute_ranking_metrics`, `findings.build_quick_hunter_prompt`, `verification.validate_evidence_transition`, `fusion.should_fuse`, `harness.ProcessorSpec` (+ `_run_processor`, `_contract_violation` with no reference at all), `rank.rank_targets_with_model`, mapping's five task/record functions.
Confidence: high for W3.2-W3.4, W4.1-W4.4, W7.1-W7.4, W8.1, W8.2, W8.5, W1.1-W1.5 and the unreachable
list (name greps across src, tests, benchmarks, ops; dispatch-table or getattr calls would be missed); medium
for W8.4 and the line estimates. The `hook_callbacks` divergence, the missing `exclude` on retry, the redirect
handling and the plane tar limits come from reading, not from running anything.
@@ -0,0 +1,164 @@
# Unified proposal (2026-10-13)
Written by the orchestrator from 00 to 02. One rule throughout: prefer deletion over abstraction, one path over
configurable paths, and no new layer "for flexibility". Everything here serves the four active lines in
`.kiro/steering/overview.md`: hunter-learning-loop, pre-push-safety-net, search-with-proof,
engine-change-scoped-analysis. What serves none of them is cut.
## What the map says in three sentences
The code is not rotten (14 clone findings, 13 unreferenced lines), it is wide: each spec added a package and
nothing retired one. The same five jobs are done in several places: calling a model and counting its cost
(seven client types, four cost paths), finding the enclosing function and its callers (two naming systems, two
caller indexes, one grep), running the quick rules (nine copies), running code in a sandbox for a verdict (two
runtimes, two verdict vocabularies, neither reaching the evidence ladder), and writing scan results (findings
written six times, five ledgers under one directory). And three subsystems serve no active line: the improve
lever, the deep-mode regress arm, and the HarnessX-era half of the plane executor.
## Systems to unify (S1 to S6), in the order they pay off
### S1. One source-facts module (D4 + D5): the hunter's annotation producer
Today: semantic/functions.py (tree-sitter + regex fallback, sentinel `<module>`), plane/tasks/repo_facts.py
(regex only, sentinel `<global>`, DECLARATION/KEYWORDS/product_files), learn/features.callers (own regex),
plane/tasks/verify.callers_of, learn/excerpt.function_bounds, hunter_tools.find_refs (grep), plus alias maps in
three places. This is the exact set of facts hunter-learning-loop R3 names (enclosing function, callers, entry
points, roles, def-use), so it is the first thing to own.
Unified: `src/openultrasast/source.py` with `ranges(path, text, lang)`, `enclosing(ranges, line)`,
`bounds(ranges, name)`, `call_sites(text, names)`, `product_files(root)`, `DECLARATION`, `KEYWORDS`,
`LANGUAGE_ALIASES` (beside `preprocess.LANGUAGE_BY_EXTENSION`), one sentinel, one reserved set. Entry point:
`source.facts_for(root, path, line) -> SourceFacts(function, span, callers, entry_points)`; entry points keep
coming from `mapping.analyze_entry_points`, called once per run and passed in.
Old call sites: semantic/functions.py becomes the implementation moved here (keep `function_at` as an alias
for one release); plane/tasks/repo_facts.py keeps `main()` and imports the tables from source.py; learn/labels.py:48,
learn/features.py:27, 365-395, learn/roles.py:35, learn/excerpt.py:35, 302, plane/tasks/alerts.py:108-114,
plane/tasks/verify.py:128, plane/harvest.py:46, hunter_tools.py:83, 317-323 → source.py.
Lost: the per-language DECLARATION patterns for Java/Go/Ruby/PHP must be merged into `_DECL`; re-label the pair
corpus and diff `labels-<sha>.jsonl` before switching. The Learn↔PlaneTasks cycle disappears.
### S2. One chat client, one meter, one resolver (D1 + D10 parsers and retry policy)
Unified: `model/endpoint.py` keeps the single client (today's `DeepSeekChatClient`, renamed `ChatAdapter`; usage
is a list of rows) and the single resolver `resolve_chat_client(config | None) -> (client, endpoint)` that
honours `[models]` and prices by the model that runs. `plane/budget.py` keeps one `Meter` with
`reserve(bound)` and `settle(row)`; `MeteredClient` (post-call) and the reserve-first mode replace `FillMeter`,
`SpendBudget`/`SpendReservation` and search/worker `_call`. One `cost(row, prices)`. `Caller` (learn/program.py)
stays as the cache and replay layer on top of the meter. `parse_json_content` (provider/openrouter.py:205) is
the one JSON-in-fences parser; retry policy lives on the client only.
Old call sites: tool_hunter.py:239-262 `resolve_hunter_client` → `resolve_chat_client`; `OpenRouterHunterClient`
(203) and the env-flag set (36-39) deleted; `_load_json`/`_looks_like_json` (581, 409) → `parse_json_content`;
cli.py:976 passes a metered client; push/assistance.py:103-110 uses `cost`; search/worker.py:218-270 uses the
client and the reserve-first meter; learn/fill.py:44 `FillMeter` deleted; plane/tasks/verify.py:66 `_Recording`
and model/scan.py:1628 `_CountingClient` read counts off the meter; benchmarks/valid/hunter_arm.py:56-65 shim
deleted.
Lost: unmetered OpenRouter calls; recorded hunter dollars change because the hunter prices by its real model.
### S3. One quick-rule pipeline (D3)
Unified: `findings.quick_pipeline(root, targets=None, *, ruleset, policy, floors=None, include_shadow=False)
-> QuickRun(targets, entry_points, rankings, findings)`. cli.py wraps each step in `run_stage` through a
callback argument. Old call sites: cli.py:641-687 (and the standard-mode pool at hunter.py:103-160, which keeps
its thread pool but calls the pipeline), gate.py:81-84, map_gate.py:52-54, pairs.py:794-815, plane/tasks/alerts.py:
120-122, improve/evolve.py:311-315 (deleted with S9), push/quick.py:87-92 (passes its changed-file targets),
hunter_tools.py:232 (deleted with S8), benchmarks/learn/build_features.py. Fixes the standard-mode floors bug
(cli.py:670 vs 683-684). Lost: nothing; gates pass `ruleset=PATTERN_RULES` to keep their numbers.
### S4. One sandbox verdict on the evidence ladder (D2)
Unified: `search/_sandbox.run` implements `sandbox.SandboxRunner` (its options move into `SandboxJob`);
`resolve_sandbox_runner` picks bwrap or docker; one outcome record `ExecutionOutcome(outcome ∈ confirmed |
refuted | inconclusive | unavailable, reason, runs)` that emits `Rung.EXECUTION_CONFIRMED` (model/ladder.py:33)
so push_scoring.py:84, mcp.py:98 and learn/schema.py:77 finally receive execution evidence. Two oracle
strategies stay (owned canary, exit code). regress/ is deleted (S9): the deep-mode REGRESS stage calls the
proof arm's verify path once that lands, and until then deep mode equals standard mode and says so in a
degradation. Lost: snippet-based regress in `scan --mode deep` until search-with-proof is wired to scan. This
is the one capability loss in the proposal and it is deliberate: the maintainer named proof as the block
gate and the search line owns it.
### S5. Deadline type and the CPG↔Model cycle (D6 + W3.5)
Unified: `ExecutionBudget` moves to `contracts.py` with `expired()`, `remaining()`, `cleanup_end`, `check()`;
`DeadlineExpired` and `remove_owned_tree` become public in a `deadline.py` next to it. Old call sites: the 66
budget expressions (push 47, model 13, cpg 16), cpg/backend.py:37, cpg/session.py:44, model/partitions.py:14,
cpg/artifact.py:151, push/cache.py:22. Also W4.3: one `_run` in cpg/backend.py so standard scans get partial
stdout; W4.1: one `ARBITERS` table used by scan.py and pipeline.py (and the `hook_callbacks` divergence closes).
Lost: nothing.
### S6. Scan assembly: write once, score last, one ledger (D9 + U1 bugs + W1.4 + W1.5)
Unified: `_run_scan` accumulates findings per proposer in memory and writes `findings.json` once in a
`finally`; scoring and verification move after MAP so model, overlay, obligation and hunter findings are scored
and verified (the U1 bug); `CalibrationLedger(target)` owns the five files under `.openultrasast/calibration/`;
`runtime.degrade(stage, reason, detail)` replaces 13 inline appends; `run.write_json` replaces 24 `write_*`
wrappers; `benchmark.run_benchmark` is shared by cli and mcp (W1.3). fusion.py is deleted: its decisions never
reach findings or score (U1), and harness.py loses the processor machinery nobody builds. Lost: a partial
findings.json on a crash, unless the finally write is kept (it is).
### S7. Memory store as a leaf; plane executor trimmed (U7 inversion + U8 + W8)
`plane/memory.py` loses its three lazy imports: `seed` moves to `plane/seed.py`, `results_root` to
`plane/paths.py`. The executor keeps reconciler, runner, delivery, router, egress, doctor, profile and loses
engine_alerts.py, harvest.py, workspaces.py, generate.py and receiver mode (their live helpers `declares`,
`docker_command`, `frozen_source`, `read_record`, `export_pin` move into benchmarks/learn, their only users).
One `Cli` wrapper and `error_tail` for ax and kubectl (reconciler, egress, benchmarks/ax/batch); one
`safe_extract` and one `ObjectURL` client shared by plane/runner, delivery, search/executor and verify_task
(plane extraction gains the size cap and the no-redirect rule). The two dispatch stacks stay two for now: the
search stack is on the active proof line and the plane stack is parked with plane-on-kubernetes; merging them
is a decision for when the ax lane is back.
## Cuts (S8, S9): what leaves the tree
S8, hunter unit: the four never-enabled tools and their schemas and dispatch branches (hunter_tools.py:174-290,
tool_hunter.py:94-137, 505-521), the dead `find_refs` mapping branch (hunter_tools.py:86-101, 118-164),
`OpenRouterHunterClient`, `_load_json`, `_looks_like_json`, `ScriptedChatClient` stays. The facts those tools
computed come back as annotations from S1, which is what the hunter-learning-loop spec asks for.
S9, off every active line: improve/ (1,175 lines; `ousast improve`, the `loop-improve` task at
plane/generate.py:380 and plane/tasks/loop.py, plane/memory.py:1002's snapshot guard, docs/memory.md and
README sections); regress/ (549) with the REGRESS branch cli.py:839-880; skills.py + index.py (446; the routed
ids are trajectory metadata only, hunter.py:137-139, 96, 157); model/audit.py (255) to a maintainer script or
gone; test-only functions in mapping.py (188-237), calibration.py:108, findings.py:102, verification.py:39,
rank.py:46-69, model/report.py:24-75, taxonomy.hierarchical/related, config_value.verdict, taint.verdict,
pipeline._arbitrate's dead branches, learn/roles.load_roles, learn/features.language_of and build_for_scan,
folds.leave_one_*, plane/tasks/features.recorded_case, coordinator.BatchExecutor, memory.purge/delete_version,
delivery.put_input, worker.main/verify.main/coordinator.main shims, cpg/queries/vocabulary.sc. Each goes with
its test file. push_inputs.py moves under benchmarks/push. mcp.py stays until the mcp-shift-left-agent spec is
decided (breadth.toml: parked, close date 2026-12-01). map_gate.py and pair_gate.py stay with CI for now; their
removal needs cli.py:49, ci.yml, dagger/ci.py and the gate baseline changed together.
Estimated effect (lines, rough): duplication removal 650 to 800 in src; cuts about 4,500 in src (improve 1,175,
regress 549, plane dead modules about 1,300, skills+index 446, audit 255, hunter tools and client about 450,
test-only functions about 350) and the tests that only exercised them, about 9,000. src goes from 60k toward
55k and tests from 54k toward 45k, with every remaining package on an active line or in breadth.toml as parked
with a close date.
## Order of work
1. S9 cuts and S8 (cheap, no design, unblocks everything else; one commit per package with its tests).
2. S1 source facts (the annotation producer hunter-learning-loop needs first).
3. S2 client and meter (the ablation runs need one metered client with replay).
4. S3 quick pipeline, S5 deadline type (small, mechanical).
5. S6 scan assembly (fixes the scoring-before-MAP bug; touches cli.py most).
6. S4 sandbox verdict (with search-with-proof's next increment).
7. S7 plane trim (when plane-on-kubernetes resumes or is closed).
## The proposed system
```mermaid
flowchart TD
subgraph inputs
SCAN[ousast scan<br/>cli.py:619] --> QP
PUSH[pre-push runner<br/>push/runner.py:609] --> QP
GATE[gate.py / pairs / alerts] --> QP
end
QP[quick_pipeline<br/>findings.py (S3)] --> FACTS
QP --> ENGINE
FACTS[source.facts_for<br/>source.py (S1)] --> ANN[annotation model<br/>hunter-learning-loop R3]
ENGINE[scan_repository<br/>model/scan.py:143] --> RUNG
ANN --> HUNT[run_tool_hunter<br/>tool_hunter.py:323]
HUNT --> CLIENT[ChatAdapter + Meter + Caller<br/>model/endpoint.py, plane/budget.py, learn/program.py (S2)]
CLIENT --> CLS[classify<br/>learn/program.py:473]
CLS --> RUNG[evidence ladder Rung<br/>model/ladder.py:29]
RUNG --> ADMIT[admit_candidates<br/>push/policy.py:889]
RUNG --> PROOF[search verify<br/>search/verify.py:648]
PROOF --> EXEC[ExecutionOutcome -> EXECUTION_CONFIRMED<br/>sandbox (S4)]
EXEC --> ADMIT
ADMIT --> REPORT[deliver_report<br/>push/report.py:423]
ENGINE --> ASSEMBLE[write once, score last<br/>cli.py (S6)]
HUNT --> ASSEMBLE
ASSEMBLE --> LEDGER[CalibrationLedger<br/>(S6)]
CLS --> STORE[(MemoryStore leaf<br/>plane/memory.py (S7))]
LEARN[learn compile / evaluate / experiment<br/>learn/cli.py] --> CLIENT
LEARN --> STORE
FACTS --> LEARN
BUDGET[ExecutionBudget + DeadlineExpired<br/>contracts.py (S5)] -.-> ENGINE
BUDGET -.-> PUSH
```
+156
View File
@@ -0,0 +1,156 @@
# Handoff prompts (copy each into /make-plan)
Order follows 03-unified-proposal.md. Every prompt is read-only until its plan is approved. Shared guards:
work on main, no branches; one package per commit with its tests; run `ruff check` and the touched test files
before each commit; plain English in comments and commit messages; never read or echo `.env`; update
`.kiro/breadth.toml` when a capability changes status.
## P0. Cuts: packages and functions on no active line (S9 + S8)
```
Plan the removal of code that serves none of the four active lines in .kiro/steering/overview.md, using
PATHFINDER-2026-10-13/03-unified-proposal.md section "Cuts" and the per-unit "removal candidates" in
PATHFINDER-2026-10-13/01-flowcharts/*.md as the inventory. One commit per package, tests included.
Packages: src/openultrasast/improve/ (and `ousast improve` in cli.py:237-281, 433, 1106-1170; the loop-improve
task at plane/generate.py:380 and plane/tasks/loop.py:67-104, 194-224; plane/memory.py:1002's snapshot guard;
README.md:220, 314; docs/memory.md, docs/examples.md:58-85, docs/index.md:45, docs/threat-model.md:18;
ops/ax/README.md:101; tests test_improve*.py, test_cli_improve.py, test_subtraction_complete.py:67-89,
test_gate.py:135-136); src/openultrasast/regress/ with cli.py:839-880 (deep mode records a degradation
"regress retired, proof arm pending" and otherwise equals standard; tests test_regress_*.py,
test_split_sink_regress.py, test_reports.py:12); skills.py + index.py (hunter.py:137-139 stops routing; drop
`selected_skills` from the task and trajectory at hunter.py:96, 157; tests test_skills, test_index, the skills
part of test_hunter); model/audit.py (either delete with tests/test_module_audit.py and the
2026-09-08-module-audit.json reference in overview.md, or move to ops/tools/); hunter tools never enabled
(hunter_tools.py:174-290 read_definition/entry_points/flows/obligations, tool_hunter.py:94-137 schemas,
505-521 dispatch, the find_refs mapping branch hunter_tools.py:86-101, 118-164; tests
test_hunter_curated_tools.py); cpg/queries/vocabulary.sc with tests/test_cpg_queries_compile.py:32 and
benchmarks/push/ontology_gap.py:102.
Functions (each with its only test): mapping.py:188-237 five task/record functions and their record classes
(:39-56); calibration.py:108; findings.py:102; verification.py:39; rank.py:46-69 and RankerClient (:31);
fusion.py:94 and, if S6 is accepted, fusion.py entirely (cli.py:720-726, fusion.json); harness.py:41-55,
125-166 processor machinery; model/report.py:24-75; model/taxonomy.py:76, 80; model/config_value.py:90 and
model/taint.py:72 (tests move onto `verdicts()[0]`), model/pipeline.py:117-121 becomes dominance only;
learn/roles.py:539; learn/features.py:65, 467; learn/folds.py:129-157; plane/tasks/features.py:202;
search/coordinator.py:426-484 and :487, search/worker.py:530, search/verify.py:714; plane/memory.py:833-843;
plane/delivery.py:167; receiver mode plane/egress.py:128-160, 233-324, plane/runner.py:829-840,
plane/reconciler.py:409-412, ops/ax/up.sh:121-124. Move push_inputs.py to benchmarks/push/ (callers
benchmarks/push/{measure,m1a_record_vetoes,transfer_replay}.py, tests/test_push_inputs.py).
Guards: do not touch mcp.py, map_gate.py, pair_gate.py, vetoes.py (parked or CI-bound, see
.kiro/breadth.toml); confirm each deletion with `grep -rnw <name> src tests benchmarks ops docs` before
removing; keep a table in the commit message of lines removed per package; re-run `fossil-mcp dead-code src
--min-confidence certain` at the end and expect zero.
```
## P1. One source-facts module (S1)
```
Plan `src/openultrasast/source.py` as the single owner of function ranges, enclosing function, function
bounds, call sites, product-file enumeration, declaration tables and language aliases, per
PATHFINDER-2026-10-13/02-duplication-report.md D4 and D5 and the facts table in 01-flowcharts/U5-semantic.md.
Entry points: `ranges(path, text, lang)`, `enclosing(ranges, line)`, `bounds(ranges, name)`,
`call_sites(text, names)`, `product_files(root)`, `facts_for(root, path, line, *, entry_points)`.
Implementation moves from semantic/functions.py (tree-sitter with the regex fallback at :120-133, sentinel
`<module>`, RESERVED_FUNCTION_LABELS :23-51, `_DECL` :56-69 merged with repo_facts DECLARATION :48-63 and
KEYWORDS :64-88 for Java, Go, Ruby, PHP). Call sites to rewrite: plane/tasks/repo_facts.py:30-146 (keep
main() and re-export the tables), plane/tasks/alerts.py:108-114, plane/tasks/verify.py:128-141,
learn/labels.py:48, learn/features.py:27, 49, 365-395, 415, learn/roles.py:35, learn/excerpt.py:35, 302,
plane/harvest.py:46, plane/generate.py:72, plane/tasks/remember.py:44, hunter_tools.py:83, 317-323,
benchmarks/valid/hunter_arm.py (annotation producer for hunter-learning-loop R3).
Guards: one sentinel and one reserved set, no compatibility shim beyond a one-release `function_at` alias;
before switching, regenerate labels-<sha>.jsonl with the old and the new module and diff them, and record
the diff in benchmarks/measurements/; do not add a registry of "range providers", a switch on language is
enough; nothing in source.py may read labels, advisories or commit messages (hunter-learning-loop R3.2).
```
## P2. One chat client, one meter, one resolver (S2)
```
Plan the collapse of the LLM client stack described in PATHFINDER-2026-10-13/02-duplication-report.md D1 and
D10 and 01-flowcharts/U6-hunter-client.md. Target: model/endpoint.py keeps one client class (rename
DeepSeekChatClient to ChatAdapter, usage as a list of rows, retry policy including the empty-JSON re-call
lives here and nowhere else) and one resolver `resolve_chat_client(config | None) -> (client, endpoint)`
that honours [models] and prices by the running model; plane/budget.py keeps one Meter with
`reserve(bound)`/`settle(row)` and one `cost(row, prices)`; learn/program.py Caller stays as the cache and
replay layer; provider/openrouter.py parse_json_content is the only JSON-in-fences parser.
Call sites: tool_hunter.py:36-39, 163-262 (delete OpenRouterHunterClient and the env-flag sets, keep
ScriptedChatClient and the ChatClient Protocol), 409, 581; cli.py:976 (pass a metered client);
push/assistance.py:84, 103-110; search/worker.py:153-165, 188-189, 218-270 (reserve-first mode of the
Meter; StubModel stays); learn/fill.py:44 (FillMeter deleted); learn/cli.py:241, 254; plane/tasks/verify.py:
66, 181, 232-244, 267, 378; model/scan.py:1628-1640; budget.py:59-60, 154-170, 260-322; endpoint.py:39,
135-144, 262; benchmarks/valid/hunter_arm.py:55-66 (shim deleted).
Guards: no new abstraction above ChatAdapter; do not keep OpenRouterHunterClient behind a flag; the only
behaviour change allowed is "hunter dollars are priced by the configured model", record it in the commit;
error messages carry exception types, never a key; run tests/test_hunter_arm.py,
tests/test_model_endpoint.py, tests/test_plane_budget*.py, tests/test_search_worker.py.
```
## P3. One quick-rule pipeline and the deadline type (S3 + S5)
```
Plan two mechanical consolidations from PATHFINDER-2026-10-13/02-duplication-report.md D3, D6 and W3.5,
W4.1, W4.3.
(a) `findings.quick_pipeline(root, targets=None, *, ruleset, policy, floors=None, include_shadow=False)
-> QuickRun(targets, entry_points, rankings, findings)` with an optional `stage(name, fn)` callback so
cli.py can keep run_stage. Call sites: cli.py:641-687, hunter.py:103-160 (keeps its pool, calls the
pipeline per target), gate.py:81-84, map_gate.py:52-54 (pass ruleset=PATTERN_RULES explicitly), pairs.py:
794-815, plane/tasks/alerts.py:120-122 (include_shadow=True), push/quick.py:87-92 (passes changed-file
targets), benchmarks/learn/build_features.py. Fix the standard-mode floors bug (cli.py:670 vs 683-684).
(b) Move ExecutionBudget (model/contracts.py:19-33) to contracts.py with expired(), remaining(),
cleanup_end, check(exc); make DeadlineExpired and remove_owned_tree public in deadline.py; rewrite the 66
budget expressions (push 47, model 13, cpg 16: snapshot.py:635, 642, scan.py:203, 1435, backend.py:1377,
1399, report.py:160, partitions.py:117-267, regions.py:313), cpg/backend.py:37, cpg/session.py:44,
model/partitions.py:14, cpg/artifact.py:151, push/cache.py:22. Then one `_run` in cpg/backend.py (merge
1514-1555 into 1414-1484 so standard scans harvest partial stdout) and one ARBITERS table for scan.py:
1491-1506 and pipeline.py:93-121 (closing the hook_callbacks divergence at taint.py:124-127).
Guards: no behaviour change except the two named bugs; gate numbers in benchmarks/measurements/
2026-09-06-gate-baseline.txt must not move; run the gate, map_gate, pair_gate and test_push_*, test_model_*,
test_cpg_* files.
```
## P4. Scan assembly: write once, score last, one ledger (S6)
```
Plan the rewrite of `_run_scan` (cli.py:619-959) per PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md
"Likely bugs" and 02-duplication-report.md D9, W1.3-W1.5: findings accumulate per proposer and are written
once in a finally; scoring (cli.py:731-746) and verification (711) move after MAP (757-835) so model,
overlay, obligation and hunter findings are scored and verified; `CalibrationLedger(target)` owns
false_positive_learnings.json, rule_policy.json, complexity_ledger.json (cli.py:94, 635-636, 765, 872,
1021-1022; plane/tasks/loop.py:43 reads the same path); `runtime.degrade(stage, reason, detail)` on
HarnessRuntime replaces the 13 inline appends (cli.py:647-1446) and stages.skip_as_degradation;
`run.write_json` and `write_records` replace the 24 write_* wrappers; `benchmark.run_benchmark` is shared
by cli.py:1076 and mcp.py:160-183 so MCP benchmark runs write artifacts; fusion.py is deleted (cli.py:720-726,
fusion.json); harness.py processor machinery is deleted (41-55, 125-166, cli.py:627-632).
Guards: keep the finally write so a crash still leaves findings.json; the manifest and SARIF formats do not
change; add one test that a model-layer finding appears in score.json and verification.json; run
tests/test_scan_pipeline_integrity.py, test_reports.py, test_scoring.py, test_mcp.py, test_benchmark.py.
```
## P5. One sandbox verdict on the ladder (S4)
```
Plan the unification in PATHFINDER-2026-10-13/02-duplication-report.md D2 and
01-flowcharts/U9-search-sandbox-regress.md: search/_sandbox.run (:105) implements sandbox.SandboxRunner
(options move into SandboxJob, sandbox/runner.py:29-45); resolve_sandbox_runner (:59) chooses bwrap or
docker; one `ExecutionOutcome(outcome in confirmed|refuted|inconclusive|unavailable, reason, runs)` that
search/verify.py:648-701 produces and that emits Rung.EXECUTION_CONFIRMED (model/ladder.py:33) into the
evidence ladder so push_scoring.py:84, mcp.py:98 and learn/schema.py:77 receive execution evidence; the
canary oracle (oracles.py:30, 307) and the exit-code oracle remain two strategies. regress/ is already gone
(P0); the deep-mode REGRESS stage (cli.py:839-880) becomes a call into search's verify path for findings
that carry a demo, else a degradation.
Guards: do not keep the regress verdict vocabulary behind a flag; the differential 3-vs-3 rule and the 0
false proofs requirement of search-with-proof Req 6.2 are unchanged; the only new rung producer is the
verify path; run tests/test_search_verify*.py, test_sandbox*.py, test_push_scoring.py.
```
## P6. Memory store as a leaf; plane executor trimmed (S7)
```
Plan, per PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md "Memory→Plane inversion",
U8-plane-executor.md "Removal candidates" and 02-duplication-report.md W8.1-W8.5: move `seed`
(plane/memory.py:984-1020) to plane/seed.py and `results_root` to plane/paths.py so memory.py has no plane
import; delete plane/engine_alerts.py, harvest.py, workspaces.py, generate.py and their cli.py subcommands
(1570-1611, 1667-1700) after moving `declares` (harvest.py:116), `docker_command`, `frozen_source`,
`read_record`, `export_pin` (engine_alerts.py) into benchmarks/learn/, their only users; one
`Cli(executable, base_args, env, timeout).run(*args)` and `error_tail(proc)` in plane/ for
reconciler.py:185-228, egress.py:174-216, benchmarks/ax/batch.py:221-262; one `safe_extract(data, dest, *,
max_bytes, max_files)` for delivery.py:134-140, runner.py:500-504, search/executor.py:658-690,
search/verify_task.py:161-215; one `ObjectURL` client (no proxy, no redirect, bounded read, backoff) for
runner.py:719-790, search/executor.py:508-541, verify_task.py:240-270; redaction.py takes `values=` and
`urls=True` so router.redact (:50), task_storage.diagnostic (:100-120) and runner.safe_url (:713) are thin.
Guards: the two dispatch stacks stay two (decision deferred to plane-on-kubernetes); credentials still travel
only in the start POST body; nothing logs a URL with a query string; run tests/test_plane_*.py,
test_search_executor*.py, test_ax_batch.py.
```