diff --git a/.kiro/breadth.toml b/.kiro/breadth.toml new file mode 100644 index 0000000..34b5455 --- /dev/null +++ b/.kiro/breadth.toml @@ -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" diff --git a/PATHFINDER-2026-10-13/00-features.md b/PATHFINDER-2026-10-13/00-features.md new file mode 100644 index 0000000..8f55fab --- /dev/null +++ b/PATHFINDER-2026-10-13/00-features.md @@ -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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md b/PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md new file mode 100644 index 0000000..aa8b4c8 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U1-scan.md @@ -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
cli.py:126] --> B[parse scan args
cli.py:226] + B --> C[_scan
cli.py:613] --> D[_run_scan
cli.py:619] --> E[plan_for_mode
stages.py:28] + E --> F[create_scan_run
run.py:18] --> G[HarnessRuntime + write_harness_config
cli.py:627] + G --> H[load_policy / load_ruleset
cli.py:634] --> I[_load_static_hints
cli.py:1037] + I --> J[preprocess_repository
cli.py:641] --> K[analyze_entry_points
cli.py:656] + K --> L[fingerprint
provenance.py:66] --> M[rank_targets + calibrate_rankings
cli.py:661] + M --> N{mode == standard
cli.py:667} + N -- standard --> O[run_hunter_pool
hunter.py:103] + N -- quick/deep --> P[quick_scan_findings
findings.py:45] + O --> Q[write_findings
cli.py:708] + P --> Q + Q --> R[verify_findings
verification.py:117] --> S[_persist_calibration_feedback
cli.py:1011] + S --> T{standard and fusion.enabled
cli.py:720} + T -- yes --> U[fuse_findings
fusion.py:191] --> V + T -- no --> V[build_score_artifact
cli.py:732] + V --> W{MAP requested
cli.py:757} + W -- quick --> AC + W -- standard/deep --> X[build_complexity_map
cli.py:758] --> Y[adjudicate overlay
cli.py:774] + Y --> Z[_check_obligations
cli.py:1388] --> AA[_run_model_layer
cli.py:1286] --> AB[_attach_tool_hunter
cli.py:962] + AB --> AR{REGRESS requested
cli.py:839} + AR -- deep, sandbox ok --> AS[run_regression
cli.py:856] --> AC + AR -- no --> AC[write_markdown_report
cli.py:886] + AC --> AD[write_sarif_report
cli.py:905] --> AE[write_manifest
cli.py:915] --> AF[scan_exit_code
reports.py:281] +``` + +## Side effects +Run dir `///`: 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). diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U10-periphery.md b/PATHFINDER-2026-10-13/01-flowcharts/U10-periphery.md new file mode 100644 index 0000000..d783384 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U10-periphery.md @@ -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 `/.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
cli.py:433] --> B[_improve
cli.py:1106] + B --> M[MemorySession.open
improve/memory.py:460] --> F[MemorySession.finish
improve/memory.py:506] + B --> R[run_improvement
improve/evolve.py:259] --> RR[run_round
improve/evolve.py:150] + RR --> S[_scan / _evaluate
improve/evolve.py:307,319] + RR --> V[EvolveValidator
improve/validator.py:69] + RR --> P[evaluate_profiles
improve/evolve.py:59] + RR --> J[write_journal
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
cli.py:1175] --> L[load_pair_catalog
pairs.py:229] + P --> E[evaluate_catalog
pairs.py:438] --> O[_evaluate_overlay_pair
pairs.py:973] + P --> PM[print_pair_metrics
pair_gate.py:52] + B[_benchmark
cli.py:1076] --> CR[create_benchmark_run
benchmark.py:185] + B --> EB[evaluate_benchmark
benchmark.py:224] + B --> W[write_benchmark_artifacts
benchmark.py:283] + R[_repos
cli.py:1199] --> RS[resolve
repos.py:172] + R --> VF[verify
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/.toml` +→ `benchmarks/fixtures/`, 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
.github/workflows/ci.yml:27-32] --> G[gate.main
gate.py:182] --> DG[detection_gate
gate.py:114] + CI --> MG[map_gate.main
map_gate.py:118] --> SS[split_sink_map_gate
map_gate.py:102] + CI --> PG[pair_gate.main
pair_gate.py:80] --> PGF[pair_gate
pair_gate.py:34] --> EC[evaluate_catalog
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
cli.py:467] --> SV[serve
mcp.py:362] --> T[_build_tools
mcp.py:238] + T --> SC[_scan
mcp.py:62] --> RS[_run_scan
cli.py:619] + T --> BM[_benchmark
mcp.py:162] --> RS + H[run_hunter_pool standard
cli.py:670] --> SK[select_skill_snippets
hunter.py:137] --> RT[route_skill_ids
skills.py:318] + CH[skill_chunks tests only
skills.py:339] --> IX[chunk_text_namespace
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`. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U2-core.md b/PATHFINDER-2026-10-13/01-flowcharts/U2-core.md new file mode 100644 index 0000000..23d69c4 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U2-core.md @@ -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
config.py:225] --> POL[load_policy
policy/verycode.py:30] --> RS[load_ruleset
ruleset/store.py:44] + RS --> CHK[assert_rules_resolve
policy/verycode.py:73] --> SARIF[ingest_sarif
mapping.py:101] --> PRE[preprocess_repository
preprocess.py:61] + PRE --> ENUM[enumerate_source_files
preprocess.py:96] + PRE --> ASH[attach_static_hints
mapping.py:116] + PRE --> EP[analyze_entry_points
mapping.py:123] --> ARH[attach_reachability_hints
mapping.py:181] + ARH --> RANK[rank_targets / heuristic_rank
rank.py:36 / rank.py:78] --> CAL[calibrate_rankings
calibration.py:161] + CAL -->|standard| POOL[run_hunter_pool
hunter.py:103] --> TASK[_run_hunter_task
hunter.py:144] --> QS + CAL -->|other modes| QS[quick_scan_findings
findings.py:45] + QS --> FM[_finding_from_match
findings.py:119] --> SEV[resolve_severity
policy/verycode.py:56] + FM --> SF[StaticFinding
findings.py:16] --> SCORE[build_score_artifact
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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U3-pre-push.md b/PATHFINDER-2026-10-13/01-flowcharts/U3-pre-push.md new file mode 100644 index 0000000..4e895b9 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U3-pre-push.md @@ -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 --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
hook/pre-push:38] --> W{docker wrapper?
hook/pre-push:10} + W -->|yes| D[docker run --network none
hook/ousast-docker:46] --> M + W -->|no| M[_main / _advisory_hook
cli.py:405-411] + M --> HM[_hook_main
cli.py:523] --> PP[_pre_push
cli.py:529] --> PU[push
runner.py:609] + RP[replay_entry --base/--head
replay_entry.py:141] --> RE[replay
runner.py:567] --> AN + PU --> B[ExecutionBudget
runner.py:632] --> RU[resolve_updates
snapshot.py:200] + RU -->|no base| LC[_last_commit_quick
runner.py:503] --> APR + RU --> AN[_analyze
runner.py:257] --> MAT[materialize head/base
snapshot.py:462] + MAT --> DIS[discovery/_discover
reuse.py:160 runner.py:118] --> CMP[compare -> ChangeContext
snapshot.py:226] + CMP --> QT[run_quick_tier
quick.py:60] --> ENG{engine mode
runner.py:392} + ENG -->|inline| HS[scan_repository head
runner.py:396] --> BS[compare_targeted_base
policy.py:731] + BS --> ADM[_admit / admit_candidates
runner.py:169 policy.py:889] + ADM --> VR[record_vetoes replay-only
runner.py:449] + ADM --> APR[admission_push_result
policy.py:1013] + ENG -->|off/background| APR + APR --> BG[_background
runner.py:526] --> BGS[background.start Popen
background.py:47] + BG --> AS[assist witness index only
assistance.py:29] --> DL[deliver_report/_save
report.py:423 report.py:158] --> EX[exit_code
report.py:104 cli.py:610] +``` + +## Side effects +Artifact JSON at `${OUSAST_ARTIFACT_DIR:-$XDG_CACHE_HOME|~/.cache}/openultrasast/push/push-.json`, atomic +`.partial-`, 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 `/engine//` (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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U4-engine.md b/PATHFINDER-2026-10-13/01-flowcharts/U4-engine.md new file mode 100644 index 0000000..034a575 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U4-engine.md @@ -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 `/-requests-*.json` (1222-1224), tries the session (1229), else `joern --script queries/.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
cli.py:1286] --> RES[resolve_cpg_backend
cpg/backend.py:1813] --> CAP[has_cpg / joern_available
cpg/capability.py:14] + CLI --> REG[regions_for + declared_sources
model/regions.py:78] + PUSH[push / _analyze
push/runner.py:609 / :257] --> BUD[ExecutionBudget
model/contracts.py:19] + PUSH --> REU[ReusingBackend.build
push/reuse.py:32] --> ART[graph_identity/load_graph/describe_graph
cpg/backend.py:447-570] + CLI --> SR[scan_repository
model/scan.py:143] + PUSH --> SR + PUSH --> BASE[compare_targeted_base
push/policy.py:731] --> SR + SR --> IMPL[_scan_repository_impl
model/scan.py:213] --> PART[build_partitions
model/partitions.py:168] --> BLD[_build
model/scan.py:1444] + BLD --> JB[JoernBackend.build/_build_impl
cpg/backend.py:419/573] --> JP[joern-parse / frontend subprocess
cpg/backend.py:642] + IMPL --> EVP[_evidence_pass
model/scan.py:1275] --> EVR[evidence_from_rows
model/evidence.py:130] + IMPL --> SCOPE[_scope_work
model/scan.py:751] --> GRP[_grouped request_params
model/scan.py:1479] --> BO[_batch_once
cpg/backend.py:1193] + EVP --> BO + BO --> SES[EngineSession opt-in
cpg/session.py:68] + BO --> JS[joern --script queries/*.sc
cpg/backend.py:1231] + IMPL --> SREG[scan_region
model/pipeline.py:124] --> VER[taint/dominance/config verdict
model/taint.py:72] --> LAD[Rung/Verdict
model/ladder.py:29] + IMPL --> RESULT[ModelScanResult
model/scan.py:662] --> SF[_finding_from_model
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). diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U5-semantic.md b/PATHFINDER-2026-10-13/01-flowcharts/U5-semantic.md new file mode 100644 index 0000000..cbff555 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U5-semantic.md @@ -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
cli.py:758] --> B[load_facts
facts.py:412] --> C[adjudicate
overlay.py:65] --> D[_flows_for
overlay.py:161] + D --> E[parse_file
ir.py:52] --> F[parse_with_cst
cst.py:65] --> G[grammar_for
extra.py:36] + F --> H[file_ir_from_tree
cst.py:86] + E --> E2[parse_python_ast fallback
ir.py:63] + H --> I[named_ranges_from_ir
functions.py:96] + H --> J[analyze_ir
taint.py:60] --> K[_analyze_function
taint.py:76] --> L[_analyze_call
taint.py:123] + D --> M[_joern_paths stub
overlay.py:192] + C --> N[_record_for_finding
overlay.py:203] --> O[write_overlay
overlay.py:126] + O --> P[finding_from_coverage
overlay.py:132] + O --> Q[filter_promoted_hotspots
prove_filter.py:13] --> R[run_regression
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
cli.py:760] --> B[build_complexity_map
map.py:37] --> C[collect_signals
signals.py:44] + C --> D[_hotspot_from_signals
map.py:62] --> E[attach_test_hints
hints.py:52] --> F[load_ledger + apply_overlay
ledger.py:37,103] + F --> G[write_complexity_map
map.py:57] + G --> H[_attach_tool_hunter
cli.py:964] --> I[run_tool_hunter
tool_hunter.py:323] + G --> J[filter_promoted_hotspots
prove_filter.py:13] --> K[select_candidates
regress/candidate.py:63] --> L[persist_verdicts
ledger.py:60] --> M[write_ledger
ledger.py:49] + N[plane hunt
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 `/overlay.json`, `/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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md b/PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md new file mode 100644 index 0000000..d1390ed --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U6-hunter-client.md @@ -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::` (615-631); `tags=[*tags, "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
cli.py:962] --> B[resolve_hunter_client
tool_hunter.py:239] --> C[run_tool_hunter
tool_hunter.py:323] + C --> D[_SYSTEM_PROMPT
tool_hunter.py:140] + C --> E[_user_message
tool_hunter.py:423] --> E2[_hotspot_prompt
tool_hunter.py:439] + E --> RF[read_file
hunter_tools.py:28] + C --> L[client.complete loop
tool_hunter.py:347-356] --> T[_dispatch_tool
tool_hunter.py:500] + T --> RF + T --> G[grep_repo
hunter_tools.py:69] --> SL[_source_lines memo
hunter_tools.py:41] + T --> FR[find_refs
hunter_tools.py:83] --> SL + T -.never enabled.-> RD[read_definition
hunter_tools.py:174] + T -.never enabled.-> EP[entry_points
hunter_tools.py:194] + T -.never enabled.-> FL[flows
hunter_tools.py:220] + T -.never enabled.-> OB[obligations
hunter_tools.py:251] + L --> FA[_final_answer
tool_hunter.py:368] --> FC + L --> FC[_findings_from_content
tool_hunter.py:557] --> FI[_finding_from_item
tool_hunter.py:593] --> SF[StaticFinding + family: tag
tool_hunter.py:615] +``` +```mermaid +flowchart TD + CP[ChatClient Protocol .complete
tool_hunter.py:163] + CA[Caller.ask cache/replay
learn/program.py:369] --> MC[MeteredClient .complete
plane/budget.py:68] + FM[FillMeter subclass
learn/fill.py:44] -.is-a.-> MC + REC[_Recording
plane/tasks/verify.py:66] --> MC + MC --> DS[DeepSeekChatClient .complete
model/endpoint.py:91] + MC --> ORH[OpenRouterHunterClient .complete
tool_hunter.py:203] + MC --> SC[ScriptedChatClient
tool_hunter.py:176] + DS --> EPC[ChatEndpoint.cost / Prices
model/endpoint.py:70,43] + DS --> OR[OpenRouterChatClient .complete_chat_raw
provider/openrouter.py:84] + ORH --> ORC[OpenRouterChatClient .complete_chat
provider/openrouter.py:69] --> OR + OR --> RT[call_with_retry
provider/openrouter.py:34] + W[search Worker._call + SpendBudget
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/.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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md b/PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md new file mode 100644 index 0000000..bc4c182 --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U7-learn-memory-tasks.md @@ -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-.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//` (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//result.json` (learn/cli.py:141-144, experiments.py:67). + +```mermaid +flowchart TD + CLI[ousast learn labels
cli.py:317] --> LL[_learn_labels
cli.py:1503] --> BL[build_labels
learn/labels.py:938] + BL --> LS[load_sources sources.toml
learn/labels.py:101] + BL --> SG[SourceGuard.check
learn/labels.py:150] + BL --> WL[write_labels labels-sha.jsonl
learn/labels.py:956] --> MB + BF[build_features static
benchmarks/learn/build_features.py:1] --> MB + REM[remember features rows
plane/tasks/remember.py:101] --> MB[_memory_build
learn/cli.py:371] + MB --> BE[build_examples
learn/examples.py:146] --> EX[excerpt / delta_diff
learn/excerpt.py:339] + BE --> ST[(put_blob excerpts + put_rows example
plane/memory.py:364)] --> PC[_program_command
learn/cli.py:274] + PC --> LE[load_examples
learn/examples.py:265] + PC --> CP[compile_program
learn/compile.py:347] + CP --> SP[compile_split / outer_folds
learn/folds.py:95] + CP --> BS[bootstrap + draw_demo_sets
learn/compile.py:166] --> DEC + CP --> PR[propose
learn/compile.py:257] + CP --> SC[score + best
learn/compile.py:296] --> DEC[Program.decide
learn/program.py:528] + DEC --> SL[function_slice
learn/slice.py:41] + DEC --> CA[Caller.ask responses cache
learn/program.py:397] + CP --> ART[(programs/pid blob
learn/compile.py:479)] + ART --> EV[evaluate
learn/evaluate.py:457] --> PD[predict
learn/evaluate.py:398] + EV --> CF[cross_fit / check
learn/calibrate.py:148] + EV --> NP[nested_points
learn/evaluate.py:113] + ART --> XR[experiments.run
learn/experiments.py:899] --> OR[(arm_outcome rows
learn/experiments.py:975)] --> XA[analyse
learn/experiments.py:815] + XA --> PB[paired_bootstrap
learn/experiments.py:673] + XA --> MN[exact_mcnemar
learn/experiments.py:722] +``` +```mermaid +flowchart TD + OS[open_store
plane/memory.py:929] --> RR[reconciler.results_root
plane/reconciler.py:64] + OS --> FS[FileStore
plane/memory.py:493] + OS --> S3[S3Store + verify_bucket
plane/memory.py:612] --> S3C[S3Client boto3
plane/memory.py:762] + MS[MemoryStore put_rows/rows/put_blob/get_blob
plane/memory.py:267] --> FS + MS --> S3 + RF[repo_facts facts.json
plane/tasks/repo_facts.py:264] --> VE[verify units.jsonl
plane/tasks/verify.py:247] --> AG[agree agreed.json
plane/tasks/agree.py:153] --> FE + AL[alerts alerts.jsonl
plane/tasks/alerts.py:117] --> FE + RO[roles roles.json
plane/tasks/roles.py:141] --> FE[features features.jsonl
plane/tasks/features.py:114] + RF --> FE + FE --> RE[remember memory.jsonl
plane/tasks/remember.py:267] --> ING[ingest / ingest_rows
plane/memory.py:968] --> MS + SEED[seed
plane/memory.py:984] --> LR[reconciler.load_run/mark_done
plane/reconciler.py:102] + SEED --> WS[loop.write_snapshot
plane/tasks/loop.py:77] --> GU[improve.memory Guard
plane/tasks/loop.py:67] + FE -.imports.-> LF[learn.features parts
learn/features.py:53] + FE -.imports.-> LRO[learn.roles infer/from_model_roles
learn/roles.py:426] + LF -.imports.-> RF + LRO -.imports.-> RF +``` + +## Side effects +Chat calls through `Caller.ask` (program.py:397), cached as `responses/` (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//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//.jsonl` (389), `facts/.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 `//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.` +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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U8-plane-executor.md b/PATHFINDER-2026-10-13/01-flowcharts/U8-plane-executor.md new file mode 100644 index 0000000..1518d1c --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U8-plane-executor.md @@ -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 ` +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 `//task.yaml` (348); `delivery.body` (delivery.py:107) presigns PUT `runs///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: /` 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.` (`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 `/.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
cli.py:1530] --> PROF[load_profile
profile.py:203] + CLI --> SEED[memory.seed
memory.py:984] --> MD[mark_done
reconciler.py:280] + CLI --> RUN[reconciler.run
reconciler.py:384] + RUN --> STORE[open_store
memory.py:929] + RUN --> LR[load_run
reconciler.py:102] --> LM[load_manifests
manifests.py:593] + RUN --> DEL[Delivery
delivery.py:78] + RUN --> OR[open_router atenet-router
router.py:147] + RUN --> EX[_execute
reconciler.py:335] + EX --> RT[render_task
reconciler.py:145] + EX --> BODY[Delivery.body presign
delivery.py:107] + EX --> CRED[credentials
router.py:58] + EX --> POL[policy_for
egress.py:87] + EX --> AXA[Ax.apply
reconciler.py:197] --> EGA[Egress.apply kubectl-ate
egress.py:194] + EX --> AW[_await resume/poll
reconciler.py:292] --> ST[Router.start_task POST
router.py:100] --> RM[runner.main
runner.py:989] + RM --> GATE[StartGate.offer
runner.py:303] --> MAT[materialise
runner.py:546] --> FI[fetch_inputs
runner.py:785] --> RC[run_command tasks.X
runner.py:671] --> FIN[_finish / PUT tar
runner.py:887] + FIN --> DLV[Delivery.delivered
delivery.py:120] --> COL[Delivery.collect
delivery.py:125] --> OUT[outcome_of
reconciler.py:258] --> STATE[_State.set + save_state
reconciler.py:237] + STATE --> ING[remember_run ingest
tasks/remember.py:229] --> STAT[status/attribution
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//{state.json, /output.tar, /, /done.json}` +(delivery.py:11-16), presigned URLs (96-105), seed/ingest rows. Local `$OUSAST_RESULTS/plane//` (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. diff --git a/PATHFINDER-2026-10-13/01-flowcharts/U9-search-sandbox-regress.md b/PATHFINDER-2026-10-13/01-flowcharts/U9-search-sandbox-regress.md new file mode 100644 index 0000000..4d8655f --- /dev/null +++ b/PATHFINDER-2026-10-13/01-flowcharts/U9-search-sandbox-regress.md @@ -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
pilot_select.py:302] --> B[pilot_run.main
pilot_run.py:411] --> C[Pilot.__init__ Board+Worker
pilot_run.py:79] --> D[Pilot.run
pilot_run.py:376] + D --> E[Coordinator.run
coordinator.py:210] --> F[Coordinator._call / SearchTask
coordinator.py:125] + F --> G[Board.commit
board.py:51] + F --> H[Pilot.dispatch
pilot_run.py:290] + H --> I[SearchExecutorTask AX pod
batch.py:675] --> J[executor.serve/InProcessExecutor
executor.py:622] + H --> K[Worker.explore
worker.py:351] --> L[Worker._call LLM
worker.py:218] + K --> M[write_demo validate_demo
demo.py:350] + H --> N[Pilot.verification
pilot_run.py:230] --> O[prepare_verification
executor.py:390] + N --> P[verify
verify.py:648] --> Q[_side x3 runs
verify.py:280] + Q --> R[_sandbox.run bwrap
_sandbox.py:105] + Q --> S[oracle_for/Canary
oracles.py:307] + P --> T[demonstrated
verify.py:698] --> U[_end goal_met
coordinator.py:268] --> V[record jsonl
pilot_run.py:540] +``` +```mermaid +flowchart TD + A[cli scan deep REGRESS
cli.py:839] --> B[SandboxProbe.available
sandbox/probe.py:22] --> C[resolve_sandbox_runner
sandbox/runner.py:59] --> D[run_regression
regress/candidate.py:177] + D --> E[select_candidates
regress/candidate.py:63] + D --> F[RegressionRunner.run_recipe
regress/candidate.py:126] + F --> G[recipe_for
regress/recipes.py:16] + F --> H[check_snippet_safety
regress/safety.py:63] + F --> I[DockerCliRunner.run
sandbox/runner.py:72] --> J[verdict_from_result
regress/verdict.py:64] --> K[write_verdicts
regress/candidate.py:222] --> L[rank_obligations on TRIGGERABLE
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//…` (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. diff --git a/PATHFINDER-2026-10-13/02-duplication-report.md b/PATHFINDER-2026-10-13/02-duplication-report.md new file mode 100644 index 0000000..ae4e9fa --- /dev/null +++ b/PATHFINDER-2026-10-13/02-duplication-report.md @@ -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 +`` (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 `` (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 +(``) and scorer ranges (``) 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///`) 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. diff --git a/PATHFINDER-2026-10-13/03-unified-proposal.md b/PATHFINDER-2026-10-13/03-unified-proposal.md new file mode 100644 index 0000000..8f893c1 --- /dev/null +++ b/PATHFINDER-2026-10-13/03-unified-proposal.md @@ -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 ``), plane/tasks/repo_facts.py +(regex only, sentinel ``, 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-.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
cli.py:619] --> QP + PUSH[pre-push runner
push/runner.py:609] --> QP + GATE[gate.py / pairs / alerts] --> QP + end + QP[quick_pipeline
findings.py (S3)] --> FACTS + QP --> ENGINE + FACTS[source.facts_for
source.py (S1)] --> ANN[annotation model
hunter-learning-loop R3] + ENGINE[scan_repository
model/scan.py:143] --> RUNG + ANN --> HUNT[run_tool_hunter
tool_hunter.py:323] + HUNT --> CLIENT[ChatAdapter + Meter + Caller
model/endpoint.py, plane/budget.py, learn/program.py (S2)] + CLIENT --> CLS[classify
learn/program.py:473] + CLS --> RUNG[evidence ladder Rung
model/ladder.py:29] + RUNG --> ADMIT[admit_candidates
push/policy.py:889] + RUNG --> PROOF[search verify
search/verify.py:648] + PROOF --> EXEC[ExecutionOutcome -> EXECUTION_CONFIRMED
sandbox (S4)] + EXEC --> ADMIT + ADMIT --> REPORT[deliver_report
push/report.py:423] + ENGINE --> ASSEMBLE[write once, score last
cli.py (S6)] + HUNT --> ASSEMBLE + ASSEMBLE --> LEDGER[CalibrationLedger
(S6)] + CLS --> STORE[(MemoryStore leaf
plane/memory.py (S7))] + LEARN[learn compile / evaluate / experiment
learn/cli.py] --> CLIENT + LEARN --> STORE + FACTS --> LEARN + BUDGET[ExecutionBudget + DeadlineExpired
contracts.py (S5)] -.-> ENGINE + BUDGET -.-> PUSH +``` diff --git a/PATHFINDER-2026-10-13/04-handoff-prompts.md b/PATHFINDER-2026-10-13/04-handoff-prompts.md new file mode 100644 index 0000000..7c93a75 --- /dev/null +++ b/PATHFINDER-2026-10-13/04-handoff-prompts.md @@ -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 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 +``, 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-.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. +```