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
@@ -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.