Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Breaking**: `inspect` no longer accepts `--check-level`; the defect check
always runs at the deep L3 level
- **Breaking**: tool schema uploads now emit the provider-neutral flat format
`{name, description, parameters}` in both upload paths (push and inspect).
The OpenAI `{type, function}` wrapper is stripped during packing; the
Expand Down
18 changes: 2 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A command-line tool for static defect inspection of AI logical artifacts (System

## Features

- **Multi-level inspection depth** — Choose between L1 (fast), L2 (standard), or L3 (deep) check levels
- **Deep inspection** — Runs a comprehensive L3 defect check on every artifact by default
- **Multiple artifact types** — Inspect System Prompts, Skills, and Tool Schemas
- **CI/CD friendly** — Non-interactive runs, configuration via environment variables, dated markdown reports for pipeline artifacts
- **Configurable rules** — Customize inspection behavior via YAML configuration
Expand Down Expand Up @@ -100,15 +100,9 @@ skills:
### 3. Run Inspection

```bash
# Standard inspection (L2 depth)
# Standard inspection (deep L3 defect check)
sanityops-cli inspect

# Fast inspection (L1 depth)
sanityops-cli inspect --check-level L1

# Deep inspection (L3 depth)
sanityops-cli inspect --check-level L3

# Skip defect check, only analyze artifacts
sanityops-cli inspect --skip-defect-check

Expand Down Expand Up @@ -203,14 +197,6 @@ CLI flags (`--timeout`, `--token-budget`) override these values when explicitly

All CLI operations are logged to `~/.sanityops/logs/sanityops-cli-<YYYY-MM-DD>.log`. Logs include timestamps, log levels, and redacted API keys for security.

### Check Levels

| Level | Description | Use Case |
|-------|-------------|----------|
| L1 | Fast | Quick validation during development |
| L2 | Standard | Default, balanced depth for regular use |
| L3 | Deep | Comprehensive analysis for production releases |

## Documentation

Full documentation is available at [sanityops.org](https://sanityops.org).
Expand Down
16 changes: 5 additions & 11 deletions src/sanityops_cli/commands/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
#: URL users attach log files to when reporting issues.
ISSUE_URL = "https://github.com/sanityops-org/sanityops-cli/issues"

#: Defect check depth used for every inspection run (fixed, not user-configurable).
DEFAULT_CHECK_LEVEL = "L3"


def _load_agent_config(config_path: Path) -> dict:
"""Read the optional `agent:` section from inspect_config.yaml.
Expand Down Expand Up @@ -479,11 +482,6 @@ def inspect(
help="Path to inspect_config.yaml. If omitted, looks for .sanityops/inspect_config.yaml in cwd.",
),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose output"),
check_level: str = typer.Option(
"L2",
"--check-level",
help="Defect check level: L1 (fast), L2 (standard), L3 (deep).",
),
skip_defect_check: bool = typer.Option(
False,
"--skip-defect-check",
Expand Down Expand Up @@ -516,10 +514,6 @@ def inspect(
if ctx.invoked_subcommand is not None:
return

if check_level not in {"L1", "L2", "L3"}:
console.print(f"[red]✗ Invalid check level: {escape(check_level)} (must be L1/L2/L3)[/red]")
raise typer.Exit(code=EXIT_FAILURE)

logger = Logger()
tracker = ProgressTracker(console, logger, verbose=verbose)

Expand Down Expand Up @@ -674,7 +668,7 @@ def inspect(

async def run_check():
checker = DefectChecker(llm_config)
return await checker.check(result, check_level=check_level)
return await checker.check(result, check_level=DEFAULT_CHECK_LEVEL)

try:
with tracker.step("Running defect check..."):
Expand All @@ -693,7 +687,7 @@ async def run_check():
response,
report_dir,
project_id=project_id,
check_level=check_level,
check_level=DEFAULT_CHECK_LEVEL,
qc_result=qc_result,
)
except OSError as e:
Expand Down
2 changes: 1 addition & 1 deletion src/sanityops_cli/defect_checker/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__(self, llm_config: dict[str, str]):
async def check(
self,
findings: FindingsResult,
check_level: str = "L2",
check_level: str = "L3",
) -> dict:
"""Run defect check and return the raw SDK response dict."""
tools = self._convert_tools(findings.tools)
Expand Down
2 changes: 1 addition & 1 deletion src/sanityops_cli/defect_checker/markdown_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ def save_markdown_report(
output_dir: Path,
*,
project_id: str | None = None,
check_level: str = "L2",
check_level: str = "L3",
qc_result: QCInspectionResult | None = None,
) -> Path:
"""Persist a full markdown report and return the written file path.
Expand Down
67 changes: 67 additions & 0 deletions tests/unit/commands/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,70 @@ def analyze_files_sync(self, prompts, tools, skills):
i for i, line in enumerate(lines) if f"sanityops-cli v{__version__}" in line
)
assert "Project ID: not set" in lines[banner_line_idx + 1]


def test_inspect_check_level_flag_removed(monkeypatch, tmp_path):
"""--check-level is no longer an accepted option (fixed at L3)."""
monkeypatch.setenv("HOME", str(tmp_path))
skill_file = tmp_path / "skill.md"
skill_file.write_text("---\nname: s\ndescription: d\n---\n# X\n")
cfg = tmp_path / "inspect_config.yaml"
cfg.write_text(
"project:\n id: 00000000-0000-0000-0000-000000000000\n"
f"skills:\n - file: {skill_file}\n"
)

class FakeAgent:
def analyze_files_sync(self, prompts, tools, skills):
return _findings()

monkeypatch.setattr(
"sanityops_cli.commands.inspect.ScannerAgent",
lambda *a, **k: FakeAgent(),
)

result = runner.invoke(app, ["inspect", "--config", str(cfg), "--check-level", "L1"])
assert result.exit_code != 0
combined = result.stdout + (result.stderr or "")
assert "no such option" in combined.lower()


def test_inspect_runs_defect_check_at_l3(monkeypatch, tmp_path):
"""The defect check always runs at the fixed L3 level and reports it."""
monkeypatch.setenv("HOME", str(tmp_path))
skill_file = tmp_path / "skill.md"
skill_file.write_text("---\nname: s\ndescription: d\n---\n# X\n")
cfg = tmp_path / "inspect_config.yaml"
cfg.write_text(
"project:\n id: 00000000-0000-0000-0000-000000000000\n"
f"skills:\n - file: {skill_file}\n"
)

class FakeAgent:
def analyze_files_sync(self, prompts, tools, skills):
return _findings()

monkeypatch.setattr(
"sanityops_cli.commands.inspect.ScannerAgent",
lambda *a, **k: FakeAgent(),
)

captured = {}

async def fake_check(self, findings, check_level="L2"):
captured["check_level"] = check_level
return {
"status": "completed",
"summary": {"total_defects": 0, "p0_count": 0, "p1_count": 0, "p2_count": 0, "gate_result": "PASS"},
"results": [],
"errors": [],
"metadata": {},
}

monkeypatch.setattr(
"sanityops_cli.defect_checker.checker.DefectChecker.check", fake_check
)

result = runner.invoke(app, ["inspect", "--config", str(cfg)])
assert result.exit_code == 0
assert captured["check_level"] == "L3"
4 changes: 2 additions & 2 deletions tests/unit/defect_checker/test_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,12 @@ async def fake_check(tools, prompts, skills, **kwargs):
"llm_base_url": "",
}
)
await checker.check(result, check_level="L2")
await checker.check(result, check_level="L3")

assert captured["skills"] == [{"id": str(skill_dir), "name": "s", "content": ""}]
assert captured["tools"] == []
assert captured["prompts"] == []
assert captured["kwargs"]["check_level"] == "L2"
assert captured["kwargs"]["check_level"] == "L3"
assert captured["kwargs"]["llm_provider"] == "anthropic"
assert captured["kwargs"]["llm_api_key"] == "k"
assert captured["kwargs"]["llm_model_id"] == "m"
2 changes: 1 addition & 1 deletion tests/unit/defect_checker/test_markdown_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def test_report_contains_summary_table(self, tmp_path):
"gate_result": "FAIL",
},
)
path = save_markdown_report(response, tmp_path, check_level="L2")
path = save_markdown_report(response, tmp_path, check_level="L3")
content = path.read_text(encoding="utf-8")
assert "## Summary" in content
assert "| Total defects | 5 |" in content
Expand Down
Loading