mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
merge(cleanup): second legacy inventory -- vector index, equality test, unread config keys, status notes
Also drops the removed ousast index line from the scan skill. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
@@ -29,7 +29,6 @@ uv run ousast scan <repo> --mode quick
|
|||||||
uv run ousast scan <repo> --mode standard --fail-on verified
|
uv run ousast scan <repo> --mode standard --fail-on verified
|
||||||
uv run ousast scan <repo> --mode deep --fail-on worth-fixing
|
uv run ousast scan <repo> --mode deep --fail-on worth-fixing
|
||||||
uv run ousast scan <repo> --mode quick --config openultrasast.toml
|
uv run ousast scan <repo> --mode quick --config openultrasast.toml
|
||||||
uv run ousast index <repo> # chunk the checkout for the embedding index
|
|
||||||
uv run ousast mcp # the MCP server over stdio, for agent hosts
|
uv run ousast mcp # the MCP server over stdio, for agent hosts
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -6,9 +6,8 @@
|
|||||||
DEEPSEEK_API_KEY=
|
DEEPSEEK_API_KEY=
|
||||||
# DEEPSEEK_BASE_URL=https://api.deepseek.com
|
# DEEPSEEK_BASE_URL=https://api.deepseek.com
|
||||||
|
|
||||||
# Embeddings only: OpenAI text-embedding-3-small through OpenRouter.
|
# Embeddings only (`ousast learn memory embed`, default openai/text-embedding-3-small through OpenRouter).
|
||||||
OPENROUTER_API_KEY=
|
OPENROUTER_API_KEY=
|
||||||
OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
|
|
||||||
|
|
||||||
# Plane memory store. Default is a local FileStore under ~/ousast-results/plane/memory.
|
# Plane memory store. Default is a local FileStore under ~/ousast-results/plane/memory.
|
||||||
# For an S3-compatible server (RustFS is the tested one): OUSAST_MEMORY=s3://<bucket>[/<prefix>], or
|
# For an S3-compatible server (RustFS is the tested one): OUSAST_MEMORY=s3://<bucket>[/<prefix>], or
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"feature_name": "ai-service-plane",
|
"feature_name": "ai-service-plane",
|
||||||
"created_at": "2026-09-29T15:40:00Z",
|
"created_at": "2026-09-29T15:40:00Z",
|
||||||
"updated_at": "2026-09-29T17:55:00Z",
|
"updated_at": "2026-10-02T00:00:00Z",
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"phase": "implementation",
|
"phase": "implementation-complete",
|
||||||
"approvals": {
|
"approvals": {
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"generated": true,
|
"generated": true,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Requirements Document: contributor-scan
|
# Requirements Document: contributor-scan
|
||||||
|
|
||||||
|
> **Status 2026-10-02:** paused since 2026-09-23 (its last change). 27 of 51 tasks done: the MAP integration was delivered and `v1.2.0-alpha.1` rolled out NO-GO (`census-repair-verification.md`); the remaining tasks wait on `pre-push-safety-net` M2/M3 and nothing here moved after the ax plane landed. Not active.
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
`model-grounded-detection` built an arbiter and measured it: 41.3% of the corpus reaches `model_entailed`
|
`model-grounded-detection` built an arbiter and measured it: 41.3% of the corpus reaches `model_entailed`
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Requirements Document: finding-feedback-loop
|
# Requirements Document: finding-feedback-loop
|
||||||
|
|
||||||
|
> **Status 2026-10-02:** dormant. Requirements approved and design generated on 2026-09-09; the design was never approved and no tasks exist. It depends on `contributor-scan`, which is paused; reopen it with that spec.
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Nine engine defects were found in one session against two repositories. **Three were the same bug in three
|
Nine engine defects were found in one session against two repositories. **Three were the same bug in three
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ Counted 2026-10-02 from each spec's `tasks.md` checkboxes and `spec.json`.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `learned-decision-engine` | implementation | 11 of 17 tasks done |
|
| `learned-decision-engine` | implementation | 11 of 17 tasks done |
|
||||||
| `pre-push-safety-net` | implementation | 67 of 79 tasks done |
|
| `pre-push-safety-net` | implementation | 67 of 79 tasks done |
|
||||||
| `contributor-scan` | implementation | 27 of 51 tasks done |
|
| `contributor-scan` | paused | 27 of 51 tasks done; paused since 2026-09-23 (`v1.2.0-alpha.1` NO-GO), waits on pre-push M2/M3 |
|
||||||
| `ai-service-plane` | implementation | 11 of 11 tasks done; the ax plane is the agentic path |
|
| `ai-service-plane` | complete | 11 of 11; the ax plane is the agentic path |
|
||||||
| `harnessx-removal` | complete | 8 of 8; HarnessX removed 2026-09-30 |
|
| `harnessx-removal` | complete | 8 of 8; HarnessX removed 2026-09-30 |
|
||||||
| `finding-feedback-loop` | design | requirements approved, design awaiting approval |
|
| `finding-feedback-loop` | dormant | requirements approved, design unapproved since 2026-09-09; depends on `contributor-scan` |
|
||||||
| `model-grounded-detection` | complete | 16 of 16; built the arbiter this all rests on |
|
| `model-grounded-detection` | complete | 16 of 16; built the arbiter this all rests on |
|
||||||
| `authorization-obligations` | dormant | tasks approved, 0 of 24 started; superseded in practice by the dominance arbiter |
|
| `authorization-obligations` | dormant | tasks approved, 0 of 24 started; superseded in practice by the dominance arbiter |
|
||||||
| `reachability-flow-model` | dormant | 23 tasks generated, nothing approved; largely delivered by adopting Joern |
|
| `reachability-flow-model` | dormant | 23 tasks generated, nothing approved; largely delivered by adopting Joern |
|
||||||
@@ -214,6 +214,5 @@ Counted 2026-10-02 from each spec's `tasks.md` checkboxes and `spec.json`.
|
|||||||
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax (`ai-service-plane`),
|
> **Retired 2026-09-30:** HarnessX was removed by `harnessx-removal`; the agentic plane is ax (`ai-service-plane`),
|
||||||
> and evolutionary proposals come from the plane memory. The HarnessX specs in the table above are history.
|
> and evolutionary proposals come from the plane memory. The HarnessX specs in the table above are history.
|
||||||
|
|
||||||
As of 2026-09-09: 97 source modules, 90 load-bearing and 7 standalone capabilities; 814 tests.
|
|
||||||
151 source modules, 146 load-bearing and 5 standalone capabilities, 0 orphaned
|
151 source modules, 146 load-bearing and 5 standalone capabilities, 0 orphaned
|
||||||
(`benchmarks/measurements/2026-09-08-module-audit.json`, regenerated 2026-10-02); 2,009 tests collected.
|
(`benchmarks/measurements/2026-09-08-module-audit.json`, regenerated 2026-10-02); 2,023 tests collected.
|
||||||
|
|||||||
@@ -126,8 +126,7 @@ Keys and store settings live in `.env` in the working directory (gitignored, nev
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
DEEPSEEK_API_KEY=... # every LLM call: model layer, tool hunter, plane tasks, decision engine
|
DEEPSEEK_API_KEY=... # every LLM call: model layer, tool hunter, plane tasks, decision engine
|
||||||
OPENROUTER_API_KEY=... # embeddings only
|
OPENROUTER_API_KEY=... # embeddings only (`ousast learn memory embed`)
|
||||||
OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
|
|
||||||
|
|
||||||
# Plane memory store (maintainers). Unset: a local file store under ~/ousast-results/plane/memory.
|
# Plane memory store (maintainers). Unset: a local file store under ~/ousast-results/plane/memory.
|
||||||
OUSAST_MEMORY=s3://<bucket> # or s3://<bucket>/<prefix>; needs the s3 extra
|
OUSAST_MEMORY=s3://<bucket> # or s3://<bucket>/<prefix>; needs the s3 extra
|
||||||
@@ -278,6 +277,5 @@ uv run python dagger/ci.py # containerized CI pipeline
|
|||||||
```
|
```
|
||||||
|
|
||||||
Maintainer commands not covered above: `ousast repos` (pinned known-vulnerable checkouts),
|
Maintainer commands not covered above: `ousast repos` (pinned known-vulnerable checkouts),
|
||||||
`ousast index` (chunk a repository for embedding-index construction), `ousast model candidates`
|
`ousast model candidates` (what the candidate enumerator can reach) and `ousast learn` (the decision engine's data; see
|
||||||
(what the candidate enumerator can reach) and `ousast learn` (the decision engine's data; see
|
|
||||||
[docs/decision-engine.md](docs/decision-engine.md)).
|
[docs/decision-engine.md](docs/decision-engine.md)).
|
||||||
|
|||||||
+9
-2
@@ -8,8 +8,15 @@
|
|||||||
within-pair 0.72-0.93, BLOCK offered nowhere, canary below 0.9 for three families) and that the engine is not
|
within-pair 0.72-0.93, BLOCK offered nowhere, canary below 0.9 for three families) and that the engine is not
|
||||||
adopted. The model layer's residual question is documented at `model/pipeline.py` (`model/judge.py` was
|
adopted. The model layer's residual question is documented at `model/pipeline.py` (`model/judge.py` was
|
||||||
deleted); a missing provider key is described as it behaves (no question asked, entailed findings reported),
|
deleted); a missing provider key is described as it behaves (no question asked, entailed findings reported),
|
||||||
not as a recorded degradation. The `.env` reference lists `OPENROUTER_EMBEDDING_MODEL` and the memory-store
|
not as a recorded degradation. The `.env` reference lists the memory-store variables; `ousast plane memory-normalise` is listed.
|
||||||
variables; `ousast plane memory-normalise` and `ousast index` are listed.
|
- **Removed: the unused vector index, `ousast index` and `[embeddings]`** (legacy cleanup, 2026-10-02). `index.py`
|
||||||
|
keeps only the text chunker the skill router uses; the `VectorIndex` half had no caller, and nothing read the
|
||||||
|
`chunks.json` the subcommand wrote. `[embeddings] model`/`store` fail to load with the reason, like the other
|
||||||
|
retired keys; `OPENROUTER_EMBEDDING_MODEL` is no longer read (`ousast learn memory embed --model` names the model).
|
||||||
|
- **Retired three config keys nothing read** (2026-10-02): `[sandbox] network`, `[sandbox] workspace_readonly` and
|
||||||
|
`[complexity] top_k` now fail to load naming why. The sandbox always ran with `--network none` and a read-only
|
||||||
|
workspace regardless of the first two; no stage read `top_k`. `memory_mb`, `timeout_seconds`, `pids_limit` and
|
||||||
|
`max_hunter_hotspots` are read and stay.
|
||||||
|
|
||||||
# v2.0.0 (2026-10-02)
|
# v2.0.0 (2026-10-02)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ maps each old path to the new one. The records themselves are not rewritten.
|
|||||||
| `model_sinks.py` | Stage-1 source-only model classification of sinks. Moved into the package as `plane/tasks/roles.py`. | `independent/selection-v2-model-sinks.json`, `measurements/2026-10-01-decision-engine-harvest/record.json`, both `2026-10-01-decision-engine-leak-audit` records | `05d5699` (named in `selection-v2-model-sinks.json`) |
|
| `model_sinks.py` | Stage-1 source-only model classification of sinks. Moved into the package as `plane/tasks/roles.py`. | `independent/selection-v2-model-sinks.json`, `measurements/2026-10-01-decision-engine-harvest/record.json`, both `2026-10-01-decision-engine-leak-audit` records | `05d5699` (named in `selection-v2-model-sinks.json`) |
|
||||||
| `sink_candidates.py` | Diagnostic: vocabulary-declared sink candidates per case. Its declaration patterns live on in `plane/tasks/repo_facts.py`. | `independent/results-v2-model-pipeline.json` ("vocabulary sink-first: 2/17") | no commit recorded |
|
| `sink_candidates.py` | Diagnostic: vocabulary-declared sink candidates per case. Its declaration patterns live on in `plane/tasks/repo_facts.py`. | `independent/results-v2-model-pipeline.json` ("vocabulary sink-first: 2/17") | no commit recorded |
|
||||||
| `hunt.py` | The v2 tool-hunter run over the population (protocol `hunter-protocol-v2.md`, including its `FAMILY_WORDS` table). Replaced by the plane's `verify` / `agree` tasks. | `independent/hunter-protocol-v2.md`, `independent/results-v2-hunter.json` (a label source in `learn/sources.toml`) | `6bfb05f`, the commit that introduced the protocol, which pins the runner |
|
| `hunt.py` | The v2 tool-hunter run over the population (protocol `hunter-protocol-v2.md`, including its `FAMILY_WORDS` table). Replaced by the plane's `verify` / `agree` tasks. | `independent/hunter-protocol-v2.md`, `independent/results-v2-hunter.json` (a label source in `learn/sources.toml`) | `6bfb05f`, the commit that introduced the protocol, which pins the runner |
|
||||||
| `harnessx_removal_equality.py` | One-off equality proof for `harnessx-removal` (record and compare deterministic outputs). The spec is closed. `tests/test_harnessx_removal_equality.py` still checks that the comparison detects differences. | `measurements/2026-09-30-harnessx-removal-baseline/`, `measurements/2026-09-30-harnessx-removal-equality/` | baseline `ca5caf7`, candidate `b9afb66` |
|
| `harnessx_removal_equality.py` | One-off equality proof for `harnessx-removal` (record and compare deterministic outputs). The spec is closed; its unit test was dropped on 2026-10-02 so the archived copy is reference only like the rest. | `measurements/2026-09-30-harnessx-removal-baseline/`, `measurements/2026-09-30-harnessx-removal-equality/` | baseline `ca5caf7`, candidate `b9afb66` |
|
||||||
|
|
||||||
The six `independent` scripts still read the population, `evaluate.py` and the results from
|
The six `independent` scripts still read the population, `evaluate.py` and the results from
|
||||||
`benchmarks/independent/`. Their `HERE` points there, so outputs land where they always did.
|
`benchmarks/independent/`. Their `HERE` points there, so outputs land where they always did.
|
||||||
|
|||||||
@@ -338,7 +338,6 @@
|
|||||||
"classification": "load_bearing",
|
"classification": "load_bearing",
|
||||||
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||||
"importers": [
|
"importers": [
|
||||||
"cli",
|
|
||||||
"skills"
|
"skills"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -530,6 +529,7 @@
|
|||||||
"importers": [
|
"importers": [
|
||||||
"learn.compile",
|
"learn.compile",
|
||||||
"learn.evaluate",
|
"learn.evaluate",
|
||||||
|
"learn.examples",
|
||||||
"learn.features",
|
"learn.features",
|
||||||
"learn.leaks",
|
"learn.leaks",
|
||||||
"learn.program",
|
"learn.program",
|
||||||
@@ -1119,7 +1119,6 @@
|
|||||||
"hunter",
|
"hunter",
|
||||||
"hunter_tools",
|
"hunter_tools",
|
||||||
"improve.evolve",
|
"improve.evolve",
|
||||||
"index",
|
|
||||||
"learn.features",
|
"learn.features",
|
||||||
"learn.labels",
|
"learn.labels",
|
||||||
"learn.roles",
|
"learn.roles",
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ Each instrument contributes one block to a candidate's feature record (`learn/sc
|
|||||||
`INSTRUMENTS`): `language`, `quick` (quick rules), `engine` (the Joern model layer), `facts` (repo
|
`INSTRUMENTS`): `language`, `quick` (quick rules), `engine` (the Joern model layer), `facts` (repo
|
||||||
facts, such as callers and function span), `source`, `entry_points`, `roles` (model source, sink
|
facts, such as callers and function span), `source`, `entry_points`, `roles` (model source, sink
|
||||||
and sanitizer roles), `delta`, `verify` and `agree` (the plane's verify passes and their
|
and sanitizer roles), `delta`, `verify` and `agree` (the plane's verify passes and their
|
||||||
agreement), and `model_sinks`. A candidate is `(path, function, family)` of one repository and pin
|
agreement), and `model_sinks` (the archived stage-1 classifier, `benchmarks/archive/model_sinks.py`, whose
|
||||||
|
logic now lives in `plane/tasks/roles.py`). A candidate is `(path, function, family)` of one repository and pin
|
||||||
where at least one instrument emitted a signal of that family (`learn/features.py`).
|
where at least one instrument emitted a signal of that family (`learn/features.py`).
|
||||||
|
|
||||||
An instrument that did not run, had no coverage or failed records that state (`none`, `failed`,
|
An instrument that did not run, had no coverage or failed records that state (`none`, `failed`,
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ from .gate import FALSE_POSITIVE_CEILING, RECALL_FLOOR
|
|||||||
from .harness import HarnessRuntime, HarnessTraceWriter, write_harness_config
|
from .harness import HarnessRuntime, HarnessTraceWriter, write_harness_config
|
||||||
from .hunter import run_hunter_pool, write_hunter_trajectories
|
from .hunter import run_hunter_pool, write_hunter_trajectories
|
||||||
from .improve import RoundOutcome, run_improvement
|
from .improve import RoundOutcome, run_improvement
|
||||||
from .index import build_code_chunks
|
|
||||||
from .learn.cli import add_commands as add_learn_commands
|
from .learn.cli import add_commands as add_learn_commands
|
||||||
from .learn.cli import run as run_learn
|
from .learn.cli import run as run_learn
|
||||||
from .mapping import analyze_entry_points, attach_reachability_hints, ingest_sarif, write_entry_points, write_static_hints
|
from .mapping import analyze_entry_points, attach_reachability_hints, ingest_sarif, write_entry_points, write_static_hints
|
||||||
@@ -173,11 +172,6 @@ def _main(argv: list[str] | None) -> int:
|
|||||||
scan.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
|
scan.add_argument("--config", type=Path, default=Path("openultrasast.toml"))
|
||||||
scan.add_argument("--fail-on", choices=("never", "findings", "verified", "worth-fixing"), default="never")
|
scan.add_argument("--fail-on", choices=("never", "findings", "verified", "worth-fixing"), default="never")
|
||||||
|
|
||||||
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 = subparsers.add_parser("benchmark", help="run a benchmark manifest and write scoreboard artifacts")
|
||||||
benchmark.add_argument("manifest", type=Path)
|
benchmark.add_argument("manifest", type=Path)
|
||||||
benchmark.add_argument("--mode", choices=("quick", "standard", "deep"), default="quick")
|
benchmark.add_argument("--mode", choices=("quick", "standard", "deep"), default="quick")
|
||||||
@@ -426,8 +420,6 @@ def _main(argv: list[str] | None) -> int:
|
|||||||
return delivery.exit_code
|
return delivery.exit_code
|
||||||
if args.command == "scan":
|
if args.command == "scan":
|
||||||
return _scan(args.path, args.config, args.mode, args.fail_on)
|
return _scan(args.path, args.config, args.mode, args.fail_on)
|
||||||
if args.command == "index":
|
|
||||||
return _index(args.path, args.config, args.chunk_lines)
|
|
||||||
if args.command == "benchmark":
|
if args.command == "benchmark":
|
||||||
return _benchmark(args.manifest, args.config, args.mode)
|
return _benchmark(args.manifest, args.config, args.mode)
|
||||||
if args.command == "improve":
|
if args.command == "improve":
|
||||||
@@ -1564,30 +1556,5 @@ def _model_candidates(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
raise SystemExit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
+15
-25
@@ -10,8 +10,6 @@ from typing import Literal
|
|||||||
|
|
||||||
from openultrasast.contracts import Contract
|
from openultrasast.contracts import Contract
|
||||||
|
|
||||||
DEFAULT_VECTOR_STORE = "json-local"
|
|
||||||
|
|
||||||
# Retirement notes (harnessx-removal Req 1.3, 3.3). A key that asked for a retired LLM capability fails and names
|
# Retirement notes (harnessx-removal Req 1.3, 3.3). A key that asked for a retired LLM capability fails and names
|
||||||
# the replacement, never a silent downgrade; the old budget section only warns. Every line below that names the
|
# the replacement, never a silent downgrade; the old budget section only warns. Every line below that names the
|
||||||
# removed plane carries its retirement date, which the reference-search test holds the rest of the tree to.
|
# removed plane carries its retirement date, which the reference-search test holds the rest of the tree to.
|
||||||
@@ -26,6 +24,15 @@ _UNREAD_ON = "retired 2026-10-02 because nothing ever read it"
|
|||||||
_UNREAD_DYNAMIC = "no dynamic or deep tier exists; deep mode will define its own keys when it is built"
|
_UNREAD_DYNAMIC = "no dynamic or deep tier exists; deep mode will define its own keys when it is built"
|
||||||
_UNREAD_EVIDENCE = "evidence tiers come from the program model's ladder (model/ladder.py), not from configuration"
|
_UNREAD_EVIDENCE = "evidence tiers come from the program model's ladder (model/ladder.py), not from configuration"
|
||||||
_UNREAD_VARIANTS = "the structural variant search it configured was deleted with semantic/variants.py"
|
_UNREAD_VARIANTS = "the structural variant search it configured was deleted with semantic/variants.py"
|
||||||
|
_UNREAD_SANDBOX = (
|
||||||
|
"the sandbox always runs with `--network none` and a read-only workspace (sandbox/runner.py); that is an "
|
||||||
|
"invariant the runner enforces, not a knob"
|
||||||
|
)
|
||||||
|
_UNREAD_TOP_K = "no stage read it; `[complexity] max_hunter_hotspots` is the one complexity key the scan reads"
|
||||||
|
_UNREAD_EMBEDDINGS = (
|
||||||
|
"the json-local vector index it configured and `ousast index` were deleted; the decision engine embeds through "
|
||||||
|
"`ousast learn memory embed --model` (learn/embeddings.py)"
|
||||||
|
)
|
||||||
RETIRED_KEYS: dict[tuple[str, str], str] = {
|
RETIRED_KEYS: dict[tuple[str, str], str] = {
|
||||||
("models", "verifier"): f"{_RETIRED_ON}: LLM verification runs on the plane (`verify` + `agree`, {_PLANE_RUN})",
|
("models", "verifier"): f"{_RETIRED_ON}: LLM verification runs on the plane (`verify` + `agree`, {_PLANE_RUN})",
|
||||||
("fusion", "panel_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
|
("fusion", "panel_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
|
||||||
@@ -38,6 +45,11 @@ RETIRED_KEYS: dict[tuple[str, str], str] = {
|
|||||||
("evidence", "minimum_patch"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
("evidence", "minimum_patch"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||||
("variants", "enabled"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
("variants", "enabled"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
||||||
("variants", "max_mechanisms"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
("variants", "max_mechanisms"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
||||||
|
("embeddings", "model"): f"{_UNREAD_ON}: {_UNREAD_EMBEDDINGS}",
|
||||||
|
("embeddings", "store"): f"{_UNREAD_ON}: {_UNREAD_EMBEDDINGS}",
|
||||||
|
("sandbox", "network"): f"{_UNREAD_ON}: {_UNREAD_SANDBOX}",
|
||||||
|
("sandbox", "workspace_readonly"): f"{_UNREAD_ON}: {_UNREAD_SANDBOX}",
|
||||||
|
("complexity", "top_k"): f"{_UNREAD_ON}: {_UNREAD_TOP_K}",
|
||||||
}
|
}
|
||||||
RETIRED_SECTION = "harnessx" # retired 2026-09-30: loads with one warning and is otherwise ignored (Req 3.3)
|
RETIRED_SECTION = "harnessx" # retired 2026-09-30: loads with one warning and is otherwise ignored (Req 3.3)
|
||||||
RETIRED_SECTION_WARNING = (
|
RETIRED_SECTION_WARNING = (
|
||||||
@@ -94,16 +106,8 @@ class ModelConfig:
|
|||||||
thinking: bool = False
|
thinking: bool = False
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class EmbeddingConfig:
|
|
||||||
model: str | None = None
|
|
||||||
store: str = DEFAULT_VECTOR_STORE
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class SandboxConfig:
|
class SandboxConfig:
|
||||||
network: bool = False
|
|
||||||
workspace_readonly: bool = True
|
|
||||||
memory_mb: int = 2048
|
memory_mb: int = 2048
|
||||||
timeout_seconds: int = 300
|
timeout_seconds: int = 300
|
||||||
pids_limit: int = 512
|
pids_limit: int = 512
|
||||||
@@ -148,8 +152,7 @@ class FusionConfig:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ComplexityConfig:
|
class ComplexityConfig:
|
||||||
# Stage-2 map size and hunter attention. Small defaults keep PR/nightly map work cheap.
|
# Hunter attention. A small default keeps PR/nightly map work cheap.
|
||||||
top_k: int = 20
|
|
||||||
max_hunter_hotspots: int = 8
|
max_hunter_hotspots: int = 8
|
||||||
|
|
||||||
|
|
||||||
@@ -205,7 +208,6 @@ class PushConfig(Contract):
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ResolvedConfig:
|
class ResolvedConfig:
|
||||||
models: ModelConfig = ModelConfig()
|
models: ModelConfig = ModelConfig()
|
||||||
embeddings: EmbeddingConfig = EmbeddingConfig()
|
|
||||||
sandbox: SandboxConfig = SandboxConfig()
|
sandbox: SandboxConfig = SandboxConfig()
|
||||||
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
|
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
|
||||||
score: ScoreConfig = ScoreConfig()
|
score: ScoreConfig = ScoreConfig()
|
||||||
@@ -231,7 +233,6 @@ def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> Reso
|
|||||||
|
|
||||||
return ResolvedConfig(
|
return ResolvedConfig(
|
||||||
models=_load_models(data.get("models", {})),
|
models=_load_models(data.get("models", {})),
|
||||||
embeddings=_load_embeddings(data.get("embeddings", {})),
|
|
||||||
sandbox=_load_sandbox(data.get("sandbox", {})),
|
sandbox=_load_sandbox(data.get("sandbox", {})),
|
||||||
static_analysis=_load_static_analysis(data.get("static_analysis", {})),
|
static_analysis=_load_static_analysis(data.get("static_analysis", {})),
|
||||||
score=_load_score(data.get("score", {})),
|
score=_load_score(data.get("score", {})),
|
||||||
@@ -277,19 +278,9 @@ def _load_models(value: object) -> ModelConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_embeddings(value: object) -> EmbeddingConfig:
|
|
||||||
data = _section(value)
|
|
||||||
return EmbeddingConfig(
|
|
||||||
model=_string(data.get("model")) or _string(os.environ.get("OPENROUTER_EMBEDDING_MODEL")),
|
|
||||||
store=_string(data.get("store")) or DEFAULT_VECTOR_STORE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_sandbox(value: object) -> SandboxConfig:
|
def _load_sandbox(value: object) -> SandboxConfig:
|
||||||
data = _section(value)
|
data = _section(value)
|
||||||
return SandboxConfig(
|
return SandboxConfig(
|
||||||
network=bool(data.get("network", False)),
|
|
||||||
workspace_readonly=bool(data.get("workspace_readonly", True)),
|
|
||||||
memory_mb=_int_value(data.get("memory_mb"), 2048),
|
memory_mb=_int_value(data.get("memory_mb"), 2048),
|
||||||
timeout_seconds=_int_value(data.get("timeout_seconds"), 300),
|
timeout_seconds=_int_value(data.get("timeout_seconds"), 300),
|
||||||
pids_limit=_int_value(data.get("pids_limit"), 512),
|
pids_limit=_int_value(data.get("pids_limit"), 512),
|
||||||
@@ -349,7 +340,6 @@ def _load_fusion(value: object) -> FusionConfig:
|
|||||||
def _load_complexity(value: object) -> ComplexityConfig:
|
def _load_complexity(value: object) -> ComplexityConfig:
|
||||||
data = _section(value)
|
data = _section(value)
|
||||||
return ComplexityConfig(
|
return ComplexityConfig(
|
||||||
top_k=_int_value(data.get("top_k"), 20),
|
|
||||||
max_hunter_hotspots=_int_value(data.get("max_hunter_hotspots"), 8),
|
max_hunter_hotspots=_int_value(data.get("max_hunter_hotspots"), 8),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+8
-207
@@ -1,13 +1,14 @@
|
|||||||
|
"""Text chunking for the skill router (`skills.py`).
|
||||||
|
|
||||||
|
The vector index that once lived beside the chunker (`VectorIndex`, `build_vector_index`, `query_vector_index`,
|
||||||
|
`build_retrieval_package`) and the `ousast index` subcommand that fed it were removed on 2026-10-02: nothing ever
|
||||||
|
read the `chunks.json` they wrote. The decision engine's embeddings live in `learn/embeddings.py`.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
from dataclasses import dataclass
|
||||||
import math
|
|
||||||
from dataclasses import asdict, dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Protocol
|
|
||||||
|
|
||||||
from .preprocess import FileTarget, RepoSnapshot
|
|
||||||
|
|
||||||
VALID_NAMESPACES = {"repo_code", "docs", "static_findings", "mechanisms", "skills", "traces"}
|
VALID_NAMESPACES = {"repo_code", "docs", "static_findings", "mechanisms", "skills", "traces"}
|
||||||
|
|
||||||
@@ -24,82 +25,6 @@ class CodeChunk:
|
|||||||
metadata: dict[str, str | int | bool]
|
metadata: dict[str, str | int | bool]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class VectorRecord:
|
|
||||||
chunk: CodeChunk
|
|
||||||
embedding: list[float]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class VectorIndex:
|
|
||||||
store: str
|
|
||||||
embedding_model: str
|
|
||||||
repo_root: str
|
|
||||||
repo_commit: str | None
|
|
||||||
records: list[VectorRecord]
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class RetrievalHit:
|
|
||||||
score: float
|
|
||||||
chunk: CodeChunk
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class RetrievalPackage:
|
|
||||||
role: str
|
|
||||||
hits: list[RetrievalHit]
|
|
||||||
text: str
|
|
||||||
truncated: bool
|
|
||||||
|
|
||||||
|
|
||||||
class EmbeddingClient(Protocol):
|
|
||||||
def embed(self, *, model: str, inputs: list[str], timeout_seconds: int = 60) -> list[list[float]]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
def chunk_file(root: Path, target: FileTarget, *, max_lines: int = 80) -> list[CodeChunk]:
|
|
||||||
if max_lines < 1:
|
|
||||||
raise ValueError("max_lines must be at least 1")
|
|
||||||
text = (root / target.path).read_text(errors="ignore")
|
|
||||||
lines = text.splitlines()
|
|
||||||
chunks: list[CodeChunk] = []
|
|
||||||
for start in range(0, len(lines), max_lines):
|
|
||||||
chunk_lines = lines[start : start + max_lines]
|
|
||||||
if not any(line.strip() for line in chunk_lines):
|
|
||||||
continue
|
|
||||||
start_line = start + 1
|
|
||||||
end_line = start + len(chunk_lines)
|
|
||||||
chunk_text = "\n".join(chunk_lines)
|
|
||||||
chunks.append(
|
|
||||||
CodeChunk(
|
|
||||||
chunk_id=_chunk_id(target.path, start_line, end_line, chunk_text),
|
|
||||||
namespace="repo_code",
|
|
||||||
path=target.path,
|
|
||||||
language=target.language,
|
|
||||||
start_line=start_line,
|
|
||||||
end_line=end_line,
|
|
||||||
text=chunk_text,
|
|
||||||
metadata={
|
|
||||||
"path": target.path,
|
|
||||||
"language": target.language,
|
|
||||||
"start_line": start_line,
|
|
||||||
"end_line": end_line,
|
|
||||||
"has_fuzz_entry_point": target.has_fuzz_entry_point,
|
|
||||||
"tags": ",".join(target.tags),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
|
|
||||||
def build_code_chunks(root: Path, targets: list[FileTarget], *, max_lines: int = 80) -> list[CodeChunk]:
|
|
||||||
chunks: list[CodeChunk] = []
|
|
||||||
for target in targets:
|
|
||||||
chunks.extend(chunk_file(root, target, max_lines=max_lines))
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
|
|
||||||
def chunk_text_namespace(
|
def chunk_text_namespace(
|
||||||
*,
|
*,
|
||||||
namespace: str,
|
namespace: str,
|
||||||
@@ -144,130 +69,6 @@ def chunk_text_namespace(
|
|||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
def index_reuse_key(snapshot: RepoSnapshot, embedding_model: str) -> str:
|
|
||||||
source = {
|
|
||||||
"root": snapshot.root,
|
|
||||||
"commit": snapshot.commit,
|
|
||||||
"file_count": snapshot.file_count,
|
|
||||||
"languages": snapshot.languages,
|
|
||||||
"embedding_model": embedding_model,
|
|
||||||
}
|
|
||||||
return hashlib.sha256(json.dumps(source, sort_keys=True).encode()).hexdigest()[:24]
|
|
||||||
|
|
||||||
|
|
||||||
def build_vector_index(
|
|
||||||
*,
|
|
||||||
snapshot: RepoSnapshot,
|
|
||||||
chunks: list[CodeChunk],
|
|
||||||
embedding_model: str,
|
|
||||||
client: EmbeddingClient,
|
|
||||||
store: str = "json-local",
|
|
||||||
batch_size: int = 32,
|
|
||||||
) -> VectorIndex:
|
|
||||||
if batch_size < 1:
|
|
||||||
raise ValueError("batch_size must be at least 1")
|
|
||||||
records: list[VectorRecord] = []
|
|
||||||
for start in range(0, len(chunks), batch_size):
|
|
||||||
batch = chunks[start : start + batch_size]
|
|
||||||
vectors = client.embed(model=embedding_model, inputs=[chunk.text for chunk in batch])
|
|
||||||
if len(vectors) != len(batch):
|
|
||||||
raise ValueError("embedding response count did not match chunk count")
|
|
||||||
records.extend(VectorRecord(chunk=chunk, embedding=vector) for chunk, vector in zip(batch, vectors, strict=True))
|
|
||||||
return VectorIndex(
|
|
||||||
store=store,
|
|
||||||
embedding_model=embedding_model,
|
|
||||||
repo_root=snapshot.root,
|
|
||||||
repo_commit=snapshot.commit,
|
|
||||||
records=records,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def write_vector_index(index: VectorIndex, path: Path) -> None:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(json.dumps(_index_to_payload(index), indent=2, sort_keys=True) + "\n")
|
|
||||||
|
|
||||||
|
|
||||||
def read_vector_index(path: Path) -> VectorIndex:
|
|
||||||
payload = json.loads(path.read_text())
|
|
||||||
return VectorIndex(
|
|
||||||
store=str(payload["store"]),
|
|
||||||
embedding_model=str(payload["embedding_model"]),
|
|
||||||
repo_root=str(payload["repo_root"]),
|
|
||||||
repo_commit=payload.get("repo_commit"),
|
|
||||||
records=[
|
|
||||||
VectorRecord(
|
|
||||||
chunk=CodeChunk(**item["chunk"]),
|
|
||||||
embedding=[float(value) for value in item["embedding"]],
|
|
||||||
)
|
|
||||||
for item in payload.get("records", [])
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def query_vector_index(
|
|
||||||
index: VectorIndex,
|
|
||||||
query_embedding: list[float],
|
|
||||||
*,
|
|
||||||
metadata_filter: dict[str, str | int | bool] | None = None,
|
|
||||||
limit: int = 5,
|
|
||||||
) -> list[tuple[float, CodeChunk]]:
|
|
||||||
matches: list[tuple[float, CodeChunk]] = []
|
|
||||||
for record in index.records:
|
|
||||||
if metadata_filter and not _metadata_matches(record.chunk.metadata, metadata_filter):
|
|
||||||
continue
|
|
||||||
matches.append((_cosine_similarity(query_embedding, record.embedding), record.chunk))
|
|
||||||
return sorted(matches, key=lambda item: item[0], reverse=True)[:limit]
|
|
||||||
|
|
||||||
|
|
||||||
def build_retrieval_package(
|
|
||||||
*,
|
|
||||||
role: str,
|
|
||||||
index: VectorIndex,
|
|
||||||
query_embedding: list[float],
|
|
||||||
metadata_filter: dict[str, str | int | bool] | None = None,
|
|
||||||
limit: int = 5,
|
|
||||||
max_chars: int = 4000,
|
|
||||||
) -> RetrievalPackage:
|
|
||||||
hits: list[RetrievalHit] = []
|
|
||||||
parts: list[str] = []
|
|
||||||
total = 0
|
|
||||||
truncated = False
|
|
||||||
for score, chunk in query_vector_index(index, query_embedding, metadata_filter=metadata_filter, limit=limit):
|
|
||||||
block = f"[{chunk.namespace}] {chunk.path}:{chunk.start_line}-{chunk.end_line}\n{chunk.text}"
|
|
||||||
if total + len(block) > max_chars:
|
|
||||||
truncated = True
|
|
||||||
break
|
|
||||||
hits.append(RetrievalHit(score=score, chunk=chunk))
|
|
||||||
parts.append(block)
|
|
||||||
total += len(block)
|
|
||||||
return RetrievalPackage(role=role, hits=hits, text="\n\n".join(parts), truncated=truncated)
|
|
||||||
|
|
||||||
|
|
||||||
def _index_to_payload(index: VectorIndex) -> dict[str, object]:
|
|
||||||
return {
|
|
||||||
"store": index.store,
|
|
||||||
"embedding_model": index.embedding_model,
|
|
||||||
"repo_root": index.repo_root,
|
|
||||||
"repo_commit": index.repo_commit,
|
|
||||||
"records": [{"chunk": asdict(record.chunk), "embedding": record.embedding} for record in index.records],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _metadata_matches(metadata: dict[str, str | int | bool], expected: dict[str, str | int | bool]) -> bool:
|
|
||||||
return all(metadata.get(key) == value for key, value in expected.items())
|
|
||||||
|
|
||||||
|
|
||||||
def _cosine_similarity(left: list[float], right: list[float]) -> float:
|
|
||||||
if len(left) != len(right):
|
|
||||||
return 0.0
|
|
||||||
numerator = sum(a * b for a, b in zip(left, right, strict=True))
|
|
||||||
left_norm = math.sqrt(sum(a * a for a in left))
|
|
||||||
right_norm = math.sqrt(sum(b * b for b in right))
|
|
||||||
if left_norm == 0 or right_norm == 0:
|
|
||||||
return 0.0
|
|
||||||
return numerator / (left_norm * right_norm)
|
|
||||||
|
|
||||||
|
|
||||||
def _chunk_id(path: str, start_line: int, end_line: int, text: str) -> str:
|
def _chunk_id(path: str, start_line: int, end_line: int, text: str) -> str:
|
||||||
digest = hashlib.sha256(f"{path}:{start_line}:{end_line}:{text}".encode()).hexdigest()[:16]
|
digest = hashlib.sha256(f"{path}:{start_line}:{end_line}:{text}".encode()).hexdigest()[:16]
|
||||||
return f"{path}:{start_line}-{end_line}:{digest}"
|
return f"{path}:{start_line}-{end_line}:{digest}"
|
||||||
|
|||||||
+12
-21
@@ -25,7 +25,6 @@ def test_load_config_reads_toml(tmp_path: Path) -> None:
|
|||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
|
|
||||||
assert config.models.ranker == "openrouter/test-ranker"
|
assert config.models.ranker == "openrouter/test-ranker"
|
||||||
assert config.embeddings.store == "json-local"
|
|
||||||
assert config.sandbox.memory_mb == 1024
|
assert config.sandbox.memory_mb == 1024
|
||||||
|
|
||||||
|
|
||||||
@@ -74,6 +73,11 @@ def test_retired_llm_keys_fail_naming_the_replacement(tmp_path: Path, body: str,
|
|||||||
('[evidence]\nminimum_patch = "root_cause_explained"\n', "`[evidence] minimum_patch`"),
|
('[evidence]\nminimum_patch = "root_cause_explained"\n', "`[evidence] minimum_patch`"),
|
||||||
("[variants]\nenabled = false\n", "`[variants] enabled`"),
|
("[variants]\nenabled = false\n", "`[variants] enabled`"),
|
||||||
("[variants]\nmax_mechanisms = 10\n", "`[variants] max_mechanisms`"),
|
("[variants]\nmax_mechanisms = 10\n", "`[variants] max_mechanisms`"),
|
||||||
|
('[embeddings]\nmodel = "openai/text-embedding-3-small"\n', "`[embeddings] model`"),
|
||||||
|
('[embeddings]\nstore = "json-local"\n', "`[embeddings] store`"),
|
||||||
|
("[sandbox]\nnetwork = true\n", "`[sandbox] network`"),
|
||||||
|
("[sandbox]\nworkspace_readonly = false\n", "`[sandbox] workspace_readonly`"),
|
||||||
|
("[complexity]\ntop_k = 3\n", "`[complexity] top_k`"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_unread_keys_fail_naming_why(tmp_path: Path, body: str, key: str) -> None:
|
def test_unread_keys_fail_naming_why(tmp_path: Path, body: str, key: str) -> None:
|
||||||
@@ -140,30 +144,27 @@ def test_write_resolved_config_creates_json(tmp_path: Path) -> None:
|
|||||||
payload = json.loads(output.read_text())
|
payload = json.loads(output.read_text())
|
||||||
assert payload["sandbox"]["memory_mb"] == 2048
|
assert payload["sandbox"]["memory_mb"] == 2048
|
||||||
# retired 2026-10-02 (never read): the unread sections are not echoed as if they configured something
|
# retired 2026-10-02 (never read): the unread sections are not echoed as if they configured something
|
||||||
assert not {"dynamic", "evidence", "variants"} & set(payload) and "patcher" not in payload["models"]
|
assert not {"dynamic", "evidence", "variants", "embeddings"} & set(payload) and "patcher" not in payload["models"]
|
||||||
|
|
||||||
|
|
||||||
def test_complexity_and_regress_defaults_when_section_absent() -> None:
|
def test_complexity_and_regress_defaults_when_section_absent() -> None:
|
||||||
config = load_config(None)
|
config = load_config(None)
|
||||||
|
|
||||||
assert config.complexity.top_k == 20
|
|
||||||
assert config.complexity.max_hunter_hotspots == 8
|
assert config.complexity.max_hunter_hotspots == 8
|
||||||
assert config.regress.max_candidates == 5
|
assert config.regress.max_candidates == 5
|
||||||
assert config.regress.images == ()
|
assert config.regress.images == ()
|
||||||
assert config.sandbox.network is False
|
|
||||||
assert config.sandbox.workspace_readonly is True
|
|
||||||
assert config.sandbox.memory_mb == 2048
|
assert config.sandbox.memory_mb == 2048
|
||||||
assert config.sandbox.timeout_seconds == 300
|
assert config.sandbox.timeout_seconds == 300
|
||||||
assert config.sandbox.pids_limit == 512
|
assert config.sandbox.pids_limit == 512
|
||||||
|
|
||||||
|
|
||||||
def test_override_changes_top_k_and_max_candidates_only(tmp_path: Path) -> None:
|
def test_override_changes_max_hunter_hotspots_and_max_candidates_only(tmp_path: Path) -> None:
|
||||||
config_path = tmp_path / "openultrasast.toml"
|
config_path = tmp_path / "openultrasast.toml"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"[complexity]",
|
"[complexity]",
|
||||||
"top_k = 3",
|
"max_hunter_hotspots = 3",
|
||||||
"[regress]",
|
"[regress]",
|
||||||
"max_candidates = 2",
|
"max_candidates = 2",
|
||||||
]
|
]
|
||||||
@@ -172,12 +173,9 @@ def test_override_changes_top_k_and_max_candidates_only(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
config = load_config(config_path)
|
config = load_config(config_path)
|
||||||
|
|
||||||
assert config.complexity.top_k == 3
|
assert config.complexity.max_hunter_hotspots == 3
|
||||||
assert config.regress.max_candidates == 2
|
assert config.regress.max_candidates == 2
|
||||||
assert config.complexity.max_hunter_hotspots == 8
|
|
||||||
assert config.regress.images == ()
|
assert config.regress.images == ()
|
||||||
assert config.sandbox.network is False
|
|
||||||
assert config.sandbox.workspace_readonly is True
|
|
||||||
assert config.sandbox.memory_mb == 2048
|
assert config.sandbox.memory_mb == 2048
|
||||||
assert config.sandbox.timeout_seconds == 300
|
assert config.sandbox.timeout_seconds == 300
|
||||||
assert config.sandbox.pids_limit == 512
|
assert config.sandbox.pids_limit == 512
|
||||||
@@ -201,24 +199,17 @@ def test_load_config_reads_regress_image_pins(tmp_path: Path) -> None:
|
|||||||
|
|
||||||
assert config.regress.max_candidates == 4
|
assert config.regress.max_candidates == 4
|
||||||
assert dict(config.regress.images) == {"python": "python:3.12-slim", "javascript": "node:22-slim"}
|
assert dict(config.regress.images) == {"python": "python:3.12-slim", "javascript": "node:22-slim"}
|
||||||
assert config.complexity.top_k == 20
|
|
||||||
assert config.complexity.max_hunter_hotspots == 8
|
assert config.complexity.max_hunter_hotspots == 8
|
||||||
assert config.sandbox.memory_mb == 2048
|
assert config.sandbox.memory_mb == 2048
|
||||||
|
|
||||||
|
|
||||||
def test_load_dotenv_sets_missing_keys_only(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
def test_load_dotenv_sets_missing_keys_only(tmp_path: Path, monkeypatch) -> None: # type: ignore[no-untyped-def]
|
||||||
env_file = tmp_path / ".env"
|
env_file = tmp_path / ".env"
|
||||||
env_file.write_text("OPENROUTER_API_KEY=from-file\nOPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small\n")
|
env_file.write_text("OPENROUTER_API_KEY=from-file\nS3_REGION=us-east-1\n")
|
||||||
monkeypatch.setenv("OPENROUTER_API_KEY", "already-set")
|
monkeypatch.setenv("OPENROUTER_API_KEY", "already-set")
|
||||||
monkeypatch.delenv("OPENROUTER_EMBEDDING_MODEL", raising=False)
|
monkeypatch.delenv("S3_REGION", raising=False)
|
||||||
|
|
||||||
load_dotenv(env_file, force=True)
|
load_dotenv(env_file, force=True)
|
||||||
|
|
||||||
assert os.environ["OPENROUTER_API_KEY"] == "already-set"
|
assert os.environ["OPENROUTER_API_KEY"] == "already-set"
|
||||||
assert os.environ["OPENROUTER_EMBEDDING_MODEL"] == "openai/text-embedding-3-small"
|
assert os.environ["S3_REGION"] == "us-east-1"
|
||||||
|
|
||||||
|
|
||||||
def test_embeddings_model_from_env(monkeypatch) -> None: # type: ignore[no-untyped-def]
|
|
||||||
monkeypatch.setenv("OPENROUTER_EMBEDDING_MODEL", "openai/text-embedding-3-small")
|
|
||||||
config = load_config(None)
|
|
||||||
assert config.embeddings.model == "openai/text-embedding-3-small"
|
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
"""The equality instrument (harnessx-removal design section 3) sees a difference when there is one.
|
|
||||||
|
|
||||||
A comparison that always says "equal" would prove nothing, so these tests feed it records that differ in one
|
|
||||||
place each and require it to name that place, and records that differ only in excluded fields and require it
|
|
||||||
to pass.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import importlib.util
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
|
||||||
_SPEC = importlib.util.spec_from_file_location("hxr_equality", ROOT / "benchmarks" / "archive" / "harnessx_removal_equality.py")
|
|
||||||
assert _SPEC is not None and _SPEC.loader is not None
|
|
||||||
equality = importlib.util.module_from_spec(_SPEC)
|
|
||||||
_SPEC.loader.exec_module(equality)
|
|
||||||
|
|
||||||
|
|
||||||
def _record(path: Path, *, findings: object, suite: dict[str, str], stdout: str = "findings=3\n") -> Path:
|
|
||||||
(path / "scan").mkdir(parents=True)
|
|
||||||
(path / "suite.json").write_text(equality.dump(suite))
|
|
||||||
(path / "scan" / "findings.json").write_text(equality.dump(findings))
|
|
||||||
(path / "scan" / "stdout.txt").write_text(stdout)
|
|
||||||
(path / "env.json").write_text(json.dumps({"tree_ish": str(path)}))
|
|
||||||
return path
|
|
||||||
|
|
||||||
|
|
||||||
def _normalized(raw: dict[str, object], root: str) -> object:
|
|
||||||
return equality.normalize(raw, root, "/tmp/x")
|
|
||||||
|
|
||||||
|
|
||||||
def test_excluded_fields_and_run_ids_do_not_count(tmp_path: Path) -> None:
|
|
||||||
a = _normalized({"rule": "sqli", "runtime_seconds": 1.2, "path": "/r1/runs/20260930T101010Z-0123abcd/f.py", "t_ms": 4}, "/r1")
|
|
||||||
b = _normalized({"rule": "sqli", "runtime_seconds": 9.9, "path": "/r2/runs/20261001T111111Z-ffffffff/f.py", "t_ms": 7}, "/r2")
|
|
||||||
suite = {"tests.test_x::test_a": "passed"}
|
|
||||||
diffs = equality.compare(_record(tmp_path / "a", findings=a, suite=suite), _record(tmp_path / "b", findings=b, suite=suite), {})
|
|
||||||
assert diffs == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_a_changed_finding_is_reported_with_its_key(tmp_path: Path) -> None:
|
|
||||||
suite = {"tests.test_x::test_a": "passed"}
|
|
||||||
a = _record(tmp_path / "a", findings=[{"rule": "sqli", "line": 3}], suite=suite)
|
|
||||||
b = _record(tmp_path / "b", findings=[{"rule": "sqli", "line": 4}], suite=suite)
|
|
||||||
assert equality.compare(a, b, {}) == ["scan/findings.json: $[0].line: 3 -> 4"]
|
|
||||||
assert equality.main(["compare", str(a), str(b)]) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_a_changed_outcome_or_unmapped_test_is_reported(tmp_path: Path) -> None:
|
|
||||||
a = _record(tmp_path / "a", findings=[], suite={"t::a": "passed", "t::gone": "passed", "t::old": "skipped"})
|
|
||||||
b = _record(tmp_path / "b", findings=[], suite={"t::a": "failure", "t::new": "skipped", "t::extra": "passed"}, stdout="findings=2\n")
|
|
||||||
diffs = equality.compare(a, b, {"deleted": ["t::gone"], "renamed": {"t::old": "t::new"}})
|
|
||||||
assert diffs == [
|
|
||||||
"suite.json: t::a passed -> failure",
|
|
||||||
"suite.json: t::extra new in candidate",
|
|
||||||
"scan/stdout.txt: first differing line 1: 'findings=3' -> 'findings=2'",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_a_file_present_on_one_side_only_is_reported(tmp_path: Path) -> None:
|
|
||||||
a = _record(tmp_path / "a", findings=[], suite={})
|
|
||||||
b = _record(tmp_path / "b", findings=[], suite={})
|
|
||||||
(b / "scan" / "fusion.json").write_text("[]\n")
|
|
||||||
assert equality.compare(a, b, {}) == ["scan/fusion.json: present only in candidate"]
|
|
||||||
+14
-83
@@ -1,80 +1,9 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from openultrasast.index import (
|
from openultrasast.index import chunk_text_namespace
|
||||||
CodeChunk,
|
|
||||||
build_code_chunks,
|
|
||||||
build_retrieval_package,
|
|
||||||
build_vector_index,
|
|
||||||
chunk_text_namespace,
|
|
||||||
index_reuse_key,
|
|
||||||
query_vector_index,
|
|
||||||
read_vector_index,
|
|
||||||
write_vector_index,
|
|
||||||
)
|
|
||||||
from openultrasast.preprocess import RepoSnapshot, preprocess_repository
|
|
||||||
|
|
||||||
|
|
||||||
class FakeEmbeddingClient:
|
def test_namespace_chunks_are_metadata_scoped() -> None:
|
||||||
def embed(self, *, model: str, inputs: list[str], timeout_seconds: int = 60) -> list[list[float]]:
|
|
||||||
return [[float(len(text)), 1.0] for text in inputs]
|
|
||||||
|
|
||||||
|
|
||||||
def test_build_code_chunks_preserves_metadata_filters(tmp_path: Path) -> None:
|
|
||||||
repo = tmp_path / "repo"
|
|
||||||
repo.mkdir()
|
|
||||||
(repo / "app.py").write_text("def login(user):\n return user\n")
|
|
||||||
_, targets = preprocess_repository(repo)
|
|
||||||
|
|
||||||
chunks = build_code_chunks(repo, targets, max_lines=1)
|
|
||||||
|
|
||||||
assert len(chunks) == 2
|
|
||||||
assert chunks[0].metadata["path"] == "app.py"
|
|
||||||
assert chunks[0].metadata["language"] == "python"
|
|
||||||
assert chunks[0].start_line == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_json_vector_index_round_trips_and_filters(tmp_path: Path) -> None:
|
|
||||||
repo = tmp_path / "repo"
|
|
||||||
repo.mkdir()
|
|
||||||
(repo / "app.py").write_text("def login(user):\n return user\n")
|
|
||||||
snapshot, targets = preprocess_repository(repo)
|
|
||||||
chunks = build_code_chunks(repo, targets)
|
|
||||||
index = build_vector_index(snapshot=snapshot, chunks=chunks, embedding_model="openrouter/embed", client=FakeEmbeddingClient())
|
|
||||||
output = tmp_path / "index.json"
|
|
||||||
|
|
||||||
write_vector_index(index, output)
|
|
||||||
loaded = read_vector_index(output)
|
|
||||||
matches = query_vector_index(loaded, [20.0, 1.0], metadata_filter={"language": "python"})
|
|
||||||
|
|
||||||
assert loaded.embedding_model == "openrouter/embed"
|
|
||||||
assert matches[0][1].path == "app.py"
|
|
||||||
assert json.loads(output.read_text())["store"] == "json-local"
|
|
||||||
|
|
||||||
|
|
||||||
def test_vector_index_rejects_embedding_count_mismatch() -> None:
|
|
||||||
class EmptyEmbeddingClient:
|
|
||||||
def embed(self, *, model: str, inputs: list[str], timeout_seconds: int = 60) -> list[list[float]]:
|
|
||||||
return []
|
|
||||||
|
|
||||||
chunk = CodeChunk(
|
|
||||||
chunk_id="chunk-1",
|
|
||||||
namespace="repo_code",
|
|
||||||
path="app.py",
|
|
||||||
language="python",
|
|
||||||
start_line=1,
|
|
||||||
end_line=1,
|
|
||||||
text="print('x')",
|
|
||||||
metadata={"path": "app.py", "language": "python"},
|
|
||||||
)
|
|
||||||
snapshot = RepoSnapshot(root="/repo", commit=None, file_count=0, languages={})
|
|
||||||
with pytest.raises(ValueError):
|
|
||||||
build_vector_index(snapshot=snapshot, chunks=[chunk], embedding_model="model", client=EmptyEmbeddingClient())
|
|
||||||
|
|
||||||
|
|
||||||
def test_namespace_chunks_and_reuse_key_are_metadata_scoped() -> None:
|
|
||||||
chunks = chunk_text_namespace(
|
chunks = chunk_text_namespace(
|
||||||
namespace="skills",
|
namespace="skills",
|
||||||
path="semgrep.md",
|
path="semgrep.md",
|
||||||
@@ -82,21 +11,23 @@ def test_namespace_chunks_and_reuse_key_are_metadata_scoped() -> None:
|
|||||||
metadata={"language": "markdown", "vulnerability_class": "mapping"},
|
metadata={"language": "markdown", "vulnerability_class": "mapping"},
|
||||||
max_lines=1,
|
max_lines=1,
|
||||||
)
|
)
|
||||||
snapshot = RepoSnapshot(root="/repo", commit="abc", file_count=1, languages={"python": 1})
|
|
||||||
|
|
||||||
assert len(chunks) == 2
|
assert len(chunks) == 2
|
||||||
assert chunks[0].namespace == "skills"
|
assert chunks[0].namespace == "skills"
|
||||||
|
assert chunks[0].language == "markdown"
|
||||||
assert chunks[0].metadata["vulnerability_class"] == "mapping"
|
assert chunks[0].metadata["vulnerability_class"] == "mapping"
|
||||||
assert index_reuse_key(snapshot, "embed-a") != index_reuse_key(snapshot, "embed-b")
|
assert (chunks[0].start_line, chunks[0].end_line) == (1, 1)
|
||||||
|
assert chunks[0].chunk_id != chunks[1].chunk_id
|
||||||
|
|
||||||
|
|
||||||
def test_retrieval_package_is_bounded_by_character_budget() -> None:
|
def test_blank_blocks_are_skipped_and_the_language_defaults_to_text() -> None:
|
||||||
chunks = chunk_text_namespace(namespace="docs", path="README.md", text="alpha\n" + "beta" * 100, max_lines=1)
|
chunks = chunk_text_namespace(namespace="docs", path="README.md", text="alpha\n\n\nbeta\n", max_lines=1)
|
||||||
snapshot = RepoSnapshot(root="/repo", commit=None, file_count=1, languages={"markdown": 1})
|
|
||||||
index = build_vector_index(snapshot=snapshot, chunks=chunks, embedding_model="model", client=FakeEmbeddingClient())
|
|
||||||
|
|
||||||
package = build_retrieval_package(role="verifier", index=index, query_embedding=[4.0, 1.0], max_chars=80)
|
assert [chunk.text for chunk in chunks] == ["alpha", "beta"]
|
||||||
|
assert chunks[0].language == "text"
|
||||||
|
|
||||||
assert package.role == "verifier"
|
|
||||||
assert package.hits
|
@pytest.mark.parametrize(("namespace", "max_lines"), [("unknown", 80), ("docs", 0)])
|
||||||
assert package.truncated is True
|
def test_invalid_namespace_or_window_is_rejected(namespace: str, max_lines: int) -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
chunk_text_namespace(namespace=namespace, path="x", text="y", max_lines=max_lines)
|
||||||
|
|||||||
Reference in New Issue
Block a user