mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
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:
co-authored by
Claude Fable 5.1
parent
e979fb0d95
commit
d2010d0693
@@ -0,0 +1,251 @@
|
||||
# Breadth ledger: every spec and every top-level capability, with a status and the deletion that closes it.
|
||||
# Draft 2026-10-13 from the 22 spec.json files and the Pathfinder feature inventory (PATHFINDER-2026-10-13/).
|
||||
# Nothing here changes code; it records decisions so a gate can hold the line.
|
||||
#
|
||||
# status values
|
||||
# active = on one of the four lines in .kiro/steering/overview.md (2026-10-13); work may land
|
||||
# done = implemented and closed; the code it produced is owned by a capability below
|
||||
# parked = no work lands; scaffolding (stubs, unused flags, dead task modules) is removed; spec stays as history
|
||||
# retiring = the "closes" deletion is scheduled; the spec directory goes with it
|
||||
# review = Pathfinder 2026-10-13 decides between parked and retiring
|
||||
#
|
||||
# open_fronts (the weekly number) = specs with status active or review
|
||||
# + capabilities with status review or retiring whose "closes" has not run.
|
||||
#
|
||||
# gate (to implement after the map is approved):
|
||||
# fail when a spec.json reaches phase "tasks-generated" without an entry here;
|
||||
# fail when a new directory under src/openultrasast/ lands without a [capability.*] entry;
|
||||
# fail when an entry has status "retiring" for more than 14 days with its "closes" unexecuted.
|
||||
|
||||
[meta]
|
||||
drafted = "2026-10-13"
|
||||
spec_count = 22
|
||||
open_fronts = 0 # filled by the gate script once it exists
|
||||
|
||||
# ---------------------------------------------------------------- specs
|
||||
|
||||
[spec.hunter-learning-loop]
|
||||
status = "active"
|
||||
line = "hunter-learning-loop"
|
||||
phase = "requirements-generated"
|
||||
owner = ["src/openultrasast/learn", "src/openultrasast/tool_hunter.py", "src/openultrasast/hunter_tools.py"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.pre-push-safety-net]
|
||||
status = "active"
|
||||
line = "pre-push-safety-net"
|
||||
phase = "implementation (77 of 87 tasks)"
|
||||
owner = ["src/openultrasast/push"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.search-with-proof]
|
||||
status = "active"
|
||||
line = "search-with-proof"
|
||||
phase = "tasks-generated (11 of 24 done)"
|
||||
owner = ["src/openultrasast/search", "src/openultrasast/sandbox"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.proof-arm-opencode]
|
||||
status = "active"
|
||||
line = "search-with-proof"
|
||||
phase = "brief"
|
||||
owner = ["src/openultrasast/search"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.engine-change-scoped-analysis]
|
||||
status = "active"
|
||||
line = "engine-change-scoped-analysis"
|
||||
phase = "brief"
|
||||
owner = ["src/openultrasast/cpg", "src/openultrasast/model", "src/openultrasast/semantic"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.valid-detection-benchmark]
|
||||
status = "active"
|
||||
line = "hunter-learning-loop" # the benchmark every line is measured on
|
||||
phase = "tasks (18 of 24 done)"
|
||||
owner = ["benchmarks/valid", "benchmarks/unseen/replay.py", "benchmarks/unseen/score.py"]
|
||||
closes = "n/a (active)"
|
||||
|
||||
[spec.learned-decision-engine]
|
||||
status = "parked"
|
||||
phase = "implementation (20 of 47 tasks)"
|
||||
owner = ["src/openultrasast/learn"]
|
||||
note = "the classifier it built is the second stage of the hunter arm and stays; new learn work goes through hunter-learning-loop, not here"
|
||||
closes = "mark remaining 27 tasks superseded by hunter-learning-loop; no code deletion"
|
||||
|
||||
[spec.plane-on-kubernetes]
|
||||
status = "parked"
|
||||
phase = "implementation (14 of 43 tasks)"
|
||||
owner = ["src/openultrasast/plane"]
|
||||
note = "kube move paused after groups 1-2 (2026-10-02); the ax lane is blocked on the gVisor checkpoint path (operator-owned)"
|
||||
closes = "when the ax lane is restored: finish; otherwise delete plane/runner.py container path and ops/k8s"
|
||||
|
||||
[spec.contributor-scan]
|
||||
status = "parked"
|
||||
phase = "implementation (27 of 51 tasks), paused since 2026-09-23"
|
||||
owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
|
||||
closes = "fold the open census/family-context tasks into engine-change-scoped-analysis; close this spec"
|
||||
|
||||
[spec.mcp-shift-left-agent]
|
||||
status = "parked"
|
||||
phase = "brief"
|
||||
owner = ["src/openultrasast/mcp.py", "src/openultrasast/skills.py", "src/openultrasast/index.py"]
|
||||
note = "follow-up to the pre-push hook; waits for a hook that catches something"
|
||||
closes = "if not started by 2026-12-01: delete mcp.py, skills.py, index.py and their tests"
|
||||
|
||||
[spec.authorization-obligations]
|
||||
status = "review"
|
||||
phase = "tasks-generated (0 of 24)"
|
||||
owner = ["src/openultrasast/semantic/obligations"]
|
||||
note = "overview.md calls it superseded in practice by the dominance arbiter; obligations code exists and is called from scan (cli.py:1388)"
|
||||
closes = "Pathfinder U5 decides: keep obligations as an annotation producer for the hunter, or delete semantic/obligations and its tests"
|
||||
|
||||
[spec.finding-feedback-loop]
|
||||
status = "retiring"
|
||||
phase = "design, dormant since 2026-09-09"
|
||||
owner = []
|
||||
closes = "delete .kiro/specs/finding-feedback-loop (no code was written)"
|
||||
|
||||
[spec.reachability-flow-model]
|
||||
status = "retiring"
|
||||
phase = "tasks-generated (0 of 23), dormant"
|
||||
owner = []
|
||||
note = "largely delivered by adopting Joern (overview.md)"
|
||||
closes = "delete .kiro/specs/reachability-flow-model"
|
||||
|
||||
[spec.flow-aware-ranking]
|
||||
status = "retiring"
|
||||
phase = "brief"
|
||||
owner = []
|
||||
closes = "delete .kiro/specs/flow-aware-ranking; keep the one sentence it owns in safety-net.md"
|
||||
|
||||
[spec.zero-with-a-reason]
|
||||
status = "retiring"
|
||||
phase = "brief"
|
||||
owner = []
|
||||
note = "its idea (coverage gaps are reported, never a clean verdict) is already a product principle in safety-net.md"
|
||||
closes = "delete .kiro/specs/zero-with-a-reason"
|
||||
|
||||
[spec.ir-consolidation]
|
||||
status = "retiring"
|
||||
phase = "brief"
|
||||
owner = []
|
||||
closes = "delete .kiro/specs/ir-consolidation"
|
||||
|
||||
[spec.ai-service-plane]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/plane"]
|
||||
|
||||
[spec.model-grounded-detection]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/model", "src/openultrasast/cpg"]
|
||||
|
||||
[spec.pair-corpus-honesty]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/pairs.py", "benchmarks/pairs"]
|
||||
|
||||
[spec.corpus-seeded-mechanisms]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/learn"]
|
||||
|
||||
[spec.propose-adjudicate-prove]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/semantic", "src/openultrasast/ruleset"]
|
||||
|
||||
[spec.tree-sitter-overlay-extra]
|
||||
status = "done"
|
||||
owner = ["src/openultrasast/semantic"]
|
||||
|
||||
# ---------------------------------------------------------------- capabilities (top-level code)
|
||||
|
||||
[capability.push]
|
||||
status = "active"
|
||||
spec = "pre-push-safety-net"
|
||||
lines = 5074
|
||||
|
||||
[capability.engine]
|
||||
status = "active"
|
||||
spec = "engine-change-scoped-analysis"
|
||||
modules = ["cpg", "model (minus endpoint.py)"]
|
||||
lines = 7764
|
||||
|
||||
[capability.semantic]
|
||||
status = "active"
|
||||
spec = "engine-change-scoped-analysis"
|
||||
modules = ["semantic", "complexity"]
|
||||
lines = 4301
|
||||
note = "complexity/ hotspots feed the hunter; obligations under review (see spec.authorization-obligations)"
|
||||
|
||||
[capability.llm-hunter-and-client]
|
||||
status = "active"
|
||||
spec = "hunter-learning-loop"
|
||||
modules = ["tool_hunter.py", "hunter_tools.py", "provider/openrouter.py", "model/endpoint.py", "plane/budget.py"]
|
||||
lines = 1700
|
||||
note = "Pathfinder U6 counts the chat-client classes; expected outcome: one client protocol, one metering path"
|
||||
|
||||
[capability.learn]
|
||||
status = "active"
|
||||
spec = "hunter-learning-loop"
|
||||
modules = ["learn", "plane/memory.py", "plane/tasks (fact producers)"]
|
||||
lines = 11350
|
||||
|
||||
[capability.search-with-proof]
|
||||
status = "active"
|
||||
spec = "search-with-proof"
|
||||
modules = ["search", "sandbox"]
|
||||
lines = 4803
|
||||
|
||||
[capability.core]
|
||||
status = "active"
|
||||
modules = ["cli.py", "config.py", "contracts.py", "preprocess.py", "findings.py", "rank.py", "mapping.py", "ruleset", "policy", "reports.py", "stages.py", "run.py", "provenance.py", "profiling.py"]
|
||||
note = "hunter.py is the regex quick-rule pool and should be renamed; harness.py processor machinery is unreachable (U1)"
|
||||
|
||||
[capability.pairs-and-benchmarks]
|
||||
status = "active"
|
||||
spec = "valid-detection-benchmark"
|
||||
modules = ["pairs.py", "benchmark.py", "repos.py"]
|
||||
note = "pairs.py label functions feed learn/labels.py; the `ousast pairs` CLI surface is under review (U10)"
|
||||
|
||||
[capability.plane-executor]
|
||||
status = "review"
|
||||
spec = "plane-on-kubernetes"
|
||||
modules = ["plane (reconciler, runner, generate, manifests, harvest, egress, delivery, router, doctor, profile, engine_alerts, workspaces)"]
|
||||
lines = 4885
|
||||
closes = "Pathfinder U8 lists which modules only the retired HarnessX loop used (engine_alerts, harvest, workspaces, profile are the suspects); delete those"
|
||||
|
||||
[capability.regress]
|
||||
status = "review"
|
||||
modules = ["regress"]
|
||||
lines = 549
|
||||
closes = "Pathfinder U9: if search/ covers the triggerable verdict, delete regress/ and the deep-mode REGRESS branch in cli.py:839-880"
|
||||
|
||||
[capability.improve]
|
||||
status = "retiring"
|
||||
modules = ["improve"]
|
||||
lines = 1175
|
||||
note = "HarnessX-era improve lever; no active spec names it"
|
||||
closes = "delete improve/, `ousast improve` (cli.py:237, 433, 1106), plane/tasks/loop.py lazy imports, tests/test_cli_improve.py, tests/test_improve_*.py"
|
||||
|
||||
[capability.scan-extras]
|
||||
status = "review"
|
||||
modules = ["fusion.py", "verification.py", "calibration.py", "scoring", "harness.py (processors)"]
|
||||
closes = "U1 found fusion decisions never reach findings or score, verification runs before MAP, processors unreachable; decide keep-and-fix or delete per module"
|
||||
|
||||
[capability.mcp-and-skills]
|
||||
status = "parked"
|
||||
spec = "mcp-shift-left-agent"
|
||||
modules = ["mcp.py", "skills.py", "index.py"]
|
||||
lines = 830
|
||||
closes = "see spec.mcp-shift-left-agent"
|
||||
|
||||
[capability.gates]
|
||||
status = "review"
|
||||
modules = ["gate.py", "map_gate.py", "pair_gate.py"]
|
||||
lines = 428
|
||||
closes = "U10: keep gate.py (CI + plane loop); delete map_gate.py and pair_gate.py if CI does not need them"
|
||||
|
||||
[capability.push-evaluation-instruments]
|
||||
status = "review"
|
||||
modules = ["push_inputs.py", "push_scoring.py"]
|
||||
lines = 462
|
||||
closes = "U3: move under benchmarks/push or delete if only tests use them"
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
```
|
||||
Reference in New Issue
Block a user