Files
Open_Ultra_SAST/src/openultrasast/cli.py
T

1536 lines
70 KiB
Python
Raw Normal View History

2026-06-17 21:13:51 +02:00
from __future__ import annotations
import argparse
import json
import tempfile
from collections.abc import Callable, Sequence
from dataclasses import dataclass, replace
2026-06-17 21:13:51 +02:00
from pathlib import Path
from time import perf_counter
from typing import TYPE_CHECKING, Any
2026-06-17 21:13:51 +02:00
from . import tool_hunter
from .benchmark import (
create_benchmark_run,
evaluate_benchmark,
load_baseline_findings,
load_benchmark_manifest,
load_findings,
resolve_benchmark_source,
write_benchmark_artifacts,
)
from .calibration import (
FalsePositiveLearning,
calibrate_rankings,
fp_reachability_overrides,
learnings_from_verifications,
load_false_positive_learnings,
merge_false_positive_learnings,
write_false_positive_learnings,
write_ranking_calibrations,
)
from .complexity import map as complexity_map
from .complexity.ledger import persist_verdicts
from .complexity.map import Hotspot
from .config import ObligationsConfig, load_config, load_dotenv
from .findings import StaticFinding, quick_scan_findings, write_findings
from .fusion import FusionDecision, fuse_findings_dispatch
from .gate import FALSE_POSITIVE_CEILING, RECALL_FLOOR
from .harness import HarnessRuntime, HarnessTraceWriter, write_harness_config
from .harness_ext import has_harnessx
2026-06-18 10:59:25 +02:00
from .hunter import run_hunter_pool, write_hunter_trajectories
from .hunter_harness import HxScanOrchestrator
from .improve import RoundOutcome, run_improvement
2026-06-17 21:13:51 +02:00
from .index import build_code_chunks
from .learning.families import FamilyTaxonomy
from .mapping import analyze_entry_points, attach_reachability_hints, ingest_sarif, write_entry_points, write_static_hints
from .pair_gate import print_pair_metrics
from .pairs import (
DEFAULT_CATALOG,
SLICE_NAMES,
PairCase,
evaluate_catalog,
load_pair_catalog,
make_hunter_scan,
result_payload,
select_profile,
select_slice,
select_split,
select_vendored,
)
from .policy import assert_rules_resolve, load_policy
from .preprocess import FileTarget, preprocess_repository, write_preprocess_artifact
from .provenance import fingerprint
from .provider.openrouter import OpenRouterEmbeddingClient, OpenRouterError
from .rank import rank_obligations, rank_targets, write_rankings
from .regress import TRIGGERABLE, CandidateVerdict, run_regression, write_verdicts
from .regress.candidate import hotspot_from_finding
2026-06-18 10:16:25 +02:00
from .reports import scan_exit_code, write_manifest, write_markdown_report, write_sarif_report
from .ruleset import DEFAULT_RULESET_DIR, load_ruleset
from .run import ScanRun, create_scan_run
from .sandbox import resolve_sandbox_probe, resolve_sandbox_runner
from .scoring import build_score_artifact
from .semantic import (
MechanismStore,
OverlayRecord,
adjudicate,
append_mechanism,
filter_promoted_hotspots,
finding_from_coverage,
load_facts,
order_promotions,
write_overlay,
)
from .semantic.facts import FactLoadError, SemanticFacts
from .stages import Stage, plan_for_mode, record_completed, record_skip, skip_as_degradation, stages_payload
from .verification import VerificationResult, verify_findings, write_verification_results
from .verify_judge import verify_findings_dispatch
CALIBRATION_DIR = ".openultrasast/calibration"
2026-06-17 21:13:51 +02:00
@dataclass(frozen=True)
class ScanOutcome:
scan_id: str
run_dir: Path
file_target_count: int
ranked_target_count: int
finding_count: int
calibrations_applied: int
exit_code: int
if TYPE_CHECKING:
from .learning.proposer import Proposer
2026-06-17 21:13:51 +02:00
def main(argv: list[str] | None = None) -> int:
load_dotenv()
2026-06-17 21:13:51 +02:00
parser = argparse.ArgumentParser(prog="ousast")
subparsers = parser.add_subparsers(dest="command", required=True)
scan = subparsers.add_parser("scan", help="scan a local repository")
scan.add_argument("path", type=Path)
scan.add_argument("--mode", choices=("quick", "standard", "deep"), default="quick")
scan.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
scan.add_argument("--fail-on", choices=("never", "findings", "verified", "worth-fixing"), default="never")
2026-06-17 21:13:51 +02:00
index = subparsers.add_parser("index", help="chunk a local repository for embedding index construction")
index.add_argument("path", type=Path)
index.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
index.add_argument("--chunk-lines", type=int, default=80)
benchmark = subparsers.add_parser("benchmark", help="run a benchmark manifest and write scoreboard artifacts")
benchmark.add_argument("manifest", type=Path)
benchmark.add_argument("--mode", choices=("quick", "standard", "deep"), default="quick")
benchmark.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
improve = subparsers.add_parser(
"improve",
help="run the bounded self-improvement loop against a benchmark manifest and update the loop-owned ruleset ledger",
)
improve.add_argument("manifest", type=Path)
improve.add_argument("--max-rounds", type=int, default=5)
improve.add_argument("--recall-floor", type=float, default=RECALL_FLOOR)
improve.add_argument("--fp-ceiling", type=float, default=FALSE_POSITIVE_CEILING)
improve.add_argument(
"--ledger", type=Path, default=None, help="ruleset ledger path (default: <target>/.openultrasast/calibration/rule_policy.json)"
)
improve.add_argument("--journal", type=Path, default=None, help="improvement journal path (default: alongside the ledger)")
improve.add_argument(
"--ruleset-dir", type=Path, default=DEFAULT_RULESET_DIR, help="ruleset directory to improve (default: the bundled ruleset)"
)
improve.add_argument("--dry-run", action="store_true", help="run rounds against a throwaway ledger; never touch the target's ledger")
improve.add_argument(
"--pair-catalog",
type=Path,
default=DEFAULT_CATALOG,
help="pair catalog whose holdout split gates every round per provenance profile",
)
improve.add_argument(
"--no-pair-gate", action="store_true", help="skip the per-profile holdout clause (faster; not for accepted ledgers)"
)
improve.add_argument(
"--profile-tolerance", type=float, default=0.0, help="allowed per-profile drop in pair_correct/Youden before rejecting"
)
improve.add_argument("--min-holdout-pairs", type=int, default=5, help="profiles with fewer holdout pairs are reported, not gated")
improve.add_argument(
"--mechanism-candidates",
type=Path,
default=None,
help=(
"exporter candidates the mechanisms lever may admit (default: <target>/.openultrasast/calibration/"
"mechanism-candidates.jsonl, then ./.openultrasast/calibration/mechanism-candidates.jsonl)"
),
)
improve.add_argument(
"--mechanism-store",
type=Path,
default=None,
help="scan-time mechanisms.jsonl the lever writes (default: <target>/.openultrasast/calibration/mechanisms.jsonl)",
)
improve.add_argument("--no-mechanisms", action="store_true", help="disable the mechanisms lever for this run")
pairs = subparsers.add_parser(
"pairs",
help="scan isolated vuln-vs-fixed pairs (TP on vuln, silent on fix) and emit improve-loop signals",
)
pairs.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG)
pairs.add_argument("--slice", choices=SLICE_NAMES, default="all")
pairs.add_argument(
"--profile", choices=("all", "human", "agent", "mixed", "synthetic"), default="all", help="filter pairs by provenance profile"
)
pairs.add_argument("--split", choices=("all", "train", "holdout"), default="all", help="filter pairs by declared split")
pairs.add_argument("--hunter", action="store_true", help="also score the LLM tool hunter on overlay slices (needs a hunter model)")
pairs.add_argument("--hunter-model", default=None, help="model id for --hunter (default: [models].hunter from openultrasast.toml)")
pairs.add_argument("--pointers", action="store_true", help="allow network for non-vendored pointer pairs this run (nightly; CI never)")
pairs.add_argument(
"--loo", action="store_true", help="leave-one-out: score each pair against a store seeded from the other trusted pairs"
)
pairs.add_argument("--loo-out", type=Path, default=None, help="where to write loo.json (default: reports/loo.json)")
pairs.add_argument("--json", action="store_true", help="print the pair scoreboard as JSON")
pairs.add_argument(
"--k-runs", type=int, default=None, help="runs per pair on the hunter path; never fewer than three, and three by default"
)
subparsers.add_parser("mcp", help="run the narrow MCP server over stdio for OpenCode integration")
mechanisms = subparsers.add_parser("mechanisms", help="maintainer: mechanism memory seeded from trusted pairs")
mechanisms_sub = mechanisms.add_subparsers(dest="mechanisms_command", required=True)
export = mechanisms_sub.add_parser("export", help="derive mechanism records from seeded/reviewed pairs (offline)")
export.add_argument("--slice", choices=SLICE_NAMES, default="all")
export.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG)
export.add_argument(
"--store",
type=Path,
default=Path(CALIBRATION_DIR) / "mechanism-candidates.jsonl",
help="append-only candidates file the improve lever admits from (the scan reads mechanisms.jsonl next to it; never written here)",
)
export.add_argument("--json", action="store_true")
learning = subparsers.add_parser("learning", help="maintainer: the classified detector set and its rounds")
learning_sub = learning.add_subparsers(dest="learning_command", required=True)
for name, help_text in (
("classify", "classify every labeled pair and report the classifier against itself"),
("score", "score a catalog by family, with the denominator every number was computed over"),
("baseline", "round zero: clone one detector into every family and measure each against itself"),
("candidates", "offline: what a candidate-driven detector could find, per family and per slice"),
("round", "one evolve round: propose one change for one family and let the evidence decide"),
("publish", "regenerate every published number from the artifacts that produced it"),
):
command = learning_sub.add_parser(name, help=help_text)
command.add_argument("--out", type=Path, default=Path(".openultrasast/learning"), help="where artifacts are read and written")
if name != "publish":
command.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG)
command.add_argument("--slice", choices=SLICE_NAMES, default="all")
command.add_argument("--json", action="store_true")
if name == "baseline":
command.add_argument(
"--resume",
action="store_true",
help="keep the pairs a previous run of this baseline already measured and paid for",
)
if name == "candidates":
command.add_argument("--generator", choices=("ir", "ruleset"), default="ir", help="which pass enumerates the candidate sites")
if name in {"baseline", "round", "score"}:
command.add_argument("--k-runs", type=int, default=None, help="runs per pair; never fewer than three")
command.add_argument("--model", default=None, help="detector model; artifacts are keyed by it")
command.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
if name == "round":
command.add_argument("--family", required=True)
command.add_argument("--cost-cap-usd", type=float, default=None)
command.add_argument(
"--proposals",
type=Path,
default=None,
help="a JSON list of proposals to try in order; without it the round asks the meta-agent",
)
if name == "publish":
command.add_argument("--measurements", type=Path, default=Path("benchmarks/measurements"))
command.add_argument("--roadmap", type=Path, default=Path(".kiro/steering/roadmap.md"))
2026-06-17 21:13:51 +02:00
args = parser.parse_args(argv)
if args.command == "scan":
2026-06-18 10:16:25 +02:00
return _scan(args.path, args.config, args.mode, args.fail_on)
2026-06-17 21:13:51 +02:00
if args.command == "index":
return _index(args.path, args.config, args.chunk_lines)
if args.command == "benchmark":
return _benchmark(args.manifest, args.config, args.mode)
if args.command == "improve":
return _improve(
args.manifest,
max_rounds=args.max_rounds,
recall_floor=args.recall_floor,
fp_ceiling=args.fp_ceiling,
ledger=args.ledger,
journal=args.journal,
ruleset_dir=args.ruleset_dir,
dry_run=args.dry_run,
pair_catalog=None if args.no_pair_gate else args.pair_catalog,
profile_tolerance=args.profile_tolerance,
min_holdout_pairs=args.min_holdout_pairs,
mechanism_candidates=args.mechanism_candidates,
mechanism_store=args.mechanism_store,
mechanisms_enabled=not args.no_mechanisms,
)
if args.command == "pairs":
return _pairs(
args.catalog,
args.slice,
json_out=args.json,
profile=args.profile,
split=args.split,
hunter=args.hunter,
hunter_model=args.hunter_model,
pointers=args.pointers,
loo=args.loo,
loo_out=args.loo_out,
k_runs=args.k_runs,
)
if args.command == "mechanisms":
return _mechanisms_export(args.catalog, args.slice, args.store, json_out=args.json)
if args.command == "learning":
return _learning(args)
if args.command == "mcp":
from .mcp import serve # lazy: keeps the import cycle (mcp -> cli) one-directional
return serve()
2026-06-17 21:13:51 +02:00
return 2
2026-06-18 10:16:25 +02:00
def _scan(path: Path, config_path: Path, mode: str, fail_on: str) -> int:
outcome = _run_scan(path, config_path, mode, fail_on)
_print_scan_outcome(outcome)
return outcome.exit_code
def _run_scan(path: Path, config_path: Path, mode: str, fail_on: str) -> ScanOutcome:
2026-06-17 21:13:51 +02:00
if not path.exists() or not path.is_dir():
raise SystemExit(f"scan path is not a directory: {path}")
plan = plan_for_mode(mode)
2026-06-17 21:13:51 +02:00
config = load_config(config_path if config_path.exists() else None)
# Capability gates for the optional HarnessX agentic plane. When the extra is
# absent (default/CI), both stay False and the deterministic path runs unchanged.
hunter_model = config.models.hunter
verifier_model = config.models.verifier
harnessx_present = has_harnessx()
hx_hunter = mode == "standard" and bool(hunter_model) and harnessx_present
hx_verify = mode == "standard" and bool(verifier_model) and harnessx_present
2026-06-17 21:13:51 +02:00
run = create_scan_run(path, config)
runtime = HarnessRuntime(
scan_id=run.scan_id,
config=config,
trace_writer=HarnessTraceWriter(run.root / "trace" / "events.jsonl", redact=config.hardening.redact_secrets),
)
write_harness_config(config=config, processors=[], contract_mode="strict", path=run.root / "harness.json")
runtime.start(mode=mode, target=run.target)
policy = runtime.run_stage("policy_load", load_policy)
ledger_path = run.target / CALIBRATION_DIR / "false_positive_learnings.json"
rule_ledger_path = run.target / CALIBRATION_DIR / "rule_policy.json"
ruleset = runtime.run_stage("ruleset_load", lambda: load_ruleset(DEFAULT_RULESET_DIR, rule_ledger_path))
runtime.run_stage("policy_check", lambda: assert_rules_resolve(ruleset, policy))
static_hints = runtime.run_stage("static_mapping", lambda: _load_static_hints(config.static_analysis.sarif_paths))
write_static_hints(static_hints, run.root / "mapping" / "static_hints.json")
snapshot, targets = runtime.run_stage(
"preprocess",
lambda: preprocess_repository(run.target, run.root / "preprocess" / "file_targets.json", static_hints),
)
entry_points = runtime.run_stage("entry_point_mapping", lambda: analyze_entry_points(run.target, targets))
write_entry_points(entry_points, run.root / "mapping" / "entry_points.json")
targets = attach_reachability_hints(targets, entry_points)
write_preprocess_artifact(snapshot, targets, run.root / "preprocess" / "file_targets.json")
provenance = runtime.run_stage("provenance", lambda: fingerprint(run.target))
rankings = runtime.run_stage("rank", lambda: rank_targets(targets))
prior_learnings = load_false_positive_learnings(ledger_path)
rankings, calibrations = runtime.run_stage("calibrate", lambda: calibrate_rankings(rankings, prior_learnings))
applied_calibrations = [calibration for calibration in calibrations if calibration.applied_learning_ids]
2026-06-17 21:13:51 +02:00
write_rankings(rankings, run.root / "rank" / "ranking.json")
write_ranking_calibrations(calibrations, run.root / "calibration" / "applied_calibrations.json")
2026-06-18 10:59:25 +02:00
if mode == "standard":
if hx_hunter and hunter_model:
orchestrator = HxScanOrchestrator(
provider_model=hunter_model,
provider=config.harnessx.provider,
max_cost_usd=config.harnessx.max_cost_usd,
token_threshold=config.harnessx.token_threshold,
)
hunter_result = runtime.run_stage(
"hunter_pool",
lambda: orchestrator.run_pool(
run.target, targets, rankings, scan_id=run.scan_id, ruleset=ruleset, policy=policy, emit=runtime.emit
),
)
else:
if bool(hunter_model) and not harnessx_present:
runtime.state["degradations"].append(
{"stage": "hunter_pool", "requested": "harnessx", "reason": "harnessx_extra_unavailable", "fallback": "run_hunter_pool"}
)
hunter_result = runtime.run_stage(
"hunter_pool",
lambda: run_hunter_pool(run.target, targets, rankings, scan_id=run.scan_id, ruleset=ruleset, policy=policy),
)
2026-06-18 10:59:25 +02:00
findings = hunter_result.findings
trajectories = hunter_result.trajectories
else:
findings = runtime.run_stage(
"quick_findings",
lambda: quick_scan_findings(
run.target,
targets,
rankings,
ruleset,
policy,
min_emit_priority=config.ruleset.min_emit_priority,
min_emit_precision=config.ruleset.min_emit_precision,
),
)
2026-06-18 10:59:25 +02:00
trajectories = []
# Shadow-status rules fire but are excluded from the report and the score;
# their outcomes are preserved separately for precision tracking.
shadow_findings = [finding for finding in findings if finding.status == "shadow"]
if shadow_findings:
findings = [finding for finding in findings if finding.status != "shadow"]
write_findings(shadow_findings, run.root / "shadow_findings.json")
# Bounded-CI budget: cap the reported finding count (0 = unlimited). Findings are
# severity/priority-sorted, so truncation keeps the most severe and is disclosed.
max_findings = config.hardening.max_findings
if max_findings and len(findings) > max_findings:
runtime.state["degradations"].append(
{"stage": "budget", "reason": "max_findings_exceeded", "requested": max_findings, "actual": len(findings)}
)
findings = findings[:max_findings]
2026-06-18 10:16:25 +02:00
findings_path = run.root / "findings.json"
verification_path = run.root / "verification.json"
markdown_path = run.root / "report.md"
sarif_path = run.root / "report.sarif"
manifest_path = run.root / "manifest.json"
2026-06-18 10:59:25 +02:00
trajectories_path = run.root / "traces" / "hunter_trajectories.jsonl"
2026-06-18 10:16:25 +02:00
write_findings(findings, findings_path)
2026-06-18 10:59:25 +02:00
if trajectories:
write_hunter_trajectories(trajectories, trajectories_path)
if mode == "standard" and bool(verifier_model) and not harnessx_present:
runtime.state["degradations"].append(
{"stage": "verify", "requested": "harnessx", "reason": "harnessx_extra_unavailable", "fallback": "structural_verifier"}
)
verifications = runtime.run_stage(
"verify",
lambda: verify_findings_dispatch(
findings,
verifier_model=verifier_model,
verifier_provider=config.harnessx.provider,
use_harnessx=hx_verify,
),
)
2026-06-18 10:16:25 +02:00
write_verification_results(verifications, verification_path)
runtime.run_stage(
"record_calibration",
lambda: _persist_calibration_feedback(run, ledger_path, prior_learnings, findings, verifications),
)
# Fusion: two-panel adjudication for triggered findings (standard mode). Runs
# deterministically by default; routes panels through the configured provider when
# a panel model is set and the extra is present, else falls back + records a degradation.
fusion_decisions: list[FusionDecision] = []
fusion_path = run.root / "fusion.json"
if mode == "standard" and config.fusion.enabled:
hx_fusion = bool(config.fusion.panel_model) and harnessx_present
if bool(config.fusion.panel_model) and not harnessx_present:
runtime.state["degradations"].append(
{"stage": "fusion", "requested": "harnessx", "reason": "harnessx_extra_unavailable", "fallback": "deterministic_panels"}
)
fusion_decisions = runtime.run_stage(
"fusion",
lambda: fuse_findings_dispatch(
findings,
verifications,
panel_model=config.fusion.panel_model,
decider_model=config.fusion.decider_model,
provider=config.harnessx.provider,
use_harnessx=hx_fusion,
high_assurance=config.fusion.high_assurance,
),
)
if fusion_decisions:
fusion_path.write_text(json.dumps([decision.to_dict() for decision in fusion_decisions], indent=2, sort_keys=True) + "\n")
cwe_by_rule = {rule.rule_id: rule.cwe for rule in ruleset}
rule_cwe_by_id = {finding.finding_id: cwe_by_rule.get(finding.finding_id.split(":", 1)[0], "") for finding in findings}
# A confirmed false positive (prior-scan learning) lowers a finding's effective
# reachability multiplier in the score instead of deleting the rule.
reachability_override = fp_reachability_overrides(findings, prior_learnings)
score_artifact = runtime.run_stage(
"score",
lambda: build_score_artifact(
findings,
rule_cwe_by_id,
policy,
k=config.score.k,
min_score=config.score.min_score,
block_severity_reachable=config.score.block_severity_reachable,
blocking=config.score.blocking,
reachability_override=reachability_override,
),
)
score_path = run.root / "score.json"
score_path.write_text(json.dumps(score_artifact.to_dict(), indent=2, sort_keys=True) + "\n")
plan = record_completed(plan, Stage.STATIC)
complexity_payload: dict[str, object] | None = None
complexity_map_path = run.root / "complexity_map.json"
overlay_path = run.root / "overlay.json"
built_map = None
overlay_records: list[OverlayRecord] = []
wrote_overlay = False
variants_payload: dict[str, object] | None = None
variant_findings: list[StaticFinding] = []
mechanisms_cited: dict[str, dict[str, object]] = {}
obligations_payload: dict[str, object] | None = None
learning_payload: dict[str, object] | None = None
learning_cited: dict[str, dict[str, object]] = {}
obligations_cited: dict[str, dict[str, object]] = {}
if Stage.MAP in plan.requested:
built_map = runtime.run_stage(
"map",
lambda: complexity_map.build_complexity_map(
targets,
findings,
complexity_map_path,
repo_files=[target.path for target in targets],
ledger_path=run.target / CALIBRATION_DIR / "complexity_ledger.json",
),
)
facts_or_error: SemanticFacts | FactLoadError
try:
facts_or_error = load_facts()
except FactLoadError as exc:
facts_or_error = exc
runtime.state["degradations"].append({"stage": "overlay", "reason": "facts_unavailable", "detail": str(exc)})
overlay_records = list(
runtime.run_stage(
"overlay",
lambda: adjudicate(root=run.target, targets=targets, findings=findings, facts=facts_or_error),
)
)
write_overlay(overlay_records, overlay_path)
wrote_overlay = True
coverage_findings = [finding_from_coverage(record) for record in overlay_records if record.disposition == "coverage"]
if coverage_findings:
seen = {finding.finding_id for finding in findings}
extra = [item for item in coverage_findings if item.finding_id not in seen]
if extra:
findings = findings + extra
write_findings(findings, findings_path)
if config.variants.enabled:
variant_findings, overlay_records, variants_payload, mechanisms_cited = _search_variants(
root=run.target,
targets=targets,
overlay_records=overlay_records,
facts=facts_or_error,
max_mechanisms=config.variants.max_mechanisms,
runtime=runtime,
)
if variant_findings:
findings = findings + variant_findings
write_findings(findings, findings_path)
write_overlay(overlay_records, overlay_path)
if config.obligations.enabled:
obligation_findings, obligations_payload, obligations_cited = _check_obligations(
root=run.target,
targets=targets,
entries=entry_points,
facts=facts_or_error,
settings=config.obligations,
runtime=runtime,
hunter_model=hunter_model,
)
if obligation_findings:
findings = rank_obligations(findings + obligation_findings)
write_findings(findings, findings_path)
if config.learning.enabled:
family_findings, learning_payload, learning_cited = _run_family_detectors(
root=run.target,
hotspots=built_map.hotspots if built_map is not None else (),
cases=None,
settings=config.learning,
config=config,
runtime=runtime,
)
if family_findings:
findings = findings + family_findings
write_findings(findings, findings_path)
plan = record_completed(plan, Stage.MAP)
if not hunter_model:
runtime.state["degradations"].append(skip_as_degradation(Stage.MAP, "hunter_model_unavailable"))
else:
built_map, findings, verifications = _attach_tool_hunter(
root=run.target,
built_map=built_map,
findings=findings,
verifications=verifications,
hunter_model=hunter_model,
max_hotspots=config.complexity.max_hunter_hotspots,
max_findings=max_findings,
findings_path=findings_path,
verification_path=verification_path,
complexity_map_path=complexity_map_path,
runtime=runtime,
)
complexity_payload = {
"hotspot_count": len(built_map.hotspots),
"heuristic_only": built_map.heuristic_only,
}
verdicts_path = run.root / "verdicts.json"
wrote_verdicts = False
verdict_records: tuple[CandidateVerdict, ...] = ()
if Stage.REGRESS in plan.requested:
probe = resolve_sandbox_probe()
if not probe.available():
plan = record_skip(plan, Stage.REGRESS, "sandbox_unavailable")
runtime.state["degradations"].append(skip_as_degradation(Stage.REGRESS, "sandbox_unavailable"))
else:
sandbox = resolve_sandbox_runner()
hotspots = built_map.hotspots if built_map is not None else ()
hotspots = filter_promoted_hotspots(hotspots, overlay_records, findings)
store = MechanismStore(run.target / CALIBRATION_DIR / "mechanisms.jsonl")
embed_client = None
embed_model = config.embeddings.model
if embed_model:
try:
embed_client = OpenRouterEmbeddingClient.from_env()
except OpenRouterError:
embed_client = None
hotspots, budget_degradation = order_promotions(
hotspots,
overlay_records,
findings,
store=store,
client=embed_client,
model=embed_model,
)
if budget_degradation:
runtime.state["degradations"].append({"stage": "prove_budget", "reason": budget_degradation})
promoted = [
finding
for finding in findings
if finding.finding_id in {record.proposal_id for record in overlay_records if record.disposition == "promote"}
]
# Variant findings are candidates after the promotions (Req 3.4): same safety check, same cap, lowest rank.
variant_ids = {finding.finding_id for finding in variant_findings}
promoted = promoted + [finding for finding in findings if finding.finding_id in variant_ids]
hotspots = hotspots + tuple(_hotspot_from_variant(finding) for finding in findings if finding.finding_id in variant_ids)
cwe_by_rule_id = {rule.rule_id: rule.cwe for rule in ruleset}
rule_cwe_by_finding = {finding.finding_id: cwe_by_rule_id.get(finding.finding_id.split(":", 1)[0], "") for finding in promoted}
languages_by_path = {target.path: target.language for target in targets}
records = runtime.run_stage(
"regress",
lambda: run_regression(
hotspots,
promoted,
max_candidates=config.regress.max_candidates,
policy=policy,
rule_cwe=rule_cwe_by_finding,
languages_by_path=languages_by_path,
repo_root=run.target,
sandbox=sandbox,
sandbox_limits=config.sandbox,
images=dict(config.regress.images),
),
)
write_verdicts(records, verdicts_path)
persist_verdicts(run.target / CALIBRATION_DIR / "complexity_ledger.json", records)
# Req 6.4: proof rungs live on verdicts, not on StaticFinding, so obligations re-rank once the sandbox has spoken.
proven_ids = {fid for record in records if record.verdict == TRIGGERABLE for fid in record.inventory_finding_ids}
if proven_ids and any(finding.finding_id.startswith("obligation:") for finding in findings):
findings = rank_obligations(findings, proven_ids=proven_ids)
write_findings(findings, findings_path)
_append_proven_mechanisms(store, records, overlay_records, findings, run.target)
verdict_records = records
wrote_verdicts = True
plan = record_completed(plan, Stage.REGRESS)
worth_fixing_payload = _worth_fixing_payload(verdict_records) if wrote_verdicts else None
runtime.run_stage(
"report",
lambda: write_markdown_report(
findings,
markdown_path,
verifications,
redact=config.hardening.redact_secrets,
complexity_map=built_map,
verdicts=verdict_records if wrote_verdicts else None,
overlay=overlay_records if wrote_overlay else None,
mechanisms=mechanisms_cited or None,
obligations=obligations_cited or None,
obligations_summary=obligations_payload or None,
learning=learning_cited or None,
),
)
runtime.run_stage(
"sarif",
lambda: write_sarif_report(
findings,
verifications,
sarif_path,
overlay=overlay_records if wrote_overlay else None,
mechanisms=mechanisms_cited or None,
obligations=obligations_cited or None,
learning=learning_cited or None,
),
)
2026-06-18 10:16:25 +02:00
runtime.run_stage(
"manifest",
lambda: write_manifest(
run=run,
findings=findings,
verifications=verifications,
2026-06-18 10:59:25 +02:00
artifact_paths=_artifact_paths(
findings=findings_path,
verification=verification_path,
markdown=markdown_path,
sarif=sarif_path,
score=score_path,
2026-06-18 10:59:25 +02:00
trajectories=trajectories_path if trajectories else None,
fusion=fusion_path if fusion_decisions else None,
complexity_map=complexity_map_path if complexity_payload is not None else None,
overlay=overlay_path if wrote_overlay else None,
verdicts=verdicts_path if wrote_verdicts else None,
2026-06-18 10:59:25 +02:00
),
2026-06-18 10:16:25 +02:00
path=manifest_path,
score=score_artifact.to_dict(),
degradations=runtime.state["degradations"] or None,
fusion=[_fusion_summary(decision) for decision in fusion_decisions] or None,
stages=stages_payload(plan),
complexity=complexity_payload,
worth_fixing=worth_fixing_payload,
provenance=provenance.to_dict(),
variants=variants_payload,
obligations=obligations_payload,
learning=learning_payload,
2026-06-18 10:16:25 +02:00
),
)
runtime.finish(status="succeeded")
return ScanOutcome(
scan_id=run.scan_id,
run_dir=run.root,
file_target_count=len(targets),
ranked_target_count=len(rankings),
finding_count=len(findings),
calibrations_applied=len(applied_calibrations),
exit_code=scan_exit_code(findings, verifications, fail_on, worth_fixing_verdicts=verdict_records),
)
def _append_proven_mechanisms(
store: MechanismStore,
verdicts: tuple[CandidateVerdict, ...],
overlay_records: list[OverlayRecord],
findings: list[StaticFinding],
target: Path,
) -> None:
overlay_by_id = {record.proposal_id: record for record in overlay_records}
finding_by_id = {finding.finding_id: finding for finding in findings}
for verdict in verdicts:
if verdict.verdict != TRIGGERABLE:
continue
for finding_id in verdict.inventory_finding_ids:
record = overlay_by_id.get(finding_id)
if record is None or record.disposition != "promote":
continue
finding = finding_by_id.get(finding_id)
append_mechanism(
store,
summary=record.reason,
cwe=record.cwe,
language=record.language or verdict.language,
tags=finding.tags if finding is not None else (),
what_made_it_exploitable=record.reason,
source_finding_id=finding_id,
source_repo=str(target),
)
def _attach_tool_hunter(
*,
root: Path,
built_map: complexity_map.ComplexityMap,
findings: list[StaticFinding],
verifications: list[VerificationResult],
hunter_model: str,
max_hotspots: int,
max_findings: int,
findings_path: Path,
verification_path: Path,
complexity_map_path: Path,
runtime: HarnessRuntime,
) -> tuple[complexity_map.ComplexityMap, list[StaticFinding], list[VerificationResult]]:
client = tool_hunter.resolve_hunter_client()
if client is None:
return built_map, findings, verifications
hunter_hotspots = list(built_map.hotspots[: max(max_hotspots, 0)])
hunter_findings = runtime.run_stage(
"tool_hunter",
lambda: tool_hunter.run_tool_hunter(
root,
hunter_hotspots,
client=client,
model=hunter_model,
max_steps=tool_hunter.DEFAULT_MAX_STEPS,
),
)
seen = {finding.finding_id for finding in findings}
extra = [finding for finding in hunter_findings if finding.finding_id not in seen]
if extra:
findings = findings + extra
if max_findings and len(findings) > max_findings:
if not any(entry.get("reason") == "max_findings_exceeded" for entry in runtime.state["degradations"]):
runtime.state["degradations"].append(
{"stage": "budget", "reason": "max_findings_exceeded", "requested": max_findings, "actual": len(findings)}
)
findings = findings[:max_findings]
kept = {finding.finding_id for finding in findings}
extra = [finding for finding in extra if finding.finding_id in kept]
if extra:
verifications = verifications + verify_findings(extra)
write_verification_results(verifications, verification_path)
write_findings(findings, findings_path)
built_map = replace(built_map, heuristic_only=False)
complexity_map.write_complexity_map(built_map, complexity_map_path)
return built_map, findings, verifications
def _persist_calibration_feedback(
run: ScanRun,
ledger_path: Path,
prior_learnings: list[FalsePositiveLearning],
findings: list[StaticFinding],
verifications: list[VerificationResult],
) -> list[FalsePositiveLearning]:
2026-06-21 21:02:18 +02:00
# Store rejected or held findings as scoped learnings for the next scan.
new_learnings = learnings_from_verifications(findings, verifications)
merged = merge_false_positive_learnings(prior_learnings, new_learnings)
write_false_positive_learnings(merged, ledger_path)
write_false_positive_learnings(new_learnings, run.root / "calibration" / "false_positive_learnings.json")
return merged
def _print_scan_outcome(outcome: ScanOutcome) -> None:
print(f"scan_id={outcome.scan_id}")
print(f"run_dir={outcome.run_dir}")
print(f"file_targets={outcome.file_target_count}")
print(f"ranked_targets={outcome.ranked_target_count}")
print(f"findings={outcome.finding_count}")
print(f"calibrations_applied={outcome.calibrations_applied}")
2026-06-17 21:13:51 +02:00
def _load_static_hints(sarif_paths: tuple[str, ...]) -> list[object]:
hints: list[object] = []
for sarif_path in sarif_paths:
path = Path(sarif_path)
if path.exists():
hints.extend(ingest_sarif(path))
return hints
def _worth_fixing_payload(verdicts: tuple[CandidateVerdict, ...]) -> dict[str, object]:
worth = [record for record in verdicts if record.worth_fixing]
return {
"count": len(worth),
"verdicts": [
{
"path": record.path,
"function_name": record.function_name,
"verdict": record.verdict,
"reason": record.reason,
"worth_fixing": True,
}
for record in worth
],
}
2026-06-18 10:59:25 +02:00
def _artifact_paths(**paths: Path | None) -> dict[str, Path]:
return {name: path for name, path in paths.items() if path is not None}
def _fusion_summary(decision: FusionDecision) -> dict[str, object]:
return {
"finding_id": decision.finding_id,
"disposition": str(decision.disposition),
"decision_source": decision.decision_source,
"triggers": decision.triggers,
}
def _benchmark(manifest_path: Path, config_path: Path, mode: str) -> int:
if not manifest_path.exists() or not manifest_path.is_file():
raise SystemExit(f"benchmark manifest is not a file: {manifest_path}")
manifest = load_benchmark_manifest(manifest_path)
target = resolve_benchmark_source(manifest_path, manifest)
run = create_benchmark_run(target, manifest)
started_at = perf_counter()
outcome = _run_scan(target, config_path, mode, "never")
runtime_seconds = perf_counter() - started_at
findings = _load_scan_findings(outcome.run_dir)
baseline_findings = load_baseline_findings(manifest_path, manifest)
result = evaluate_benchmark(
run=run,
mode=mode,
findings=findings,
scan_id=outcome.scan_id,
scan_run_dir=outcome.run_dir,
runtime_seconds=runtime_seconds,
baseline_findings=baseline_findings,
)
write_benchmark_artifacts(run, manifest_path, result)
_print_scan_outcome(outcome)
print(f"benchmark_run_id={run.benchmark_run_id}")
print(f"benchmark_run_dir={run.root}")
print(f"benchmark_expected={result.metrics.expected_findings_total}")
print(f"benchmark_matched={result.metrics.matched_findings_total}")
print(f"benchmark_missed={result.metrics.missed_findings_total}")
return 0
def _improve(
manifest_path: Path,
*,
max_rounds: int,
recall_floor: float,
fp_ceiling: float,
ledger: Path | None,
journal: Path | None,
ruleset_dir: Path,
dry_run: bool,
pair_catalog: Path | None = None,
profile_tolerance: float = 0.0,
min_holdout_pairs: int = 5,
mechanism_candidates: Path | None = None,
mechanism_store: Path | None = None,
mechanisms_enabled: bool = True,
) -> int:
if not manifest_path.exists() or not manifest_path.is_file():
raise SystemExit(f"benchmark manifest is not a file: {manifest_path}")
pair_cases: tuple[object, ...] = ()
if pair_catalog is not None:
if not pair_catalog.is_file():
raise SystemExit(f"pair catalog is not a file: {pair_catalog} (pass --no-pair-gate to skip the per-profile holdout clause)")
pair_cases = select_split(load_pair_catalog(pair_catalog), "holdout")
manifest = load_benchmark_manifest(manifest_path)
target = resolve_benchmark_source(manifest_path, manifest)
# corpus-seeded-mechanisms Req 5: the lever runs when a candidates file exists (exporter output) and the pair gate is on.
# Candidates: an explicit path, else the target's calibration dir, else the working directory's (where `mechanisms export`
# writes by default). The store always lives where a scan of this target reads it unless overridden.
calibration = target / CALIBRATION_DIR
candidates_path: Path | None = mechanism_candidates
if candidates_path is None:
for candidate in (calibration / "mechanism-candidates.jsonl", Path(CALIBRATION_DIR) / "mechanism-candidates.jsonl"):
if candidate.is_file():
candidates_path = candidate
break
if not mechanisms_enabled or not pair_cases or candidates_path is None or not candidates_path.is_file():
candidates_path = None
store_path = mechanism_store if mechanism_store is not None else calibration / "mechanisms.jsonl"
policy = load_policy()
# The loop writes its accepted ledger where `scan`/`benchmark` read it, so a
# subsequent scan of this target automatically picks up the improved ruleset.
default_ledger = target / CALIBRATION_DIR / "rule_policy.json"
with tempfile.TemporaryDirectory(prefix="ousast-improve-") as scratch:
if dry_run:
ledger_path = Path(scratch) / "rule_policy.json"
journal_path = Path(scratch) / "improve_journal.json"
if candidates_path is not None: # a dry run must not mutate the scan-time store either
scratch_store = Path(scratch) / "mechanisms.jsonl"
if store_path.is_file():
scratch_store.write_bytes(store_path.read_bytes())
store_path = scratch_store
else:
ledger_path = ledger or default_ledger
journal_path = journal or ledger_path.with_name("improve_journal.json")
outcomes = run_improvement(
target,
manifest,
ledger_path=ledger_path,
journal_path=journal_path,
ruleset_dir=ruleset_dir,
policy=policy,
max_rounds=max_rounds,
recall_floor=recall_floor,
fp_ceiling=fp_ceiling,
pair_cases=pair_cases,
profile_tolerance=profile_tolerance,
min_holdout_pairs=min_holdout_pairs,
mechanism_candidates=candidates_path,
mechanism_store=store_path if candidates_path is not None else None,
)
_print_improve_outcomes(outcomes, manifest_path, target, ledger_path, dry_run=dry_run)
return 0
def _pairs(
catalog: Path,
slice_name: str,
*,
json_out: bool,
profile: str = "all",
split: str = "all",
hunter: bool = False,
hunter_model: str | None = None,
pointers: bool = False,
loo: bool = False,
loo_out: Path | None = None,
k_runs: int | None = None,
) -> int:
if not catalog.exists() or not catalog.is_file():
raise SystemExit(f"pair catalog is not a file: {catalog}")
cases = select_split(select_profile(select_slice(load_pair_catalog(catalog), slice_name), profile), split)
if loo:
return _pairs_loo(cases, loo_out or Path("reports") / "loo.json", json_out=json_out)
scan = None
if hunter:
config_path = Path("openultrasast.toml")
config = load_config(config_path if config_path.exists() else None)
model = hunter_model or config.models.hunter or ""
client = tool_hunter.resolve_hunter_client()
if model and client is not None:
from .learning.families import load_families
scan = make_hunter_scan(client, model, taxonomy=load_families())
try:
result = evaluate_catalog(cases, hunter=scan, pointers=True if pointers else None, k_runs=k_runs)
except ValueError as exc:
raise SystemExit(f"--k-runs: {exc}") from exc
if json_out:
print(json.dumps(result_payload(result), indent=2, sort_keys=True))
return 0
print_pair_metrics("pair eval", result)
print(f"signals={len(result.signals)}")
return 0
def _search_variants(
*,
root: Path,
targets: list[FileTarget],
overlay_records: list[OverlayRecord],
facts: SemanticFacts | FactLoadError,
max_mechanisms: int,
runtime: HarnessRuntime,
) -> tuple[list[StaticFinding], list[OverlayRecord], dict[str, object], dict[str, dict[str, object]]]:
"""Third MAP proposer (corpus-seeded-mechanisms Req 3): shapes from the mechanism store, merged with overlay flows.
Also returns the mechanisms the findings cite (summary, guard, pairs, cwe) for the reports (Req 6.1).
"""
from .semantic.mechanisms import corpus_mechanisms
from .semantic.variant_search import hits_to_findings, search_tree
store = MechanismStore(root / CALIBRATION_DIR / "mechanisms.jsonl")
empty: dict[str, object] = {"mechanisms_searched": 0, "files_searched": 0, "findings": 0, "merged_into_overlay": 0}
if isinstance(facts, FactLoadError):
return [], overlay_records, empty, {}
result = runtime.run_stage("variants", lambda: search_tree(root, targets, store, facts, max_mechanisms=max_mechanisms))
for degradation in result.degradations:
runtime.state["degradations"].append(degradation)
records_by_id = {record.id: record for record in corpus_mechanisms(store.load())}
summaries = {record.id: (record.summary, record.pairs, record.cwe) for record in records_by_id.values()}
findings, records = hits_to_findings(result.hits, overlay_records, summaries)
merged = sum(1 for before, after in zip(overlay_records, records, strict=True) if before.mechanism_id is None and after.mechanism_id)
payload: dict[str, object] = {
"mechanisms_searched": result.mechanisms_searched,
"files_searched": result.files_searched,
"findings": len(findings),
"merged_into_overlay": merged,
}
cited_ids = {tag.split(":", 1)[1] for finding in findings for tag in finding.tags if tag.startswith("mechanism:")} | {
str(record.mechanism_id) for record in records if record.mechanism_id
}
cited: dict[str, dict[str, object]] = {
mechanism_id: {"summary": record.summary, "guard": record.guard, "pairs": list(record.pairs), "cwe": record.cwe}
for mechanism_id, record in records_by_id.items()
if mechanism_id in cited_ids
}
return findings, records, payload, cited
def _check_obligations(
*,
root: Path,
targets: list[FileTarget],
entries: Sequence[object],
facts: SemanticFacts | FactLoadError,
settings: ObligationsConfig,
runtime: HarnessRuntime,
hunter_model: str | None = None,
) -> tuple[list[StaticFinding], dict[str, object], dict[str, dict[str, object]]]:
"""Fourth MAP proposer (authorization-obligations Req 3-6): obligations without a dominating discharger, at suspicion.
Function-local until path records exist; the declared policy is read from the target; obligation findings never
enter the sandbox candidate set.
"""
from .semantic.ir import parse_file
from .semantic.obligations import load_obligation_facts, obligation_mechanisms
from .semantic.obligations.check import check_obligations, findings_to_static
from .semantic.obligations.dominance import OrderDominance
from .semantic.obligations.policy import PolicyError, load_declared_policy
payload: dict[str, object] = {"sibling_sets": 0, "under_populated": 0, "operations": 0, "findings_by_label": {}, "policy_version": None}
if isinstance(facts, FactLoadError):
runtime.state["degradations"].append({"stage": "obligations", "reason": "facts_unavailable"})
return [], payload, {}
irs: dict[str, tuple[Any, str]] = {}
texts: dict[str, str] = {}
for target in targets:
try:
text = (root / target.path).read_text(errors="ignore")
except OSError:
continue
texts[target.path] = text
irs[target.path] = (parse_file(target.path, text, target.language), text)
policy = None
policy_path = root / settings.policy_path
try:
policy = load_declared_policy(policy_path)
except PolicyError as exc:
runtime.state["degradations"].append({"stage": "obligations", "reason": "policy_invalid", "detail": str(exc)[:200]})
store = MechanismStore(root / CALIBRATION_DIR / "mechanisms.jsonl")
result = runtime.run_stage(
"obligations",
lambda: check_obligations(
irs=irs,
entries=entries,
facts=load_obligation_facts(),
flow_facts=facts,
policy=policy,
paths=(),
dominance=OrderDominance(texts=texts),
store_shapes=obligation_mechanisms(store.load()),
min_siblings=settings.min_siblings,
),
)
from .semantic.obligations.intent import adjudicate_intent
result = adjudicate_intent(result, client=tool_hunter.resolve_hunter_client(), model=hunter_model or "", texts=texts)
for degradation in result.degradations:
runtime.state["degradations"].append(degradation)
by_label: dict[str, int] = {}
for finding in result.findings:
by_label[finding.label] = by_label.get(finding.label, 0) + 1
payload = {
"sibling_sets": len(result.sibling_sets),
"under_populated": len(result.under_populated),
"operations": result.operations,
"findings_by_label": by_label,
"policy_version": result.policy_version,
"degradations": [dict(item) for item in result.degradations],
"sets": [{"module": group.key[0], "resource": group.key[1], "handlers": len(group.handlers)} for group in result.sibling_sets],
}
statics = findings_to_static(result)
cited: dict[str, dict[str, object]] = {}
for finding, static in zip(result.findings, statics, strict=True):
cited[static.finding_id] = {
"obligation": finding.operation.kind,
"resource": finding.operation.resource,
"missing": finding.missing,
"provenance": finding.provenance,
"label": finding.label,
"evidence": list(finding.evidence),
"known_fix": finding.known_fix,
"intent": finding.intent,
"intent_rationale": finding.intent_rationale,
}
return statics, payload, cited
def _run_family_detectors(
*,
root: Path,
hotspots: Sequence[object],
cases: object,
settings: object,
config: object,
runtime: HarnessRuntime,
) -> tuple[list[StaticFinding], dict[str, object], dict[str, dict[str, object]]]:
"""Fifth MAP proposer (learning-harness Req 6.3, 7.1): one detector per admitted family, plus the generalist.
Every claim passes through its family's verifier and carries what produced it. Nothing rises above
suspicion here: the verifier writes a tag, and the evidence ladder is another boundary's to move.
"""
del cases
from .learning.classify import classify_region
from .learning.detectors import DetectorConfigError, Region, load_family_configs, run_region_detectors
from .learning.endpoint import resolve_chat_endpoint, resolve_models
from .learning.families import FamiliesError, load_families
from .learning.verifiers import apply_verification, verifier_for
empty: dict[str, dict[str, object]] = {}
configs_dir = root / str(getattr(settings, "configs_dir", ".openultrasast/learning/configs"))
try:
taxonomy = load_families(Path(p) if (p := getattr(settings, "families_path", None)) else None)
configs = load_family_configs(configs_dir, taxonomy)
except (FamiliesError, DetectorConfigError) as exc:
runtime.state["degradations"].append({"stage": "learning", "reason": "learning_configs_unavailable", "detail": str(exc)[:200]})
return [], {}, empty
if not configs:
runtime.state["degradations"].append(
{"stage": "learning", "reason": "learning_configs_unavailable", "detail": f"no family configurations under {configs_dir}"}
)
return [], {}, empty
resolved = resolve_chat_endpoint(config) # type: ignore[arg-type]
if resolved is None:
runtime.state["degradations"].append({"stage": "learning", "reason": "learning_endpoint_unavailable"})
return [], {}, empty
client, endpoint = resolved
model = resolve_models(config)[0] # type: ignore[arg-type]
findings: list[StaticFinding] = []
per_family: dict[str, int] = {}
per_tier: dict[str, int] = {}
abstained = 0
seen: set[str] = set()
for hotspot in hotspots:
path = str(getattr(hotspot, "path", ""))
function = getattr(hotspot, "function_name", None)
key = f"{path}::{function}"
if not path or key in seen:
continue
seen.add(key)
answer = classify_region(root, path, function=function, taxonomy=taxonomy, client=None)
per_tier[answer.tier] = per_tier.get(answer.tier, 0) + 1
abstained += 0 if answer.families else 1
region = Region(path=path, function=str(function) if function else None, families=answer.families)
def _run(region: Region = region) -> list[StaticFinding]:
return run_region_detectors(root, region, configs, client=client, model=model)
produced = runtime.run_stage("learning", _run)
for finding in produced:
family_id = next((tag.split(":", 1)[1] for tag in finding.tags if tag.startswith("family:")), "unknown")
verification = verifier_for(taxonomy.by_id(family_id)).verify(finding, root=root, language="python")
findings.append(apply_verification(finding, verification))
per_family[family_id] = per_family.get(family_id, 0) + 1
payload: dict[str, object] = {
"taxonomy_version": taxonomy.version,
"families": per_family,
# What the classifier did on *this* scan. Scoring it needs labels a maintainer stands behind, which a scan
# of someone else's repository does not have, so the block reports the routing rather than an accuracy.
"classifier": {
"regions": len(seen),
"per_tier": dict(sorted(per_tier.items())),
"abstained": abstained,
# Req 2.6/3.7: say out loud that no family declares a parent at this taxonomy version, so the
# partial credit for a parent-child relation never applied to any number here.
"hierarchical_credit": taxonomy.hierarchical,
},
"round": None,
"endpoint": endpoint.provider,
"regions": len(seen),
}
cited: dict[str, dict[str, object]] = {
finding.finding_id: {
"family": next((tag.split(":", 1)[1] for tag in finding.tags if tag.startswith("family:")), ""),
"detector": next((tag.split(":", 1)[1] for tag in finding.tags if tag.startswith("detector:")), ""),
"verifier": next((tag.split(":", 1)[1] for tag in finding.tags if tag.startswith("verifier:")), ""),
}
for finding in findings
}
return findings, payload, cited
def _learning(args: argparse.Namespace) -> int:
"""The maintainer's entry points into the classified detector set. Everything degrades with a reason."""
from .learning.classify import measure_classifier, write_review_queue
from .learning.families import load_families
from .learning.publish import publish
out: Path = args.out
taxonomy = load_families()
if args.learning_command == "publish":
report = publish(learning_dir=out, measurements_dir=args.measurements, roadmap=args.roadmap)
print(f"learning publish: {len(report.families)} families over {len(report.models)} model(s) -> {args.roadmap}")
for artifact in report.artifacts:
print(f" artifact {artifact}")
return 0
cases = select_vendored(select_slice(load_pair_catalog(args.catalog), args.slice))
if args.learning_command == "candidates":
# No model is called anywhere in this command. The ceiling is a property of the corpus and the enumerator,
# so it can end this feature before a cent is spent on it (constrained-detector task 1.3).
from .learning.candidates import ceiling
coverage = ceiling(cases, taxonomy=taxonomy, generator=args.generator)
out.mkdir(parents=True, exist_ok=True)
payload = coverage.to_dict()
(out / "candidate-ceiling.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
for family, block in sorted(coverage.per_family.items()):
print(
f"learning candidates {family}: {block['with_candidate']}/{block['pairs']} "
f"= {_as_float(block['ceiling']):.1%} ceiling, sites {block['sites']}"
)
for slice_name, block in sorted(coverage.per_slice.items()):
print(f" slice {slice_name}: {_as_float(block['ceiling']):.1%} over {block['pairs']} pairs")
return 0
if args.learning_command == "classify":
measured = measure_classifier(cases, taxonomy, client=None)
out.mkdir(parents=True, exist_ok=True)
payload = measured.to_dict()
(out / "classifier.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
written = write_review_queue(out / "review-queue.jsonl", measured.queue)
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(f"learning classify: classified {measured.classified}, unknown {measured.unknown}, queued {written}")
return 0
return _learning_run(args, cases, taxonomy)
def _learning_run(args: argparse.Namespace, cases: Sequence[PairCase], taxonomy: FamilyTaxonomy) -> int:
"""score, baseline and round: the three that need a detector, and degrade with a reason without one."""
from .learning.detectors import load_family_configs, write_default_configs
from .learning.endpoint import resolve_chat_endpoint, resolve_models
from .learning.journal import Archive, LearningJournal
from .learning.rounds import DEFAULT_SLICES, MIN_K_RUNS, load_noise_floors, run_baseline, run_learning_round
from .learning.scoring import aggregate
from .tool_hunter import _SYSTEM_PROMPT
out: Path = args.out
config = load_config(args.config if args.config.exists() else None)
k_runs = args.k_runs or max(config.learning.k_runs, MIN_K_RUNS)
model = args.model or resolve_models(config)[0]
configs_dir = out / "configs"
if not configs_dir.exists():
write_default_configs(configs_dir, taxonomy, prompt=_SYSTEM_PROMPT, version="0") # type: ignore[arg-type]
resolved = resolve_chat_endpoint(config)
if resolved is None:
print("learning: learning_endpoint_unavailable; scoring what can be scored without a detector")
client = resolved[0] if resolved else None
# The client meters itself; a run that never reads the meter publishes a cost of zero and can never trip its
# own cap (Req 8.3, 9.8).
meter = getattr(client, "cost_usd", None)
spent: Callable[[], float] | None = meter if callable(meter) else None
configs = load_family_configs(configs_dir, taxonomy) # type: ignore[arg-type]
def factory(family_config: object) -> Callable[[Path], list[StaticFinding]]:
from .learning.detectors import Region, run_family_detector
def scan(root: Path) -> list[StaticFinding]:
if client is None:
return []
region = Region(path=_first_source(root), function=None)
return run_family_detector(root, region, family_config, client=client, model=model) # type: ignore[arg-type]
return scan
if args.learning_command == "score":
from .learning.rounds import score_case
scores = [
score_case(case, factory(configs[_label(case, taxonomy)]), taxonomy=taxonomy, runs=k_runs, family=_label(case, taxonomy))
for case in cases
if _label(case, taxonomy) in configs
]
cutoff = config.learning.cutoff_date or ""
families = {name: block.to_dict() for name, block in aggregate(scores, taxonomy=taxonomy, cutoff=cutoff).items()}
if args.json:
print(json.dumps({"families": families}, indent=2, sort_keys=True))
else:
for name, block in sorted(families.items()):
raw = block.get("post_cutoff")
recent: dict[str, object] = raw if isinstance(raw, dict) else {}
tail = (
f" post-cutoff({cutoff})={recent.get('scorable', 0)} undated={block['undated']}"
if cutoff
else " post-cutoff=none configured"
)
print(
f"learning score {name}: scorable={block['scorable']} recall={block['recall']:.3f} youden={block['youden']:.3f}{tail}"
)
return 0
if args.learning_command == "baseline":
report = run_baseline(
cases,
taxonomy=taxonomy,
configs_dir=configs_dir,
scan_factory=factory,
model=model,
out_dir=out,
k_runs=k_runs,
cutoff=config.learning.cutoff_date or "",
# `--slice` already chose the rows; without this the baseline's own default filters them out again and
# a run over the memory-safety slice reports nothing at all (Req 8.4).
slices=(args.slice,) if args.slice != "all" else DEFAULT_SLICES,
spent_usd=spent,
thinking=config.learning.thinking,
resume=args.resume,
)
print(f"learning baseline cost: ${report.cost_usd:.4f}")
print(f"learning baseline {model}: {len(report.floors)} families, k={report.k_runs} -> {out / 'baseline'}")
return 0
journal = LearningJournal(out / "journal.jsonl")
record = run_learning_round(
cases,
family=args.family,
taxonomy=taxonomy,
configs_dir=configs_dir,
proposer=_proposer(args, configs_dir, model, config.harnessx.provider),
scan_factory=factory,
model=model,
journal=journal,
archive=Archive(out / "archive.jsonl"),
floors=load_noise_floors(out, model, (args.slice,) if args.slice != "all" else DEFAULT_SLICES, config.learning.thinking),
out_dir=out,
cost_cap_usd=args.cost_cap_usd or config.learning.round_cost_cap_usd,
minibatch=config.learning.minibatch,
k_runs=k_runs,
spent_usd=spent,
# Every other family the corpus actually carries, so a change that reaches beyond its own directory is
# measured rather than assumed impossible (Req 9.5).
sweep_families=tuple(sorted({_label(case, taxonomy) for case in cases} & set(configs) - {args.family})),
)
print(f"learning round {record.round} {record.family}: {record.outcome} ({record.reason}) cost ${record.cost_usd:.4f}")
return 0
def _as_float(value: object) -> float:
return float(value) if isinstance(value, int | float) else 0.0
def _label(case: PairCase, taxonomy: object) -> str:
"""The family that routes this pair: the maintainer's label when there is one, else the classifier's answer.
Reading only the declared label would send every unlabeled row to the generalist, whose findings earn no credit
against any real family, so a scoreboard built that way reports structural zeros (Req 2.1, 8.2)."""
from .learning.rounds import _family_of
return _family_of(case, taxonomy) # type: ignore[arg-type]
def _proposer(args: argparse.Namespace, configs_dir: Path, model: str, provider: str = "anthropic") -> Proposer:
"""`--proposals FILE` runs a recorded sequence offline; without it the round asks the meta-agent."""
from .learning.proposer import HarnessXProposer, Proposal, ScriptedProposer
path = getattr(args, "proposals", None)
if path is None:
return HarnessXProposer(configs_dir=configs_dir, model=model, provider=provider)
rows = json.loads(Path(path).read_text(encoding="utf-8"))
return ScriptedProposer(
[
Proposal(
hypothesis=str(row["hypothesis"]),
lever=row["lever"],
change=dict(row["change"]),
predicted_affected=tuple(row.get("predicted_affected", ())),
predicted_at_risk=tuple(row.get("predicted_at_risk", ())),
)
for row in rows
]
)
def _first_source(root: Path) -> str:
from .preprocess import preprocess_repository
_, targets = preprocess_repository(root)
return targets[0].path if targets else "."
def _hotspot_from_variant(finding: StaticFinding) -> Hotspot:
return hotspot_from_finding(finding, rationale="variant of a known mechanism (suspicion; sandbox may raise)")
def _pairs_loo(cases: Sequence[PairCase], out: Path, *, json_out: bool) -> int:
"""Req 4: the corpus's own detection rate; written as an artifact, never a gate."""
from .semantic.loo import evaluate_loo
result = evaluate_loo(select_vendored(cases))
payload = result.to_dict()
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
if json_out:
print(json.dumps(payload, indent=2, sort_keys=True))
return 0
for slice_name, metrics in result.per_slice.items():
print(
f"loo {slice_name}: pairs={metrics['pairs']} detected={metrics['detected']} silent={metrics['silent']} "
f"youden={metrics['youden']:+.3f} teaching={metrics['teaching']}"
)
print(f"artifact={out}")
return 0
def _mechanisms_export(catalog: Path, slice_name: str, store_path: Path, *, json_out: bool) -> int:
from .semantic.mechanisms import MechanismStore
from .semantic.seed import export_mechanisms
if not catalog.is_file():
raise SystemExit(f"pair catalog is not a file: {catalog}")
cases = select_vendored(select_slice(load_pair_catalog(catalog), slice_name))
report = export_mechanisms(cases, MechanismStore(store_path))
payload = {"slice": slice_name, "store": str(store_path), "pairs": len(cases), **report.to_dict()}
if json_out:
print(json.dumps(payload, indent=2, sort_keys=True))
return 0
print(f"mechanisms export slice={slice_name} pairs={len(cases)} seeded={report.seeded} records={report.records} -> {store_path}")
for pair, reason in report.skipped:
print(f" skip {pair}: {reason}")
return 0
def _print_improve_outcomes(outcomes: list[RoundOutcome], manifest_path: Path, target: Path, ledger_path: Path, *, dry_run: bool) -> None:
print(f"manifest={manifest_path}")
print(f"target={target}")
accepted = [o for o in outcomes if o.accepted]
for outcome in outcomes:
edits = (
", ".join(
[f"{e.rule_id}:{e.from_status}->{e.to_status}" for e in outcome.edits]
+ [f"{m.action} {m.mechanism_id}" for m in outcome.mechanism_edits]
)
or "-"
)
print(
f"round {outcome.round}: {outcome.reason} | "
f"recall {outcome.recall_before:.2%}->{outcome.recall_after:.2%} "
f"fp {outcome.fp_before:.2%}->{outcome.fp_after:.2%} "
f"score {outcome.score_before}->{outcome.score_after} | edits: {edits}"
)
if outcome.profile_regressions or outcome.profiles_under_minimum:
print(f" profiles: regressed={outcome.profile_regressions or '-'} under_minimum={outcome.profiles_under_minimum or '-'}")
print(f"rounds={len(outcomes)} accepted={len(accepted)}")
if dry_run:
print("dry_run=true (no ledger or mechanism store written)")
elif accepted:
print(f"ledger={ledger_path}")
else:
print("ledger=unchanged (no round accepted)")
def _load_scan_findings(run_dir: Path) -> list[StaticFinding]:
return load_findings(run_dir / "findings.json")
2026-06-17 21:13:51 +02:00
def _index(path: Path, config_path: Path, chunk_lines: int) -> int:
if not path.exists() or not path.is_dir():
raise SystemExit(f"index path is not a directory: {path}")
config = load_config(config_path if config_path.exists() else None)
run = create_scan_run(path, config)
snapshot, targets = preprocess_repository(run.target, run.root / "preprocess" / "file_targets.json")
chunks = build_code_chunks(run.target, targets, max_lines=chunk_lines)
payload = {
"store": config.embeddings.store or "json-local",
"embedding_model": config.embeddings.model,
"repo_root": snapshot.root,
"repo_commit": snapshot.commit,
"chunk_count": len(chunks),
"chunks": [chunk.__dict__ for chunk in chunks],
}
path_out = run.root / "index" / "chunks.json"
path_out.parent.mkdir(parents=True, exist_ok=True)
path_out.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
print(f"scan_id={run.scan_id}")
print(f"run_dir={run.root}")
print(f"chunks={len(chunks)}")
print(f"index_artifact={path_out}")
return 0
if __name__ == "__main__":
raise SystemExit(main())