2026-06-17 21:13:51 +02:00
from __future__ import annotations
import argparse
import json
2026-06-22 17:36:58 +02:00
import tempfile
2026-09-05 14:55:00 +02:00
from collections.abc import Sequence
2026-09-04 00:04:36 +02:00
from dataclasses import dataclass , replace
2026-06-17 21:13:51 +02:00
from pathlib import Path
2026-06-21 11:49:31 +02:00
from time import perf_counter
2026-06-17 21:13:51 +02:00
2026-09-04 00:04:36 +02:00
from . import tool_hunter
2026-06-21 11:49:31 +02:00
from .benchmark import (
create_benchmark_run ,
evaluate_benchmark ,
load_baseline_findings ,
load_benchmark_manifest ,
load_findings ,
resolve_benchmark_source ,
write_benchmark_artifacts ,
)
2026-06-21 20:44:08 +02:00
from .calibration import (
FalsePositiveLearning ,
calibrate_rankings ,
2026-06-22 15:41:17 +02:00
fp_reachability_overrides ,
2026-06-21 20:44:08 +02:00
learnings_from_verifications ,
load_false_positive_learnings ,
merge_false_positive_learnings ,
write_false_positive_learnings ,
write_ranking_calibrations ,
)
2026-09-03 23:26:54 +02:00
from .complexity import map as complexity_map
2026-09-04 00:18:33 +02:00
from .complexity.ledger import persist_verdicts
2026-09-05 14:40:15 +02:00
from .complexity.map import Hotspot
2026-09-04 14:06:18 +02:00
from .config import load_config , load_dotenv
2026-06-22 15:27:16 +02:00
from .findings import StaticFinding , quick_scan_findings , write_findings
2026-06-22 18:57:03 +02:00
from .fusion import FusionDecision , fuse_findings_dispatch
2026-06-22 17:36:58 +02:00
from .gate import FALSE_POSITIVE_CEILING , RECALL_FLOOR
2026-06-18 07:57:06 +02:00
from .harness import HarnessRuntime , HarnessTraceWriter , write_harness_config
2026-06-22 16:29:52 +02:00
from .harness_ext import has_harnessx
2026-06-18 10:59:25 +02:00
from .hunter import run_hunter_pool , write_hunter_trajectories
2026-06-22 16:29:52 +02:00
from .hunter_harness import HxScanOrchestrator
2026-06-22 17:36:58 +02:00
from .improve import RoundOutcome , run_improvement
2026-06-17 21:13:51 +02:00
from .index import build_code_chunks
2026-06-18 07:57:06 +02:00
from .mapping import analyze_entry_points , attach_reachability_hints , ingest_sarif , write_entry_points , write_static_hints
2026-09-04 13:57:33 +02:00
from .pair_gate import print_pair_metrics
2026-09-05 09:27:40 +02:00
from .pairs import (
DEFAULT_CATALOG ,
SLICE_NAMES ,
2026-09-05 14:55:00 +02:00
PairCase ,
2026-09-05 09:27:40 +02:00
evaluate_catalog ,
load_pair_catalog ,
make_hunter_scan ,
result_payload ,
select_profile ,
select_slice ,
select_split ,
2026-09-05 14:40:15 +02:00
select_vendored ,
2026-09-05 09:27:40 +02:00
)
2026-06-22 12:31:35 +02:00
from .policy import assert_rules_resolve , load_policy
2026-09-05 14:40:15 +02:00
from .preprocess import FileTarget , preprocess_repository , write_preprocess_artifact
2026-09-05 09:27:50 +02:00
from .provenance import fingerprint
2026-09-04 13:57:33 +02:00
from .provider.openrouter import OpenRouterEmbeddingClient , OpenRouterError
2026-06-17 21:13:51 +02:00
from .rank import rank_targets , write_rankings
2026-09-04 13:57:33 +02:00
from .regress import TRIGGERABLE , CandidateVerdict , run_regression , write_verdicts
2026-09-05 14:55:00 +02:00
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
2026-06-22 15:27:16 +02:00
from .ruleset import DEFAULT_RULESET_DIR , load_ruleset
2026-06-21 20:44:08 +02:00
from .run import ScanRun , create_scan_run
2026-09-03 23:43:15 +02:00
from .sandbox import resolve_sandbox_probe , resolve_sandbox_runner
2026-06-22 12:31:35 +02:00
from .scoring import build_score_artifact
2026-09-04 13:57:33 +02:00
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
2026-09-03 23:43:15 +02:00
from .stages import Stage , plan_for_mode , record_completed , record_skip , skip_as_degradation , stages_payload
2026-09-04 00:04:36 +02:00
from .verification import VerificationResult , verify_findings , write_verification_results
2026-06-22 16:29:52 +02:00
from .verify_judge import verify_findings_dispatch
2026-06-21 20:44:08 +02:00
CALIBRATION_DIR = ".openultrasast/calibration"
2026-06-17 21:13:51 +02:00
2026-06-21 11:49:31 +02:00
@dataclass ( frozen = True )
class ScanOutcome :
scan_id : str
run_dir : Path
file_target_count : int
ranked_target_count : int
finding_count : int
2026-06-21 20:44:08 +02:00
calibrations_applied : int
2026-06-21 11:49:31 +02:00
exit_code : int
2026-06-17 21:13:51 +02:00
def main ( argv : list [ str ] | None = None ) -> int :
2026-09-04 14:06:18 +02:00
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" ))
2026-09-03 19:44:04 +02:00
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 )
2026-06-21 11:49:31 +02:00
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" ))
2026-06-22 17:36:58 +02:00
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" )
2026-09-05 09:28:00 +02:00
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" )
2026-09-05 15:35:08 +02:00
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" )
2026-06-22 17:36:58 +02:00
2026-09-04 13:57:33 +02:00
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 )
2026-09-05 09:27:40 +02:00
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)" )
2026-09-05 10:17:50 +02:00
pairs . add_argument ( "--pointers" , action = "store_true" , help = "allow network for non-vendored pointer pairs this run (nightly; CI never)" )
2026-09-05 14:55:00 +02:00
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)" )
2026-09-04 13:57:33 +02:00
pairs . add_argument ( "--json" , action = "store_true" , help = "print the pair scoreboard as JSON" )
2026-06-22 19:42:01 +02:00
subparsers . add_parser ( "mcp" , help = "run the narrow MCP server over stdio for OpenCode integration" )
2026-09-05 14:40:15 +02:00
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 (
2026-09-05 15:35:08 +02:00
"--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)" ,
2026-09-05 14:40:15 +02:00
)
export . add_argument ( "--json" , action = "store_true" )
2026-06-22 19:42:01 +02:00
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 )
2026-06-21 11:49:31 +02:00
if args . command == "benchmark" :
return _benchmark ( args . manifest , args . config , args . mode )
2026-06-22 17:36:58 +02:00
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 ,
2026-09-05 09:28:00 +02:00
pair_catalog = None if args . no_pair_gate else args . pair_catalog ,
profile_tolerance = args . profile_tolerance ,
min_holdout_pairs = args . min_holdout_pairs ,
2026-09-05 15:35:08 +02:00
mechanism_candidates = args . mechanism_candidates ,
mechanism_store = args . mechanism_store ,
mechanisms_enabled = not args . no_mechanisms ,
2026-06-22 17:36:58 +02:00
)
2026-09-04 13:57:33 +02:00
if args . command == "pairs" :
2026-09-05 09:27:40 +02:00
return _pairs (
args . catalog ,
args . slice ,
json_out = args . json ,
profile = args . profile ,
split = args . split ,
hunter = args . hunter ,
hunter_model = args . hunter_model ,
2026-09-05 10:17:50 +02:00
pointers = args . pointers ,
2026-09-05 14:55:00 +02:00
loo = args . loo ,
loo_out = args . loo_out ,
2026-09-05 09:27:40 +02:00
)
2026-09-05 14:40:15 +02:00
if args . command == "mechanisms" :
return _mechanisms_export ( args . catalog , args . slice , args . store , json_out = args . json )
2026-06-22 19:42:01 +02:00
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 :
2026-06-21 11:49:31 +02:00
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 } " )
2026-09-03 23:14:00 +02:00
plan = plan_for_mode ( mode )
2026-06-17 21:13:51 +02:00
config = load_config ( config_path if config_path . exists () else None )
2026-06-22 16:29:52 +02:00
# 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 )
2026-06-18 07:57:06 +02:00
runtime = HarnessRuntime (
scan_id = run . scan_id ,
config = config ,
2026-06-22 20:00:42 +02:00
trace_writer = HarnessTraceWriter ( run . root / "trace" / "events.jsonl" , redact = config . hardening . redact_secrets ),
2026-06-18 07:57:06 +02:00
)
write_harness_config ( config = config , processors = [], contract_mode = "strict" , path = run . root / "harness.json" )
runtime . start ( mode = mode , target = run . target )
2026-06-22 12:31:35 +02:00
policy = runtime . run_stage ( "policy_load" , load_policy )
2026-06-22 15:41:17 +02:00
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 ))
2026-06-22 15:27:16 +02:00
runtime . run_stage ( "policy_check" , lambda : assert_rules_resolve ( ruleset , policy ))
2026-06-18 07:57:06 +02:00
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" )
2026-09-05 09:27:50 +02:00
provenance = runtime . run_stage ( "provenance" , lambda : fingerprint ( run . target ))
2026-06-18 07:57:06 +02:00
rankings = runtime . run_stage ( "rank" , lambda : rank_targets ( targets ))
2026-06-21 20:44:08 +02:00
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" )
2026-06-21 20:44:08 +02:00
write_ranking_calibrations ( calibrations , run . root / "calibration" / "applied_calibrations.json" )
2026-06-18 10:59:25 +02:00
if mode == "standard" :
2026-06-22 16:29:52 +02:00
if hx_hunter and hunter_model :
2026-06-22 17:59:44 +02:00
orchestrator = HxScanOrchestrator (
provider_model = hunter_model ,
provider = config . harnessx . provider ,
max_cost_usd = config . harnessx . max_cost_usd ,
token_threshold = config . harnessx . token_threshold ,
)
2026-06-22 16:29:52 +02:00
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 :
2026-06-22 15:27:16 +02:00
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 = []
2026-06-22 15:27:16 +02:00
# 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" )
2026-06-22 20:00:42 +02:00
# 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 )
2026-06-22 16:29:52 +02:00
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" ,
2026-06-22 17:59:44 +02:00
lambda : verify_findings_dispatch (
findings ,
verifier_model = verifier_model ,
verifier_provider = config . harnessx . provider ,
use_harnessx = hx_verify ,
),
2026-06-22 16:29:52 +02:00
)
2026-06-18 10:16:25 +02:00
write_verification_results ( verifications , verification_path )
2026-06-21 20:44:08 +02:00
runtime . run_stage (
"record_calibration" ,
lambda : _persist_calibration_feedback ( run , ledger_path , prior_learnings , findings , verifications ),
)
2026-06-22 18:57:03 +02:00
# 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 " )
2026-06-22 15:27:16 +02:00
cwe_by_rule = { rule . rule_id : rule . cwe for rule in ruleset }
2026-06-22 12:31:35 +02:00
rule_cwe_by_id = { finding . finding_id : cwe_by_rule . get ( finding . finding_id . split ( ":" , 1 )[ 0 ], "" ) for finding in findings }
2026-06-22 15:41:17 +02:00
# 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 )
2026-06-22 12:31:35 +02:00
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 ,
2026-06-22 15:41:17 +02:00
reachability_override = reachability_override ,
2026-06-22 12:31:35 +02:00
),
)
score_path = run . root / "score.json"
score_path . write_text ( json . dumps ( score_artifact . to_dict (), indent = 2 , sort_keys = True ) + " \n " )
2026-09-03 23:14:00 +02:00
plan = record_completed ( plan , Stage . STATIC )
2026-09-03 23:26:54 +02:00
complexity_payload : dict [ str , object ] | None = None
complexity_map_path = run . root / "complexity_map.json"
2026-09-04 13:57:33 +02:00
overlay_path = run . root / "overlay.json"
2026-09-03 23:43:15 +02:00
built_map = None
2026-09-04 13:57:33 +02:00
overlay_records : list [ OverlayRecord ] = []
wrote_overlay = False
2026-09-05 14:40:15 +02:00
variants_payload : dict [ str , object ] | None = None
variant_findings : list [ StaticFinding ] = []
2026-09-05 15:35:08 +02:00
mechanisms_cited : dict [ str , dict [ str , object ]] = {}
2026-09-03 23:26:54 +02:00
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" ,
),
)
2026-09-04 13:57:33 +02:00
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 )
2026-09-05 14:40:15 +02:00
if config . variants . enabled :
2026-09-05 15:35:08 +02:00
variant_findings , overlay_records , variants_payload , mechanisms_cited = _search_variants (
2026-09-05 14:40:15 +02:00
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 )
2026-09-03 23:26:54 +02:00
plan = record_completed ( plan , Stage . MAP )
if not hunter_model :
runtime . state [ "degradations" ] . append ( skip_as_degradation ( Stage . MAP , "hunter_model_unavailable" ))
2026-09-04 00:04:36 +02:00
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 ,
)
2026-09-03 23:26:54 +02:00
complexity_payload = {
"hotspot_count" : len ( built_map . hotspots ),
"heuristic_only" : built_map . heuristic_only ,
}
2026-09-03 23:43:15 +02:00
verdicts_path = run . root / "verdicts.json"
wrote_verdicts = False
2026-09-04 00:18:33 +02:00
verdict_records : tuple [ CandidateVerdict , ... ] = ()
2026-09-03 23:43:15 +02:00
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 ()
2026-09-04 13:57:33 +02:00
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" }
]
2026-09-05 14:40:15 +02:00
# 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 )
2026-09-03 23:43:15 +02:00
cwe_by_rule_id = { rule . rule_id : rule . cwe for rule in ruleset }
2026-09-04 13:57:33 +02:00
rule_cwe_by_finding = { finding . finding_id : cwe_by_rule_id . get ( finding . finding_id . split ( ":" , 1 )[ 0 ], "" ) for finding in promoted }
2026-09-03 23:43:15 +02:00
languages_by_path = { target . path : target . language for target in targets }
records = runtime . run_stage (
"regress" ,
lambda : run_regression (
hotspots ,
2026-09-04 13:57:33 +02:00
promoted ,
2026-09-03 23:43:15 +02:00
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 )
2026-09-04 00:18:33 +02:00
persist_verdicts ( run . target / CALIBRATION_DIR / "complexity_ledger.json" , records )
2026-09-04 13:57:33 +02:00
_append_proven_mechanisms ( store , records , overlay_records , findings , run . target )
2026-09-04 00:18:33 +02:00
verdict_records = records
2026-09-03 23:43:15 +02:00
wrote_verdicts = True
plan = record_completed ( plan , Stage . REGRESS )
2026-09-04 00:18:33 +02:00
worth_fixing_payload = _worth_fixing_payload ( verdict_records ) if wrote_verdicts else None
2026-06-22 20:00:42 +02:00
runtime . run_stage (
2026-09-04 00:18:33 +02:00
"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 ,
2026-09-04 13:57:33 +02:00
overlay = overlay_records if wrote_overlay else None ,
2026-09-05 15:35:08 +02:00
mechanisms = mechanisms_cited or None ,
2026-09-04 13:57:33 +02:00
),
)
runtime . run_stage (
"sarif" ,
lambda : write_sarif_report (
findings ,
verifications ,
sarif_path ,
overlay = overlay_records if wrote_overlay else None ,
2026-09-05 15:35:08 +02:00
mechanisms = mechanisms_cited or None ,
2026-09-04 00:18:33 +02:00
),
2026-06-22 20:00:42 +02:00
)
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 ,
2026-06-22 12:31:35 +02:00
score = score_path ,
2026-06-18 10:59:25 +02:00
trajectories = trajectories_path if trajectories else None ,
2026-06-22 18:57:03 +02:00
fusion = fusion_path if fusion_decisions else None ,
2026-09-03 23:26:54 +02:00
complexity_map = complexity_map_path if complexity_payload is not None else None ,
2026-09-04 13:57:33 +02:00
overlay = overlay_path if wrote_overlay else None ,
2026-09-03 23:43:15 +02:00
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 ,
2026-06-22 12:31:35 +02:00
score = score_artifact . to_dict (),
2026-06-22 16:29:52 +02:00
degradations = runtime . state [ "degradations" ] or None ,
2026-06-22 18:57:03 +02:00
fusion = [ _fusion_summary ( decision ) for decision in fusion_decisions ] or None ,
2026-09-03 23:14:00 +02:00
stages = stages_payload ( plan ),
2026-09-03 23:26:54 +02:00
complexity = complexity_payload ,
2026-09-04 00:18:33 +02:00
worth_fixing = worth_fixing_payload ,
2026-09-05 09:27:50 +02:00
provenance = provenance . to_dict (),
2026-09-05 14:40:15 +02:00
variants = variants_payload ,
2026-06-18 10:16:25 +02:00
),
)
2026-06-18 07:57:06 +02:00
runtime . finish ( status = "succeeded" )
2026-06-21 11:49:31 +02:00
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 ),
2026-06-21 20:44:08 +02:00
calibrations_applied = len ( applied_calibrations ),
2026-09-04 00:18:33 +02:00
exit_code = scan_exit_code ( findings , verifications , fail_on , worth_fixing_verdicts = verdict_records ),
2026-06-21 11:49:31 +02:00
)
2026-09-04 13:57:33 +02:00
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 ),
)
2026-09-04 00:04:36 +02:00
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
2026-06-21 20:44:08 +02:00
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.
2026-06-21 20:44:08 +02:00
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
2026-06-21 11:49:31 +02:00
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 } " )
2026-06-21 20:44:08 +02:00
print ( f "calibrations_applied= { outcome . calibrations_applied } " )
2026-06-17 21:13:51 +02:00
2026-06-18 07:57:06 +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
2026-09-04 00:18:33 +02:00
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 }
2026-06-22 18:57:03 +02:00
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 ,
}
2026-06-21 11:49:31 +02:00
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
2026-06-22 17:36:58 +02:00
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 ,
2026-09-05 09:28:00 +02:00
pair_catalog : Path | None = None ,
profile_tolerance : float = 0.0 ,
min_holdout_pairs : int = 5 ,
2026-09-05 15:35:08 +02:00
mechanism_candidates : Path | None = None ,
mechanism_store : Path | None = None ,
mechanisms_enabled : bool = True ,
2026-06-22 17:36:58 +02:00
) -> int :
if not manifest_path . exists () or not manifest_path . is_file ():
raise SystemExit ( f "benchmark manifest is not a file: { manifest_path } " )
2026-09-05 09:28:00 +02:00
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" )
2026-06-22 17:36:58 +02:00
manifest = load_benchmark_manifest ( manifest_path )
target = resolve_benchmark_source ( manifest_path , manifest )
2026-09-05 15:35:08 +02:00
# 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"
2026-06-22 17:36:58 +02:00
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"
2026-09-05 15:35:08 +02:00
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
2026-06-22 17:36:58 +02:00
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 ,
2026-09-05 09:28:00 +02:00
pair_cases = pair_cases ,
profile_tolerance = profile_tolerance ,
min_holdout_pairs = min_holdout_pairs ,
2026-09-05 15:35:08 +02:00
mechanism_candidates = candidates_path ,
mechanism_store = store_path if candidates_path is not None else None ,
2026-06-22 17:36:58 +02:00
)
_print_improve_outcomes ( outcomes , manifest_path , target , ledger_path , dry_run = dry_run )
return 0
2026-09-05 09:27:40 +02:00
def _pairs (
catalog : Path ,
slice_name : str ,
* ,
json_out : bool ,
profile : str = "all" ,
split : str = "all" ,
hunter : bool = False ,
hunter_model : str | None = None ,
2026-09-05 10:17:50 +02:00
pointers : bool = False ,
2026-09-05 14:55:00 +02:00
loo : bool = False ,
loo_out : Path | None = None ,
2026-09-05 09:27:40 +02:00
) -> int :
2026-09-04 13:57:33 +02:00
if not catalog . exists () or not catalog . is_file ():
raise SystemExit ( f "pair catalog is not a file: { catalog } " )
2026-09-05 09:27:40 +02:00
cases = select_split ( select_profile ( select_slice ( load_pair_catalog ( catalog ), slice_name ), profile ), split )
2026-09-05 14:55:00 +02:00
if loo :
return _pairs_loo ( cases , loo_out or Path ( "reports" ) / "loo.json" , json_out = json_out )
2026-09-05 09:27:40 +02:00
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 :
scan = make_hunter_scan ( client , model )
2026-09-05 10:17:50 +02:00
result = evaluate_catalog ( cases , hunter = scan , pointers = True if pointers else None )
2026-09-04 13:57:33 +02:00
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
2026-09-05 14:40:15 +02:00
def _search_variants (
* ,
root : Path ,
targets : list [ FileTarget ],
overlay_records : list [ OverlayRecord ],
facts : SemanticFacts | FactLoadError ,
max_mechanisms : int ,
runtime : HarnessRuntime ,
2026-09-05 15:35:08 +02:00
) -> 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).
"""
2026-09-05 14:40:15 +02:00
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 ):
2026-09-05 15:35:08 +02:00
return [], overlay_records , empty , {}
2026-09-05 14:40:15 +02:00
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 )
2026-09-05 15:35:08 +02:00
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 ()}
2026-09-05 14:40:15 +02:00
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 ,
}
2026-09-05 15:35:08 +02:00
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
2026-09-05 14:40:15 +02:00
def _hotspot_from_variant ( finding : StaticFinding ) -> Hotspot :
2026-09-05 14:55:00 +02:00
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
2026-09-05 14:40:15 +02:00
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
2026-06-22 17:36:58 +02:00
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 :
2026-09-05 15:35:08 +02:00
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 "-"
)
2026-06-22 17:36:58 +02:00
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 } "
)
2026-09-05 09:28:00 +02:00
if outcome . profile_regressions or outcome . profiles_under_minimum :
print ( f " profiles: regressed= { outcome . profile_regressions or '-' } under_minimum= { outcome . profiles_under_minimum or '-' } " )
2026-06-22 17:36:58 +02:00
print ( f "rounds= { len ( outcomes ) } accepted= { len ( accepted ) } " )
if dry_run :
2026-09-05 15:35:08 +02:00
print ( "dry_run=true (no ledger or mechanism store written)" )
2026-06-22 17:36:58 +02:00
elif accepted :
print ( f "ledger= { ledger_path } " )
else :
print ( "ledger=unchanged (no round accepted)" )
2026-06-21 11:49:31 +02:00
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 ())