2026-06-17 21:13:51 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
2026-06-21 11:49:31 +02:00
|
|
|
from dataclasses import dataclass
|
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-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-06-17 21:13:51 +02:00
|
|
|
from .config import load_config
|
2026-06-22 15:27:16 +02:00
|
|
|
from .findings import StaticFinding, quick_scan_findings, write_findings
|
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-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-06-22 12:31:35 +02:00
|
|
|
from .policy import assert_rules_resolve, load_policy
|
2026-06-18 07:57:06 +02:00
|
|
|
from .preprocess import preprocess_repository, write_preprocess_artifact
|
2026-06-17 21:13:51 +02:00
|
|
|
from .rank import rank_targets, write_rankings
|
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-06-22 12:31:35 +02:00
|
|
|
from .scoring import build_score_artifact
|
2026-06-22 16:29:52 +02:00
|
|
|
from .verification import VerificationResult, write_verification_results
|
|
|
|
|
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:
|
|
|
|
|
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-06-18 10:16:25 +02:00
|
|
|
scan.add_argument("--fail-on", choices=("never", "findings", "verified"), 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-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-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-18 10:59:25 +02:00
|
|
|
if mode == "deep":
|
|
|
|
|
raise SystemExit("mode 'deep' is specified but sandboxed dynamic analysis is not implemented")
|
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}")
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
trace_writer=HarnessTraceWriter(run.root / "trace" / "events.jsonl"),
|
|
|
|
|
)
|
|
|
|
|
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")
|
|
|
|
|
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:
|
|
|
|
|
orchestrator = HxScanOrchestrator(provider_model=hunter_model)
|
|
|
|
|
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-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",
|
|
|
|
|
lambda: verify_findings_dispatch(findings, verifier_model=verifier_model, use_harnessx=hx_verify),
|
|
|
|
|
)
|
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 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-06-18 10:16:25 +02:00
|
|
|
runtime.run_stage("report", lambda: write_markdown_report(findings, markdown_path, verifications))
|
|
|
|
|
runtime.run_stage("sarif", lambda: write_sarif_report(findings, verifications, sarif_path))
|
|
|
|
|
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-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-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-06-21 11:49:31 +02:00
|
|
|
exit_code=scan_exit_code(findings, verifications, fail_on),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
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-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-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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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())
|