mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 20:19:29 +00:00
chore(config): retire unread keys [dynamic], [evidence], [variants], [models] patcher
Nothing in src reads config.dynamic, config.evidence or config.variants, and
[models] patcher was only echoed into harness.json. They now go through the
existing RETIRED_KEYS mechanism and fail with RetiredConfigError naming why
("retired 2026-10-02 because nothing ever read it: ..."); RETIRED_KEYS now
carries the full reason per key so the HarnessX entries keep their wording.
- DynamicConfig, EvidenceConfig, VariantsConfig and their loaders removed;
ModelConfig.patcher removed. resolved_config.json loses those sections.
- harness.json keeps its shape: model_roles.patcher is always null.
- RELEASE_NOTES "Unreleased" lists the keys and the output shape change.
- Delete .opencode/skills/openultrasast-kiro-impl: it pointed agents at the
retired openrouter-sast-harness tasks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3091ed42ea
commit
bd74ba63b2
@@ -1,26 +0,0 @@
|
|||||||
---
|
|
||||||
name: openultrasast-kiro-impl
|
|
||||||
description: OpenUltraSAST Kiro implementation steering. Use when continuing `.kiro/specs/openrouter-sast-harness/tasks.md`, choosing the next phase, updating checkboxes, or keeping implementation aligned with the spec.
|
|
||||||
---
|
|
||||||
|
|
||||||
# OpenUltraSAST Kiro Implementation
|
|
||||||
|
|
||||||
Use this skill when implementing the OpenUltraSAST Kiro spec from opencode.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
1. Read `.kiro/specs/openrouter-sast-harness/tasks.md` before choosing work.
|
|
||||||
2. Implement the next unchecked task slice unless the user names a different phase.
|
|
||||||
3. Keep each slice independently verifiable with `uv run pytest`.
|
|
||||||
4. Update task checkboxes only after implementation and tests pass.
|
|
||||||
5. Prefer minimal harness-compatible primitives over large framework imports.
|
|
||||||
6. Keep OpenUltraSAST independent of Clearwing; use Clearwing only as an oracle for design patterns.
|
|
||||||
7. Use project-local opencode skills to steer scan, triage, and fix workflows.
|
|
||||||
|
|
||||||
## Current Phase Order
|
|
||||||
|
|
||||||
- Phase 5: Harness runtime events, processors, contracts, and traces.
|
|
||||||
- Phase 5A: Static mapping disciplines for SARIF, Semgrep, CodeQL, differential review, and sharp edges.
|
|
||||||
- Phase 6: Evidence ladder verifier.
|
|
||||||
- Phase 7 and 7A: Docker sandbox and dynamic evidence.
|
|
||||||
- Phase 8 and later: Hunter pool, SARIF, deep mode, patch oracle, MCP, fusion, skill index, hardening.
|
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
|
## Unread config keys retired (2026-10-02)
|
||||||
|
|
||||||
|
These keys loaded but nothing ever read them. They now fail with `RetiredConfigError` (the
|
||||||
|
CLI prints it and exits 2), naming why. Remove them from your config:
|
||||||
|
|
||||||
|
- `[models] patcher`: no patching stage calls a model.
|
||||||
|
- `[dynamic] enabled`, `[dynamic] network_scope`: no dynamic or deep tier exists yet.
|
||||||
|
- `[evidence] minimum_report_verified`, `minimum_exploit`, `minimum_patch`: evidence tiers
|
||||||
|
come from the program model's ladder, not from configuration.
|
||||||
|
- `[variants] enabled`, `[variants] max_mechanisms`: the variant search they configured was
|
||||||
|
deleted earlier.
|
||||||
|
|
||||||
|
**Output shape.** `resolved_config.json` no longer has the `dynamic`, `evidence` and
|
||||||
|
`variants` sections or `models.patcher`. `harness.json` keeps its shape:
|
||||||
|
`model_roles.patcher` is always `null`, like `model_roles.verifier`.
|
||||||
|
|
||||||
## HarnessX removed; the ax plane is the agentic path (2026-09-30)
|
## HarnessX removed; the ax plane is the agentic path (2026-09-30)
|
||||||
|
|
||||||
HarnessX, the optional agentic extra (`openultrasast[harnessx]`), is removed with its code,
|
HarnessX, the optional agentic extra (`openultrasast[harnessx]`), is removed with its code,
|
||||||
|
|||||||
+19
-58
@@ -20,10 +20,24 @@ _PLANE_RUN = "`ousast plane run`, see ops/ax/README.md"
|
|||||||
_RETIRED_PANELS = (
|
_RETIRED_PANELS = (
|
||||||
f"LLM fusion panels were retired; fusion is deterministic; independent LLM agreement is the plane's `agree` task ({_PLANE_RUN})"
|
f"LLM fusion panels were retired; fusion is deterministic; independent LLM agreement is the plane's `agree` task ({_PLANE_RUN})"
|
||||||
)
|
)
|
||||||
|
# Keys that loaded but were never read (retired 2026-10-02, legacy cleanup): accepting them silently would let
|
||||||
|
# a user believe they configure something.
|
||||||
|
_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_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"
|
||||||
RETIRED_KEYS: dict[tuple[str, str], str] = {
|
RETIRED_KEYS: dict[tuple[str, str], str] = {
|
||||||
("models", "verifier"): f"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"): _RETIRED_PANELS,
|
("fusion", "panel_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
|
||||||
("fusion", "decider_model"): _RETIRED_PANELS,
|
("fusion", "decider_model"): f"{_RETIRED_ON}: {_RETIRED_PANELS}",
|
||||||
|
("models", "patcher"): f"{_UNREAD_ON}: no patching stage calls a model",
|
||||||
|
("dynamic", "enabled"): f"{_UNREAD_ON}: {_UNREAD_DYNAMIC}",
|
||||||
|
("dynamic", "network_scope"): f"{_UNREAD_ON}: {_UNREAD_DYNAMIC}",
|
||||||
|
("evidence", "minimum_report_verified"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||||
|
("evidence", "minimum_exploit"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||||
|
("evidence", "minimum_patch"): f"{_UNREAD_ON}: {_UNREAD_EVIDENCE}",
|
||||||
|
("variants", "enabled"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
||||||
|
("variants", "max_mechanisms"): f"{_UNREAD_ON}: {_UNREAD_VARIANTS}",
|
||||||
}
|
}
|
||||||
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 = (
|
||||||
@@ -38,10 +52,10 @@ class RetiredConfigError(ValueError):
|
|||||||
|
|
||||||
def _check_retired(data: dict[str, object]) -> None:
|
def _check_retired(data: dict[str, object]) -> None:
|
||||||
problems = []
|
problems = []
|
||||||
for (section, key), replacement in RETIRED_KEYS.items():
|
for (section, key), reason in RETIRED_KEYS.items():
|
||||||
table = data.get(section)
|
table = data.get(section)
|
||||||
if isinstance(table, dict) and key in table:
|
if isinstance(table, dict) and key in table:
|
||||||
problems.append(f"`[{section}] {key}` was {_RETIRED_ON}: {replacement}. Remove the key.")
|
problems.append(f"`[{section}] {key}` was {reason}. Remove the key.")
|
||||||
if problems:
|
if problems:
|
||||||
raise RetiredConfigError("\n".join(problems))
|
raise RetiredConfigError("\n".join(problems))
|
||||||
if RETIRED_SECTION in data:
|
if RETIRED_SECTION in data:
|
||||||
@@ -70,7 +84,6 @@ def load_dotenv(path: Path | None = None, *, force: bool = False) -> None:
|
|||||||
class ModelConfig:
|
class ModelConfig:
|
||||||
ranker: str | None = None
|
ranker: str | None = None
|
||||||
hunter: str | None = None
|
hunter: str | None = None
|
||||||
patcher: str | None = None
|
|
||||||
judge: str | None = None # second, independent judgement before anything is published as proven
|
judge: str | None = None # second, independent judgement before anything is published as proven
|
||||||
chat_base_url: str | None = None # chat endpoint; independent of the embedding endpoint
|
chat_base_url: str | None = None # chat endpoint; independent of the embedding endpoint
|
||||||
chat_api_key_env: str | None = None # environment variable holding that endpoint's key
|
chat_api_key_env: str | None = None # environment variable holding that endpoint's key
|
||||||
@@ -96,19 +109,6 @@ class SandboxConfig:
|
|||||||
pids_limit: int = 512
|
pids_limit: int = 512
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class DynamicConfig:
|
|
||||||
enabled: bool = False
|
|
||||||
network_scope: tuple[str, ...] = ()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class EvidenceConfig:
|
|
||||||
minimum_report_verified: str = "static_corroboration"
|
|
||||||
minimum_exploit: str = "crash_reproduced"
|
|
||||||
minimum_patch: str = "root_cause_explained"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class StaticAnalysisConfig:
|
class StaticAnalysisConfig:
|
||||||
sarif_paths: tuple[str, ...] = ()
|
sarif_paths: tuple[str, ...] = ()
|
||||||
@@ -160,13 +160,6 @@ class RegressConfig:
|
|||||||
images: tuple[tuple[str, str], ...] = ()
|
images: tuple[tuple[str, str], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class VariantsConfig:
|
|
||||||
# corpus-seeded-mechanisms: structural variant search in MAP (standard/deep only).
|
|
||||||
enabled: bool = True
|
|
||||||
max_mechanisms: int = 500
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ModelLayerConfig:
|
class ModelLayerConfig:
|
||||||
# contributor-scan: the model layer in MAP. Named for the LAYER, not the model: `ModelConfig` is already
|
# contributor-scan: the model layer in MAP. Named for the LAYER, not the model: `ModelConfig` is already
|
||||||
@@ -214,8 +207,6 @@ class ResolvedConfig:
|
|||||||
models: ModelConfig = ModelConfig()
|
models: ModelConfig = ModelConfig()
|
||||||
embeddings: EmbeddingConfig = EmbeddingConfig()
|
embeddings: EmbeddingConfig = EmbeddingConfig()
|
||||||
sandbox: SandboxConfig = SandboxConfig()
|
sandbox: SandboxConfig = SandboxConfig()
|
||||||
dynamic: DynamicConfig = DynamicConfig()
|
|
||||||
evidence: EvidenceConfig = EvidenceConfig()
|
|
||||||
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
|
static_analysis: StaticAnalysisConfig = StaticAnalysisConfig()
|
||||||
score: ScoreConfig = ScoreConfig()
|
score: ScoreConfig = ScoreConfig()
|
||||||
ruleset: RulesetConfig = RulesetConfig()
|
ruleset: RulesetConfig = RulesetConfig()
|
||||||
@@ -223,7 +214,6 @@ class ResolvedConfig:
|
|||||||
hardening: HardeningConfig = HardeningConfig()
|
hardening: HardeningConfig = HardeningConfig()
|
||||||
complexity: ComplexityConfig = ComplexityConfig()
|
complexity: ComplexityConfig = ComplexityConfig()
|
||||||
regress: RegressConfig = RegressConfig()
|
regress: RegressConfig = RegressConfig()
|
||||||
variants: VariantsConfig = VariantsConfig()
|
|
||||||
obligations: ObligationsConfig = ObligationsConfig()
|
obligations: ObligationsConfig = ObligationsConfig()
|
||||||
model: ModelLayerConfig = ModelLayerConfig()
|
model: ModelLayerConfig = ModelLayerConfig()
|
||||||
push: PushConfig = PushConfig()
|
push: PushConfig = PushConfig()
|
||||||
@@ -243,8 +233,6 @@ def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> Reso
|
|||||||
models=_load_models(data.get("models", {})),
|
models=_load_models(data.get("models", {})),
|
||||||
embeddings=_load_embeddings(data.get("embeddings", {})),
|
embeddings=_load_embeddings(data.get("embeddings", {})),
|
||||||
sandbox=_load_sandbox(data.get("sandbox", {})),
|
sandbox=_load_sandbox(data.get("sandbox", {})),
|
||||||
dynamic=_load_dynamic(data.get("dynamic", {})),
|
|
||||||
evidence=_load_evidence(data.get("evidence", {})),
|
|
||||||
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", {})),
|
||||||
ruleset=_load_ruleset_config(data.get("ruleset", {})),
|
ruleset=_load_ruleset_config(data.get("ruleset", {})),
|
||||||
@@ -252,7 +240,6 @@ def load_config(config_path: Path | None = None, *, dotenv: bool = True) -> Reso
|
|||||||
hardening=_load_hardening(data.get("hardening", {})),
|
hardening=_load_hardening(data.get("hardening", {})),
|
||||||
complexity=_load_complexity(data.get("complexity", {})),
|
complexity=_load_complexity(data.get("complexity", {})),
|
||||||
regress=_load_regress(data.get("regress", {})),
|
regress=_load_regress(data.get("regress", {})),
|
||||||
variants=_load_variants(data.get("variants", {})),
|
|
||||||
obligations=_load_obligations(data.get("obligations", {})),
|
obligations=_load_obligations(data.get("obligations", {})),
|
||||||
model=_load_model(data.get("model", {})),
|
model=_load_model(data.get("model", {})),
|
||||||
push=PushConfig.from_payload(data.get("push", {})),
|
push=PushConfig.from_payload(data.get("push", {})),
|
||||||
@@ -283,7 +270,6 @@ def _load_models(value: object) -> ModelConfig:
|
|||||||
return ModelConfig(
|
return ModelConfig(
|
||||||
ranker=_string(data.get("ranker")),
|
ranker=_string(data.get("ranker")),
|
||||||
hunter=_string(data.get("hunter")),
|
hunter=_string(data.get("hunter")),
|
||||||
patcher=_string(data.get("patcher")),
|
|
||||||
judge=_string(data.get("judge")),
|
judge=_string(data.get("judge")),
|
||||||
chat_base_url=_string(data.get("chat_base_url")),
|
chat_base_url=_string(data.get("chat_base_url")),
|
||||||
chat_api_key_env=_string(data.get("chat_api_key_env")),
|
chat_api_key_env=_string(data.get("chat_api_key_env")),
|
||||||
@@ -310,23 +296,6 @@ def _load_sandbox(value: object) -> SandboxConfig:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_dynamic(value: object) -> DynamicConfig:
|
|
||||||
data = _section(value)
|
|
||||||
scope = data.get("network_scope", [])
|
|
||||||
if not isinstance(scope, list):
|
|
||||||
scope = []
|
|
||||||
return DynamicConfig(enabled=bool(data.get("enabled", False)), network_scope=tuple(str(item) for item in scope))
|
|
||||||
|
|
||||||
|
|
||||||
def _load_evidence(value: object) -> EvidenceConfig:
|
|
||||||
data = _section(value)
|
|
||||||
return EvidenceConfig(
|
|
||||||
minimum_report_verified=str(data.get("minimum_report_verified", "static_corroboration")),
|
|
||||||
minimum_exploit=str(data.get("minimum_exploit", "crash_reproduced")),
|
|
||||||
minimum_patch=str(data.get("minimum_patch", "root_cause_explained")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _int_value(value: object, default: int) -> int:
|
def _int_value(value: object, default: int) -> int:
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
return default
|
return default
|
||||||
@@ -416,14 +385,6 @@ def _float_value(value: object, default: float) -> float:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _load_variants(value: object) -> VariantsConfig:
|
|
||||||
data = _section(value)
|
|
||||||
return VariantsConfig(
|
|
||||||
enabled=bool(data.get("enabled", True)),
|
|
||||||
max_mechanisms=_int_value(data.get("max_mechanisms"), 500),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _load_model(value: object) -> ModelLayerConfig:
|
def _load_model(value: object) -> ModelLayerConfig:
|
||||||
data = _section(value)
|
data = _section(value)
|
||||||
return ModelLayerConfig(
|
return ModelLayerConfig(
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ def write_harness_config(
|
|||||||
"ranker": config.models.ranker,
|
"ranker": config.models.ranker,
|
||||||
"hunter": config.models.hunter,
|
"hunter": config.models.hunter,
|
||||||
"verifier": None, # schema kept byte for byte; LLM verification runs on the plane
|
"verifier": None, # schema kept byte for byte; LLM verification runs on the plane
|
||||||
"patcher": config.models.patcher,
|
"patcher": None, # schema kept; `[models] patcher` was retired 2026-10-02 (never read)
|
||||||
},
|
},
|
||||||
processor_versions={processor.spec.name: processor.spec.version for processor in processors},
|
processor_versions={processor.spec.name: processor.spec.version for processor in processors},
|
||||||
prompt_hashes={name: _hash_text(prompt) for name, prompt in sorted(prompts.items())},
|
prompt_hashes={name: _hash_text(prompt) for name, prompt in sorted(prompts.items())},
|
||||||
|
|||||||
+30
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
@@ -17,9 +18,6 @@ def test_load_config_reads_toml(tmp_path: Path) -> None:
|
|||||||
'ranker = "openrouter/test-ranker"',
|
'ranker = "openrouter/test-ranker"',
|
||||||
"[sandbox]",
|
"[sandbox]",
|
||||||
"memory_mb = 1024",
|
"memory_mb = 1024",
|
||||||
"[dynamic]",
|
|
||||||
"enabled = true",
|
|
||||||
'network_scope = ["127.0.0.1:8080"]',
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -29,8 +27,6 @@ def test_load_config_reads_toml(tmp_path: Path) -> None:
|
|||||||
assert config.models.ranker == "openrouter/test-ranker"
|
assert config.models.ranker == "openrouter/test-ranker"
|
||||||
assert config.embeddings.store == "json-local"
|
assert config.embeddings.store == "json-local"
|
||||||
assert config.sandbox.memory_mb == 1024
|
assert config.sandbox.memory_mb == 1024
|
||||||
assert config.dynamic.enabled is True
|
|
||||||
assert config.dynamic.network_scope == ("127.0.0.1:8080",)
|
|
||||||
|
|
||||||
|
|
||||||
def test_retired_agentic_section_loads_with_one_warning(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
def test_retired_agentic_section_loads_with_one_warning(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
||||||
@@ -67,6 +63,31 @@ def test_retired_llm_keys_fail_naming_the_replacement(tmp_path: Path, body: str,
|
|||||||
assert key in message and replacement in message and "ousast plane run" in message and "Remove the key" in message
|
assert key in message and replacement in message and "ousast plane run" in message and "Remove the key" in message
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("body", "key"),
|
||||||
|
[
|
||||||
|
('[models]\npatcher = "gpt-4o"\n', "`[models] patcher`"),
|
||||||
|
("[dynamic]\nenabled = true\n", "`[dynamic] enabled`"),
|
||||||
|
('[dynamic]\nnetwork_scope = ["127.0.0.1:8080"]\n', "`[dynamic] network_scope`"),
|
||||||
|
('[evidence]\nminimum_report_verified = "static_corroboration"\n', "`[evidence] minimum_report_verified`"),
|
||||||
|
('[evidence]\nminimum_exploit = "crash_reproduced"\n', "`[evidence] minimum_exploit`"),
|
||||||
|
('[evidence]\nminimum_patch = "root_cause_explained"\n', "`[evidence] minimum_patch`"),
|
||||||
|
("[variants]\nenabled = false\n", "`[variants] enabled`"),
|
||||||
|
("[variants]\nmax_mechanisms = 10\n", "`[variants] max_mechanisms`"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unread_keys_fail_naming_why(tmp_path: Path, body: str, key: str) -> None:
|
||||||
|
"""Keys that loaded but were never read are retired (2026-10-02) the same way, each naming its reason."""
|
||||||
|
config_path = tmp_path / "openultrasast.toml"
|
||||||
|
config_path.write_text(body)
|
||||||
|
|
||||||
|
with pytest.raises(RetiredConfigError) as raised:
|
||||||
|
load_config(config_path)
|
||||||
|
|
||||||
|
message = str(raised.value)
|
||||||
|
assert key in message and "retired 2026-10-02 because nothing ever read it" in message and "Remove the key" in message
|
||||||
|
|
||||||
|
|
||||||
def test_the_cli_exits_2_on_a_retired_key(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
def test_the_cli_exits_2_on_a_retired_key(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
from openultrasast.cli import main
|
from openultrasast.cli import main
|
||||||
|
|
||||||
@@ -116,7 +137,10 @@ def test_write_resolved_config_creates_json(tmp_path: Path) -> None:
|
|||||||
write_resolved_config(load_config(None), output)
|
write_resolved_config(load_config(None), output)
|
||||||
|
|
||||||
assert output.exists()
|
assert output.exists()
|
||||||
assert '"minimum_report_verified": "static_corroboration"' in output.read_text()
|
payload = json.loads(output.read_text())
|
||||||
|
assert payload["sandbox"]["memory_mb"] == 2048
|
||||||
|
# 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"]
|
||||||
|
|
||||||
|
|
||||||
def test_complexity_and_regress_defaults_when_section_absent() -> None:
|
def test_complexity_and_regress_defaults_when_section_absent() -> None:
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ def test_project_opencode_skills_have_required_frontmatter() -> None:
|
|||||||
|
|
||||||
assert {path.parent.name for path in skill_files} == {
|
assert {path.parent.name for path in skill_files} == {
|
||||||
"openultrasast-fix-audit",
|
"openultrasast-fix-audit",
|
||||||
"openultrasast-kiro-impl",
|
|
||||||
"openultrasast-scan",
|
"openultrasast-scan",
|
||||||
"openultrasast-triage",
|
"openultrasast-triage",
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user