diff --git a/.coverage b/.coverage deleted file mode 100644 index 95d31f5f..00000000 Binary files a/.coverage and /dev/null differ diff --git a/.gitignore b/.gitignore index 04f37103..cb53f576 100644 --- a/.gitignore +++ b/.gitignore @@ -16,12 +16,16 @@ site/ # Wardline runtime output findings.jsonl .wardline-cache/ -.clarion/ +.clarion # Filigree issue tracker .filigree/ .env -# Filigree-generated agent operator playbooks (local tooling, not project docs) -CLAUDE.md -AGENTS.md +# Filigree/Wardline-generated MCP wiring (local tooling, not a project doc). +# CLAUDE.md and AGENTS.md ARE tracked: they carry curated developer guidance +# (a Filigree-managed block lives within them, but the file is a project doc). .mcp.json + +# Coverage artifacts (regenerated by pytest-cov / make test-cov) +.coverage +coverage.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..7627fb57 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,297 @@ +# AGENTS.md + +This file provides guidance to coding agents (Claude Code, Codex, etc.) when working with code in this repository. + +> The Filigree issue-tracker section below is auto-managed by `filigree init` +> (it lives between `` markers and is regenerated +> in place). Keep Wardline-specific guidance *above* those markers so it +> survives a refresh. Its twin `AGENTS.md` carries the same Wardline developer guidance; the Filigree block below is auto-managed in each. + +> **Audience — developing Wardline.** This file (and `AGENTS.md`) is for people +> and agents changing Wardline itself. End-user "how to *use* Wardline" guidance +> is NOT here — it lives in the `wardline install` instruction block, the +> `wardline-gate` skill (`src/wardline/skills/wardline-gate/SKILL.md`), and the +> docs site. Keep usage guidance out of this file. + +## What Wardline is + +Wardline is a **generic, lightweight semantic-tainting static analyzer for +Python**. It reads source statically (never runs it) and answers one question of +every trust-annotated function: *is the data this function works with as trusted +as it claims?* It is part of the **Loom** suite (Wardline analysis + Clarion code +intelligence + Filigree issue tracking). + +Core design tenets — internalize these before changing behavior: + +- **Zero-dependency base.** The installed package (`pip install wardline`) has + **no runtime dependencies**. Third-party libs live behind optional extras: + `scanner` (pyyaml/jsonschema/click — the CLI), `clarion` (blake3), `docs` + (mkdocs). The LLM judge is dep-free (stdlib `urllib` → OpenRouter). Do not add + an import of an extra's package outside its subpackage, and never make the base + import-time depend on one. +- **Opt-in / fail-closed.** Undecorated code is the "developer-freedom zone": + unknown-trust, no findings. Policy fires only where a developer has declared + trust via decorators. When the engine *cannot prove* something, it records an + honest `UNKNOWN_*` state and emits an observable `WLN-ENGINE-*` FACT rather + than silently passing — a silent skip is a "false-green" and is treated as a + bug. +- **CLI and MCP are identical by construction.** Both call the same + `core/run.py` functions, so a scan via the CLI and via the MCP server produce + the same findings/gate. Don't reintroduce logic into a command that belongs in + `core/`. + +## Dev commands + +The repo uses [uv](https://docs.astral.sh/uv/). Set up with +`uv sync --all-extras --group dev`. A `Makefile` wraps the common flows +(`make help` lists them; `make ci` runs the full local gate). + +```bash +# Tests (network + clarion_e2e are deselected by default via pyproject addopts) +uv run pytest # full suite (~1000 tests) +uv run pytest tests/unit/scanner # one directory +uv run pytest tests/unit/core/test_run.py # one file +uv run pytest -k "py_wl_101" # by name substring +uv run pytest -m network # opt in: live OpenRouter judge e2e (SP5) +WARDLINE_CLARION_BIN=~/clarion/target/release/clarion \ + uv run pytest -m clarion_e2e # opt in: real `clarion serve` round-trip (SP9) + +# Lint / type-check / coverage (CI gates on all three; floor is 90%) +uv run ruff check src tests +uv run ruff format --check src tests # CI enforces formatting +uv run mypy # strict, src/wardline only (see pyproject) +make test-cov # pytest with the 90% coverage floor + +# Run the analyzer on a project +uv run wardline scan PATH --fail-on ERROR # gate: exit 1 if active DEFECT >= ERROR +uv run wardline scan PATH --format sarif # or jsonl (default) +uv run wardline vocab # emit the NG-25 trust-vocabulary descriptor +uv run wardline judge PATH --write # opt-in LLM triage of active DEFECTs +uv run wardline baseline create PATH # snapshot current findings as accepted +uv run wardline mcp # MCP-over-stdio server (JSON-RPC 2.0, no SDK) +uv run wardline install # wire Wardline into an agent project + +# Docs +uv run mkdocs serve # local preview; CI builds --strict +``` + +Tests run under `pytest-randomly` (random order) — order-dependence is a real +failure, not flakiness. + +## Architecture: the taint pipeline + +A scan is a pure function of *(disk + config)*. The flow: + +**`core/discovery.py`** finds analysable files → **`scanner/analyzer.py` +(`WardlineAnalyzer`)** orchestrates the layered taint engine → policy rules run +→ **`core/suppression.py`** classifies each finding (active / baselined / waived +/ judged) → **`core/emit.py`** / `sarif.py` / `filigree_emit.py` serialize. + +The taint engine in `scanner/taint/` is layered (terms used throughout the code +and plans): + +- **L1 — function-level seeding** (`function_level.py`): ask the + `TaintSourceProvider` for each function's declared taint; fall back to + `UNKNOWN_RAW`. The default provider is `DecoratorTaintSourceProvider`, which + reads the three trust decorators from source. +- **L3 — project-scope fixed point** (`project_resolver.py` + `propagation.py`): + build the inter-module call graph (`callgraph.py`), run Tarjan SCC + decomposition, and iterate to a fixed point. A function is only as trusted as + the least-trusted value it returns; trust flows transitively up the call graph + across files. An optional `SummaryCache` memoizes per-module summaries (warm + cache must produce byte-identical findings to cold — enforced by test). +- **L2 — per-variable taint within a body** (`variable_level.py`): walks a + function AST tracking taint per variable through assignments, control-flow + joins, match arms, and call sites. Combines with the **rank-meet + `least_trusted` (weakest-link)** — a branch merge holds one alternative, not a + mixture, so it is *not* `taint_join`'s provenance-clash `MIXED_RAW`. (Full L3 + subsumes the old `minimum_scope` one-hop refinement, which is off the pipeline.) + +Everything is bounded fail-closed: a parse error, too-deep recursion, or missing +source root skips the unit and emits a `Severity.NONE` `WLN-ENGINE-*` FACT so the +under-scan is visible (`UNANALYZED_RULE_IDS` distinguishes genuine under-scans +from benign no-module skips). + +### The trust model + +Eight ordered taint states (`core/taints.py`), most→least trusted: +`INTEGRAL → ASSURED → GUARDED → UNKNOWN_ASSURED → UNKNOWN_GUARDED → EXTERNAL_RAW +→ UNKNOWN_RAW → MIXED_RAW`. Developers declare only four (`INTEGRAL`, `ASSURED`, +`GUARDED`, `EXTERNAL_RAW`) via three decorators in `decorators/trust.py` +(runtime no-op markers Wardline reads statically): +`@external_boundary` (source → `EXTERNAL_RAW`), `@trust_boundary(to_level=...)` +(validator that raises trust), `@trusted(level=...)` (trusted producer). The +`UNKNOWN_*` / `MIXED_RAW` states are engine-inferred and never written. + +### The four policy rules (`scanner/rules/`) + +| Rule | Flags | Gating | +|---|---|---| +| `PY-WL-101` | trusted producer returns data less trusted than declared | declaration-gated (always base severity) | +| `PY-WL-102` | trust boundary with no rejection path (can't say "no") | declaration-gated | +| `PY-WL-103` | broad exception handler in a trusted-tier function | tier-modulated severity | +| `PY-WL-104` | silently-swallowed exception in a trusted-tier function | tier-modulated severity | + +Rules are toggled / re-severitied per project via `wardline.yaml` +(`rules.enable` / `rules.severity`). + +## Package map (`src/wardline/`) + +- **`core/`** — engine-agnostic orchestration and I/O. `run.py` (the shared + `run_scan`/`gate_decision`), `config.py` + `config_schema.py` (jsonschema-validated + `wardline.yaml`), `finding.py` (`Finding`/`Severity`/`Kind`), `taints.py` (the + lattice + `least_trusted`/`taint_join`), `suppression.py`, `baseline.py`, + `waivers.py`, `judged.py`, `judge.py`/`judge_run.py` (LLM triage), `descriptor.py` + (the read-instead-of-import NG-25 vocabulary export), `explain.py`, emitters. +- **`scanner/`** — the AST engine: `analyzer.py`, `index.py` (entity discovery), + `ast_primitives.py`, `taint/` (the L1/L2/L3 layers above), `rules/`. +- **`decorators/`** — the public trust decorators + their `REGISTRY`. +- **`cli/`** — `click` command group (`main.py` → `scan`, `judge`, `mcp`, + `install`, `vocab`, `baseline`); thin wrappers over `core/`. +- **`mcp/`** — dep-free stdlib MCP-over-stdio server (`server.py` + `protocol.py`). + Tools: scan / explain_taint / judge / baseline_create|update / waiver_add. + Findings are NEVER exposed as a resource. MCP confines all paths under root + (`confine_to_root=True`) where the CLI does not. +- **`clarion/`** — opt-in (`[clarion]`) write of per-entity `wardline-taint-1` + facts to a Clarion taint store; HMAC-signed, blake3 freshness gate, fail-soft. +- **`install/`** — `wardline install`: injects a hash-fenced instruction block + into `CLAUDE.md`/`AGENTS.md`, installs the `wardline-gate` skill, merges a + `wardline` entry into `.mcp.json`, records Clarion/Filigree bindings. + +### Error model + +`WardlineError` subclasses (`core/errors.py`) carry tool-actionable failures. +Split by surface: **tool-execution** errors (bad config, missing judge key, +stale fingerprint) surface to the agent as a result payload / CLI exit 2; +**protocol** faults (unknown MCP tool/method/bad args) become JSON-RPC errors. + +## Conventions + +- **No back-compat shims for unreleased specs** — make clean changes. +- Wardline scans **its own source** as a dogfood gate; keep the tree finding-clean + (or baselined) when you touch trust-annotated code. +- The shipped `vocabulary.yaml` is a *derived snapshot* of `descriptor.py`; a + byte-identity test fails if they drift — regenerate, don't hand-edit. +- Findings carry repo-relative paths and stable SHA-256 fingerprints; changing a + fingerprint input silently invalidates baselines/waivers — treat as breaking. +- Specs live in `docs/superpowers/specs/`, plans in `docs/superpowers/plans/`, + prose docs in `docs/` (served via mkdocs). Concepts: `docs/concepts/{model,rules,taint-algebra}.md`. + + +## Filigree Issue Tracker + +`filigree` tracks tasks for this project. Data lives in `.filigree/`. Prefer +the MCP tools (`mcp__filigree__*`) when available; fall back to the `filigree` +CLI otherwise. + +### Workflow + +```bash +# At session start +filigree session-context # ready / in-progress / critical path + +# Pick up the next startable issue (atomic claim + transition into its working status) +filigree start-next-work --assignee +# ...or claim a specific issue +filigree start-work --assignee + +# Do the work, commit, then +filigree close +``` + +Use the atomic claim+transition verbs — `start_work` / `start_next_work` +(MCP) or `start-work` / `start-next-work` (CLI). Do **not** chain +`claim_issue` (MCP) or `filigree claim` (CLI) with a subsequent status +update — the two-step form races against other agents; the combined verb is +atomic. + +**Ready ≠ startable.** The working status is type-specific (tasks → +`in_progress`, features → `building`). Bugs start at `triage`, which has no +single-hop transition into work (`triage → confirmed → fixing`), so a triage +bug is *ready* but not directly *startable*: `start_work` on one returns +`INVALID_TRANSITION` naming the next status, and `start_next_work` skips it. +`get_ready` items carry a `startable` flag (plus a `next_action` hint when +false). Pass `advance=true` (MCP) / `--advance` (CLI) to walk the soft +transitions to the nearest working status automatically. + +### Observations: when (and when not) to use them + +`observe` is a fire-and-forget scratchpad for *incidental* defects — things +you notice *outside the scope of your current task* (a code smell in a +neighbouring file, a stale TODO, a missing test for an edge case you happened +to spot). Notes expire after 14 days unless promoted. Include `file_path` and +`line` when relevant. At session end, skim `list_observations` and either +`dismiss_observation` or `promote_observation` for what has accumulated. + +**You fix bugs in your currently defined scope. You do NOT use observations +to finish work prematurely.** If a defect, gap, or follow-up belongs to your +current task, you own it — handle it as part of that task: fix it now, expand +the task's scope, file a proper issue with a dependency, or surface it to the +user. Filing it as an observation and closing the task is *not* completing +the task; it is shipping known-broken work and hiding the debt in a 14-day +expiring scratchpad. The test is "would I have noticed this even if I weren't +working on this task?" If no, it's task scope, not an observation. + +### Priority scale + +- P0: Critical (drop everything) +- P1: High (do next) +- P2: Medium (default) +- P3: Low +- P4: Backlog + +### Reaching for tools + +MCP tool schemas describe each tool; `filigree --help` and `filigree +--help` are the authoritative CLI reference. You do not need to memorise +either catalogue. The verbs you will reach for most: + +- **Find work:** `get_ready`, `get_blocked`, `list_issues`, `search_issues` +- **Claim work:** `start_work`, `start_next_work` +- **Update:** `add_comment`, `add_label`, `update_issue`, `close_issue` +- **Admin (irreversible):** `delete_issue` (MCP) / `delete-issue` (CLI) — + hard-deletes a terminal issue and its rows; `undo_last` cannot reverse it. +- **Scratchpad:** `observe`, `list_observations`, `promote_observation`, `dismiss_observation` +- **Cross-product entity bindings (ADR-029):** `add_entity_association`, + `remove_entity_association`, `list_entity_associations`, + `list_associations_by_entity`. Used when a sibling tool (e.g. + Clarion) needs to bind a Filigree issue to a function, class, or + module identifier it owns. The `entity_id` is an opaque string + from Filigree's perspective; the consumer (the sibling tool's read + path) does drift detection against the stored + `content_hash_at_attach`. `list_associations_by_entity` is the + reverse-lookup surface — given a Clarion entity ID, return every + Filigree issue bound to it (project isolation is by DB file). Also + reachable over HTTP as + `GET/POST /api/issue/{issue_id}/entity-associations`, + `DELETE /api/issue/{issue_id}/entity-associations?entity_id=…`, + and `GET /api/entity-associations?entity_id=…`. +- **Health:** `get_stats`, `get_metrics`, `get_mcp_status` + +Pass `--actor ` (CLI) so events attribute to your agent identity. It +works in either position — before the verb (`filigree --actor X update …`) or +after it (`filigree update … --actor X`); the post-verb value overrides the +group-level one. + +### Error handling + +Errors return `{error: str, code: ErrorCode, details?: dict}`. Switch on +`code`, not on message text. Codes: `VALIDATION`, `NOT_FOUND`, `CONFLICT`, +`INVALID_TRANSITION`, `PERMISSION`, `NOT_INITIALIZED`, `IO`, +`INVALID_API_URL`, `FILE_REGISTRY_DISPLACED`, `REGISTRY_UNAVAILABLE`, +`CLARION_REGISTRY_VERSION_MISMATCH`, `BRIEFING_BLOCKED`, `STOP_FAILED`, +`SCHEMA_MISMATCH`, `INTERNAL`. + +On `INVALID_TRANSITION`, call `get_valid_transitions` (MCP) or +`filigree transitions ` to see what the workflow allows from here. + +Two failure modes deserve a specific response: + +- **`SCHEMA_MISMATCH`** — the installed `filigree` is older than the project + database. The error message contains upgrade guidance. Surface it to the + user; do not retry. +- **`ForeignDatabaseError`** — filigree found a parent project's database + but no local `.filigree.conf`. Run `filigree init` in the current + directory. Do **not** `cd` upward to a different project unless that was + the actual intent. + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..3a4de1f9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,297 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +> The Filigree issue-tracker section below is auto-managed by `filigree init` +> (it lives between `` markers and is regenerated +> in place). Keep Wardline-specific guidance *above* those markers so it +> survives a refresh. Its twin `AGENTS.md` carries the same Wardline developer guidance; the Filigree block below is auto-managed in each. + +> **Audience — developing Wardline.** This file (and `AGENTS.md`) is for people +> and agents changing Wardline itself. End-user "how to *use* Wardline" guidance +> is NOT here — it lives in the `wardline install` instruction block, the +> `wardline-gate` skill (`src/wardline/skills/wardline-gate/SKILL.md`), and the +> docs site. Keep usage guidance out of this file. + +## What Wardline is + +Wardline is a **generic, lightweight semantic-tainting static analyzer for +Python**. It reads source statically (never runs it) and answers one question of +every trust-annotated function: *is the data this function works with as trusted +as it claims?* It is part of the **Loom** suite (Wardline analysis + Clarion code +intelligence + Filigree issue tracking). + +Core design tenets — internalize these before changing behavior: + +- **Zero-dependency base.** The installed package (`pip install wardline`) has + **no runtime dependencies**. Third-party libs live behind optional extras: + `scanner` (pyyaml/jsonschema/click — the CLI), `clarion` (blake3), `docs` + (mkdocs). The LLM judge is dep-free (stdlib `urllib` → OpenRouter). Do not add + an import of an extra's package outside its subpackage, and never make the base + import-time depend on one. +- **Opt-in / fail-closed.** Undecorated code is the "developer-freedom zone": + unknown-trust, no findings. Policy fires only where a developer has declared + trust via decorators. When the engine *cannot prove* something, it records an + honest `UNKNOWN_*` state and emits an observable `WLN-ENGINE-*` FACT rather + than silently passing — a silent skip is a "false-green" and is treated as a + bug. +- **CLI and MCP are identical by construction.** Both call the same + `core/run.py` functions, so a scan via the CLI and via the MCP server produce + the same findings/gate. Don't reintroduce logic into a command that belongs in + `core/`. + +## Dev commands + +The repo uses [uv](https://docs.astral.sh/uv/). Set up with +`uv sync --all-extras --group dev`. A `Makefile` wraps the common flows +(`make help` lists them; `make ci` runs the full local gate). + +```bash +# Tests (network + clarion_e2e are deselected by default via pyproject addopts) +uv run pytest # full suite (~1000 tests) +uv run pytest tests/unit/scanner # one directory +uv run pytest tests/unit/core/test_run.py # one file +uv run pytest -k "py_wl_101" # by name substring +uv run pytest -m network # opt in: live OpenRouter judge e2e (SP5) +WARDLINE_CLARION_BIN=~/clarion/target/release/clarion \ + uv run pytest -m clarion_e2e # opt in: real `clarion serve` round-trip (SP9) + +# Lint / type-check / coverage (CI gates on all three; floor is 90%) +uv run ruff check src tests +uv run ruff format --check src tests # CI enforces formatting +uv run mypy # strict, src/wardline only (see pyproject) +make test-cov # pytest with the 90% coverage floor + +# Run the analyzer on a project +uv run wardline scan PATH --fail-on ERROR # gate: exit 1 if active DEFECT >= ERROR +uv run wardline scan PATH --format sarif # or jsonl (default) +uv run wardline vocab # emit the NG-25 trust-vocabulary descriptor +uv run wardline judge PATH --write # opt-in LLM triage of active DEFECTs +uv run wardline baseline create PATH # snapshot current findings as accepted +uv run wardline mcp # MCP-over-stdio server (JSON-RPC 2.0, no SDK) +uv run wardline install # wire Wardline into an agent project + +# Docs +uv run mkdocs serve # local preview; CI builds --strict +``` + +Tests run under `pytest-randomly` (random order) — order-dependence is a real +failure, not flakiness. + +## Architecture: the taint pipeline + +A scan is a pure function of *(disk + config)*. The flow: + +**`core/discovery.py`** finds analysable files → **`scanner/analyzer.py` +(`WardlineAnalyzer`)** orchestrates the layered taint engine → policy rules run +→ **`core/suppression.py`** classifies each finding (active / baselined / waived +/ judged) → **`core/emit.py`** / `sarif.py` / `filigree_emit.py` serialize. + +The taint engine in `scanner/taint/` is layered (terms used throughout the code +and plans): + +- **L1 — function-level seeding** (`function_level.py`): ask the + `TaintSourceProvider` for each function's declared taint; fall back to + `UNKNOWN_RAW`. The default provider is `DecoratorTaintSourceProvider`, which + reads the three trust decorators from source. +- **L3 — project-scope fixed point** (`project_resolver.py` + `propagation.py`): + build the inter-module call graph (`callgraph.py`), run Tarjan SCC + decomposition, and iterate to a fixed point. A function is only as trusted as + the least-trusted value it returns; trust flows transitively up the call graph + across files. An optional `SummaryCache` memoizes per-module summaries (warm + cache must produce byte-identical findings to cold — enforced by test). +- **L2 — per-variable taint within a body** (`variable_level.py`): walks a + function AST tracking taint per variable through assignments, control-flow + joins, match arms, and call sites. Combines with the **rank-meet + `least_trusted` (weakest-link)** — a branch merge holds one alternative, not a + mixture, so it is *not* `taint_join`'s provenance-clash `MIXED_RAW`. (Full L3 + subsumes the old `minimum_scope` one-hop refinement, which is off the pipeline.) + +Everything is bounded fail-closed: a parse error, too-deep recursion, or missing +source root skips the unit and emits a `Severity.NONE` `WLN-ENGINE-*` FACT so the +under-scan is visible (`UNANALYZED_RULE_IDS` distinguishes genuine under-scans +from benign no-module skips). + +### The trust model + +Eight ordered taint states (`core/taints.py`), most→least trusted: +`INTEGRAL → ASSURED → GUARDED → UNKNOWN_ASSURED → UNKNOWN_GUARDED → EXTERNAL_RAW +→ UNKNOWN_RAW → MIXED_RAW`. Developers declare only four (`INTEGRAL`, `ASSURED`, +`GUARDED`, `EXTERNAL_RAW`) via three decorators in `decorators/trust.py` +(runtime no-op markers Wardline reads statically): +`@external_boundary` (source → `EXTERNAL_RAW`), `@trust_boundary(to_level=...)` +(validator that raises trust), `@trusted(level=...)` (trusted producer). The +`UNKNOWN_*` / `MIXED_RAW` states are engine-inferred and never written. + +### The four policy rules (`scanner/rules/`) + +| Rule | Flags | Gating | +|---|---|---| +| `PY-WL-101` | trusted producer returns data less trusted than declared | declaration-gated (always base severity) | +| `PY-WL-102` | trust boundary with no rejection path (can't say "no") | declaration-gated | +| `PY-WL-103` | broad exception handler in a trusted-tier function | tier-modulated severity | +| `PY-WL-104` | silently-swallowed exception in a trusted-tier function | tier-modulated severity | + +Rules are toggled / re-severitied per project via `wardline.yaml` +(`rules.enable` / `rules.severity`). + +## Package map (`src/wardline/`) + +- **`core/`** — engine-agnostic orchestration and I/O. `run.py` (the shared + `run_scan`/`gate_decision`), `config.py` + `config_schema.py` (jsonschema-validated + `wardline.yaml`), `finding.py` (`Finding`/`Severity`/`Kind`), `taints.py` (the + lattice + `least_trusted`/`taint_join`), `suppression.py`, `baseline.py`, + `waivers.py`, `judged.py`, `judge.py`/`judge_run.py` (LLM triage), `descriptor.py` + (the read-instead-of-import NG-25 vocabulary export), `explain.py`, emitters. +- **`scanner/`** — the AST engine: `analyzer.py`, `index.py` (entity discovery), + `ast_primitives.py`, `taint/` (the L1/L2/L3 layers above), `rules/`. +- **`decorators/`** — the public trust decorators + their `REGISTRY`. +- **`cli/`** — `click` command group (`main.py` → `scan`, `judge`, `mcp`, + `install`, `vocab`, `baseline`); thin wrappers over `core/`. +- **`mcp/`** — dep-free stdlib MCP-over-stdio server (`server.py` + `protocol.py`). + Tools: scan / explain_taint / judge / baseline_create|update / waiver_add. + Findings are NEVER exposed as a resource. MCP confines all paths under root + (`confine_to_root=True`) where the CLI does not. +- **`clarion/`** — opt-in (`[clarion]`) write of per-entity `wardline-taint-1` + facts to a Clarion taint store; HMAC-signed, blake3 freshness gate, fail-soft. +- **`install/`** — `wardline install`: injects a hash-fenced instruction block + into `CLAUDE.md`/`AGENTS.md`, installs the `wardline-gate` skill, merges a + `wardline` entry into `.mcp.json`, records Clarion/Filigree bindings. + +### Error model + +`WardlineError` subclasses (`core/errors.py`) carry tool-actionable failures. +Split by surface: **tool-execution** errors (bad config, missing judge key, +stale fingerprint) surface to the agent as a result payload / CLI exit 2; +**protocol** faults (unknown MCP tool/method/bad args) become JSON-RPC errors. + +## Conventions + +- **No back-compat shims for unreleased specs** — make clean changes. +- Wardline scans **its own source** as a dogfood gate; keep the tree finding-clean + (or baselined) when you touch trust-annotated code. +- The shipped `vocabulary.yaml` is a *derived snapshot* of `descriptor.py`; a + byte-identity test fails if they drift — regenerate, don't hand-edit. +- Findings carry repo-relative paths and stable SHA-256 fingerprints; changing a + fingerprint input silently invalidates baselines/waivers — treat as breaking. +- Specs live in `docs/superpowers/specs/`, plans in `docs/superpowers/plans/`, + prose docs in `docs/` (served via mkdocs). Concepts: `docs/concepts/{model,rules,taint-algebra}.md`. + + +## Filigree Issue Tracker + +`filigree` tracks tasks for this project. Data lives in `.filigree/`. Prefer +the MCP tools (`mcp__filigree__*`) when available; fall back to the `filigree` +CLI otherwise. + +### Workflow + +```bash +# At session start +filigree session-context # ready / in-progress / critical path + +# Pick up the next startable issue (atomic claim + transition into its working status) +filigree start-next-work --assignee +# ...or claim a specific issue +filigree start-work --assignee + +# Do the work, commit, then +filigree close +``` + +Use the atomic claim+transition verbs — `start_work` / `start_next_work` +(MCP) or `start-work` / `start-next-work` (CLI). Do **not** chain +`claim_issue` (MCP) or `filigree claim` (CLI) with a subsequent status +update — the two-step form races against other agents; the combined verb is +atomic. + +**Ready ≠ startable.** The working status is type-specific (tasks → +`in_progress`, features → `building`). Bugs start at `triage`, which has no +single-hop transition into work (`triage → confirmed → fixing`), so a triage +bug is *ready* but not directly *startable*: `start_work` on one returns +`INVALID_TRANSITION` naming the next status, and `start_next_work` skips it. +`get_ready` items carry a `startable` flag (plus a `next_action` hint when +false). Pass `advance=true` (MCP) / `--advance` (CLI) to walk the soft +transitions to the nearest working status automatically. + +### Observations: when (and when not) to use them + +`observe` is a fire-and-forget scratchpad for *incidental* defects — things +you notice *outside the scope of your current task* (a code smell in a +neighbouring file, a stale TODO, a missing test for an edge case you happened +to spot). Notes expire after 14 days unless promoted. Include `file_path` and +`line` when relevant. At session end, skim `list_observations` and either +`dismiss_observation` or `promote_observation` for what has accumulated. + +**You fix bugs in your currently defined scope. You do NOT use observations +to finish work prematurely.** If a defect, gap, or follow-up belongs to your +current task, you own it — handle it as part of that task: fix it now, expand +the task's scope, file a proper issue with a dependency, or surface it to the +user. Filing it as an observation and closing the task is *not* completing +the task; it is shipping known-broken work and hiding the debt in a 14-day +expiring scratchpad. The test is "would I have noticed this even if I weren't +working on this task?" If no, it's task scope, not an observation. + +### Priority scale + +- P0: Critical (drop everything) +- P1: High (do next) +- P2: Medium (default) +- P3: Low +- P4: Backlog + +### Reaching for tools + +MCP tool schemas describe each tool; `filigree --help` and `filigree +--help` are the authoritative CLI reference. You do not need to memorise +either catalogue. The verbs you will reach for most: + +- **Find work:** `get_ready`, `get_blocked`, `list_issues`, `search_issues` +- **Claim work:** `start_work`, `start_next_work` +- **Update:** `add_comment`, `add_label`, `update_issue`, `close_issue` +- **Admin (irreversible):** `delete_issue` (MCP) / `delete-issue` (CLI) — + hard-deletes a terminal issue and its rows; `undo_last` cannot reverse it. +- **Scratchpad:** `observe`, `list_observations`, `promote_observation`, `dismiss_observation` +- **Cross-product entity bindings (ADR-029):** `add_entity_association`, + `remove_entity_association`, `list_entity_associations`, + `list_associations_by_entity`. Used when a sibling tool (e.g. + Clarion) needs to bind a Filigree issue to a function, class, or + module identifier it owns. The `entity_id` is an opaque string + from Filigree's perspective; the consumer (the sibling tool's read + path) does drift detection against the stored + `content_hash_at_attach`. `list_associations_by_entity` is the + reverse-lookup surface — given a Clarion entity ID, return every + Filigree issue bound to it (project isolation is by DB file). Also + reachable over HTTP as + `GET/POST /api/issue/{issue_id}/entity-associations`, + `DELETE /api/issue/{issue_id}/entity-associations?entity_id=…`, + and `GET /api/entity-associations?entity_id=…`. +- **Health:** `get_stats`, `get_metrics`, `get_mcp_status` + +Pass `--actor ` (CLI) so events attribute to your agent identity. It +works in either position — before the verb (`filigree --actor X update …`) or +after it (`filigree update … --actor X`); the post-verb value overrides the +group-level one. + +### Error handling + +Errors return `{error: str, code: ErrorCode, details?: dict}`. Switch on +`code`, not on message text. Codes: `VALIDATION`, `NOT_FOUND`, `CONFLICT`, +`INVALID_TRANSITION`, `PERMISSION`, `NOT_INITIALIZED`, `IO`, +`INVALID_API_URL`, `FILE_REGISTRY_DISPLACED`, `REGISTRY_UNAVAILABLE`, +`CLARION_REGISTRY_VERSION_MISMATCH`, `BRIEFING_BLOCKED`, `STOP_FAILED`, +`SCHEMA_MISMATCH`, `INTERNAL`. + +On `INVALID_TRANSITION`, call `get_valid_transitions` (MCP) or +`filigree transitions ` to see what the workflow allows from here. + +Two failure modes deserve a specific response: + +- **`SCHEMA_MISMATCH`** — the installed `filigree` is older than the project + database. The error message contains upgrade guidance. Surface it to the + user; do not retry. +- **`ForeignDatabaseError`** — filigree found a parent project's database + but no local `.filigree.conf`. Run `filigree init` in the current + directory. Do **not** `cd` upward to a different project unless that was + the actual intent. + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 47f7e4c5..eba8d5d0 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,7 +1,83 @@ -# Code of Conduct +# Contributor Covenant Code of Conduct -This project adopts the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/), -version 2.1. Be respectful, assume good faith, and keep discussion focused on -the work. +## Our Pledge -Report unacceptable behaviour to **john@wardline.dev**. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at john@wardline.dev. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/superpowers/specs/2026-06-01-wardline-config-trust-declarations-design.md b/docs/superpowers/specs/2026-06-01-wardline-config-trust-declarations-design.md new file mode 100644 index 00000000..f72cd50b --- /dev/null +++ b/docs/superpowers/specs/2026-06-01-wardline-config-trust-declarations-design.md @@ -0,0 +1,462 @@ +# Wardline — config-backed trust declarations (design) + +**Date:** 2026-06-01 +**Status:** Approved design (brainstormed; ready for implementation planning) +**Scope:** Extend Wardline's existing trust-declaration capability so agentic coding +agents can mark code as trust-relevant **without editing source**, using +`wardline.yaml`. + +--- + +## 1. Goal + +Add an out-of-source trust-declaration surface to Wardline so agents can mark +parts of the codebase as: + +- `external_boundary` +- `trust_boundary(to_level=...)` +- `trusted(level=...)` + +and have those declarations change Wardline's normal analysis and gating +behavior exactly the way in-source decorators do today. + +This is a **Wardline-first** feature. It does not depend on Clarion, Filigree, +or any remote store. It must preserve Wardline's standalone, fail-loud config +discipline and reuse the existing analyzer semantics rather than introducing a +second trust model. + +--- + +## 2. Current capability review + +Wardline already has the right extension seam: + +1. `TaintSourceProvider` is explicitly documented as the place declarations come + from, including future declarations from "decorators, annotations, or + config" (`src/wardline/scanner/taint/provider.py`). +2. `WardlineAnalyzer` already injects a provider into function seeding, so a + config-backed declaration source can participate without reshaping the engine + (`src/wardline/scanner/analyzer.py`). +3. `wardline.yaml` is already strict-schema and fail-loud, which is the right + posture for behavior-affecting trust declarations + (`src/wardline/core/config.py`, `src/wardline/core/config_schema.py`). +4. SP2 already deferred a `module_default`-style out-of-source declaration + concept; this design picks up that thread instead of inventing a parallel + subsystem (`docs/superpowers/specs/2026-05-30-wardline-sp2-rules-and-vocabulary-design.md`). + +So the design is not "teach Wardline a new concept." It is "add a new +declaration surface to an existing concept." + +--- + +## 3. Fixed design decisions + +The following decisions were made during brainstorming and are not re-opened in +this document: + +1. **Behavior changed:** Wardline analysis and gating behavior, not just + prompting or issue metadata. +2. **Property family:** trust semantics only in v1 (`external_boundary`, + `trust_boundary`, `trusted`). +3. **Persistence surface:** out-of-source declarations in `wardline.yaml`, not + inserted decorators or a remote/shared store. +4. **Targeting model:** path globs for broad scope; exact qualnames for precise + overrides. +5. **Config location:** inline `wardline.yaml`, not a separate `.wardline/*.yaml` + sidecar. + +Everything below is designed to serve those five constraints. + +--- + +## 4. Design posture + +### 4.1 One trust vocabulary, two declaration surfaces + +Wardline keeps a single trust vocabulary. The config-backed declarations do not +invent new trust kinds or alternative semantics. They map 1:1 onto the existing +decorator shapes: + +| Config `declare` | Equivalent decorator | Emitted `FunctionTaint` | +|---|---|---| +| `external_boundary` | `@external_boundary` | `(EXTERNAL_RAW, EXTERNAL_RAW)` | +| `trust_boundary` + `to_level` | `@trust_boundary(to_level=...)` | `(EXTERNAL_RAW, )` | +| `trusted` + `level` | `@trusted(level=...)` | `(, )` | + +This keeps the explainability story intact: the source of the declaration +changes, but the engine's meaning does not. + +### 4.2 Small, explicit scope model + +V1 supports exactly two targeting layers: + +- **`trust.defaults`** — broad defaults selected by `path_glob` +- **`trust.entities`** — exact function/entity overrides selected by `qualname` + +Notably absent in v1: + +- module globs +- qualname globs or prefixes +- line/span targeting +- non-trust properties + +These are deliberately excluded to keep matching deterministic and reviewable. + +### 4.3 Precedence + +Declaration precedence is: + +**entity config > source decorator > path default > existing fallback** + +Rationale: + +- entity config is the sharpest tool and is the reason this feature exists; +- in-source decorators should beat broad defaults; +- broad defaults should only fill silence, not silently stomp deliberate source + declarations; +- the analyzer's existing fallback behavior stays unchanged when nothing matches. + +--- + +## 5. Architecture + +Four units, each small and independently testable. + +### 5.1 `core/config_schema.py` / `core/config.py` + +Add a new top-level `trust` block to the schema and to `WardlineConfig`. + +The schema stays fail-loud: + +- unknown keys are errors; +- invalid field combinations are errors; +- wrong types are errors. + +`WardlineConfig` exposes the raw `trust` mapping, parallel to the existing +`judge`, `filigree`, and `clarion` raw maps. + +### 5.2 `core/trust_config.py` + +New typed parser for the `trust:` block. Suggested model: + +```python +@dataclass(frozen=True, slots=True) +class PathTrustDefault: + path_glob: str + declaration: FunctionTaint + reason: str + + +@dataclass(frozen=True, slots=True) +class EntityTrustDeclaration: + qualname: str + declaration: FunctionTaint + reason: str + + +@dataclass(frozen=True, slots=True) +class TrustConfig: + defaults: tuple[PathTrustDefault, ...] + entities: Mapping[str, EntityTrustDeclaration] +``` + +This module owns the typed parse and validation rules. `config.load()` remains a +shape loader, following the existing pattern used for judge settings and waivers. + +### 5.3 `scanner/taint/config_provider.py` + +New config-backed providers implementing `TaintSourceProvider`: + +- `ConfigEntityTrustProvider` +- `ConfigPathDefaultProvider` + +Their responsibilities are intentionally split so the precedence ladder stays +explicit. + +- `ConfigEntityTrustProvider.taint_for(entity, ctx)` returns the exact-qualname + declaration for `entity.qualname`, or `None`. +- `ConfigPathDefaultProvider.taint_for(entity, ctx)` returns the last matching + path default for `entity.path`, or `None`. + +Neither provider inspects decorators. That remains the decorator provider's job. + +### 5.4 Composite provider wiring in `scanner/analyzer.py` + +`WardlineAnalyzer` switches from a single default provider to a concrete +`CompositeTaintSourceProvider` with this resolution chain: + +1. `ConfigEntityTrustProvider` +2. `DecoratorTaintSourceProvider` +3. `ConfigPathDefaultProvider` +4. fallback/default behavior + +That order is the contract. The implementation should not collapse it into a +single "do everything" provider, because keeping the chain explicit makes the +precedence easier to test and reason about. + +### 5.5 Provider fingerprint and summary cache invalidation + +This detail is load-bearing. + +The summary cache key already includes `provider.fingerprint()`. Because +`trust:` declarations live outside scanned source, changing `wardline.yaml` +would otherwise leave cached summaries stale. + +Therefore the effective provider fingerprint must include a stable digest of the +parsed `trust:` config. A good scheme is: + +- normalize parsed trust declarations to deterministic JSON/YAML order; +- hash that payload; +- fold it into the provider fingerprint string. + +Changing only `trust:` must invalidate cached summaries. + +--- + +## 6. `wardline.yaml` surface + +V1 shape: + +```yaml +trust: + defaults: + - path_glob: "src/api/**" + declare: external_boundary + reason: "HTTP handlers return untrusted request data" + + - path_glob: "src/app/validation/**" + declare: trust_boundary + to_level: GUARDED + reason: "Validation layer raises trust before sinks" + + entities: + - qualname: "app.storage.save_order" + declare: trusted + level: INTEGRAL + reason: "Trusted commit point" +``` + +### 6.1 Entry rules + +For `trust.defaults`: + +- `path_glob` is required +- `declare` is required +- `reason` is required + +For `trust.entities`: + +- `qualname` is required +- `declare` is required +- `reason` is required + +Parameter rules: + +- `declare: external_boundary` — forbids `level` and `to_level` +- `declare: trust_boundary` — requires `to_level ∈ {GUARDED, ASSURED}` +- `declare: trusted` — requires `level ∈ {INTEGRAL, ASSURED}` + +### 6.2 Matching rules + +- `path_glob` matches the repo-relative POSIX path Wardline already uses in + findings and discovery. +- `qualname` matches Wardline's existing exact qualname composition. +- In `trust.defaults`, **last matching entry wins**. +- In `trust.entities`, duplicate `qualname` entries are a hard config error. + +### 6.3 Why `reason` is required + +The declaration changes analysis behavior. Requiring `reason` makes agent-authored +trust shifts auditable in git and keeps the config aligned with Wardline's +existing "required reason" posture for waivers. + +`reason` is explanatory metadata only; it does not affect matching or the cache +fingerprint beyond being part of the normalized config document. + +--- + +## 7. Analyzer behavior + +This feature changes **L1 seeding**, not the downstream engine model. + +What changes: + +- a function can receive its declared taint from config rather than only from + an AST decorator. + +What does not change: + +- the taint lattice +- L2 variable propagation +- L3 project fixed-point behavior +- rule semantics +- explain output shape +- SARIF / Filigree / Clarion transports + +This is important: a config-backed `trusted` declaration must behave exactly the +same as an in-source `@trusted` decorator for rule firing and gate behavior. + +--- + +## 8. Validation and error handling + +### 8.1 Hard errors + +The following are hard `ConfigError`s and exit `2`: + +- unknown keys under `trust` +- wrong top-level types +- invalid `declare` values +- invalid `level` / `to_level` combinations +- missing required fields +- duplicate `entities.qualname` entries + +### 8.2 Non-fatal visibility for unmatched exact qualnames + +An exact qualname declaration may fail to match during a given run because: + +- the qualname is typo'd, or +- the user scanned only part of the repo. + +Failing the whole run would make partial-path scans brittle, so v1 does **not** +turn unmatched entity declarations into exit-2 errors. + +Instead Wardline emits a `Severity.NONE` config fact when an exact `qualname` +declaration matches nothing in the scanned set. Suggested shape: + +- `rule_id = "WLN-CONFIG-TRUST-NOMATCH"` +- `kind = FACT` +- `properties` include the unmatched `qualname` + +This keeps typos observable without breaking narrow scans. + +### 8.3 Path defaults that match nothing + +Path defaults are broad policy hints. A default that matches nothing in one run +is inert and not an error. + +--- + +## 9. Testing strategy + +### 9.1 Config parse tests + +- valid examples for all three trust declaration kinds +- reject invalid `declare`/`level`/`to_level` combinations +- reject missing required fields +- reject duplicate exact qualnames + +### 9.2 Matching tests + +- `path_glob` matches repo-relative paths correctly +- last-match-wins for overlapping defaults +- exact qualname match selects the entity declaration + +### 9.3 Precedence tests + +Pin the full precedence ladder: + +1. entity config beats decorator +2. decorator beats path default +3. path default beats fallback + +These are the highest-value correctness tests in the design. + +### 9.4 End-to-end behavior tests + +At least three fixtures: + +1. **config-only external boundary** — a function marked only in config as + `external_boundary` behaves like the decorator case and can taint downstream + trusted code. +2. **config-only trusted sink** — a function declared only in config as + `trusted(level=INTEGRAL)` produces the same PY-WL-101 gate behavior as a + decorated trusted function. +3. **mixed config + decorator** — proves precedence is implemented, not assumed. + +### 9.5 Cache invalidation tests + +Changing only the parsed `trust:` config must change the provider fingerprint +and invalidate summary-cache reuse. + +### 9.6 Config fact visibility tests + +- unmatched exact qualname emits `WLN-CONFIG-TRUST-NOMATCH` +- the scan still exits normally unless another real error occurs + +--- + +## 10. Non-goals + +V1 does **not** include: + +- module globs +- qualname globs or prefixes +- line/span declarations +- generic arbitrary properties +- auto-rewriting source to insert decorators +- shared/network-backed declaration stores +- rule-specific behavior knobs beyond trust seeding + +Those may become future features, but they are intentionally excluded here. + +--- + +## 11. Risks and mitigations + +### 11.1 Hidden divergence from source intent + +An out-of-source entity declaration can disagree with source decorators or human +expectation. + +Mitigation: + +- explicit precedence +- required `reason` +- git-visible config +- tests pinning entity-over-decorator behavior so it is a deliberate contract, + not accidental shadowing + +### 11.2 Broad path defaults can over-taint large areas + +A coarse `path_glob` may unexpectedly mark too much code. + +Mitigation: + +- keep defaults broad but path-only +- keep exact-qualname overrides available +- preserve decorator precedence over path defaults + +### 11.3 Silent staleness via the summary cache + +Out-of-source config is invisible to source hashes. + +Mitigation: + +- include normalized `trust:` config in the provider fingerprint +- pin with cache invalidation tests + +--- + +## 12. Forward extension path + +This design intentionally stops at trust declarations, but it creates a clean +pattern for future expansion if Wardline later wants more behavior-affecting +properties. + +The extension path is: + +1. keep **trust** as its own block now; +2. prove the config-backed provider pattern works; +3. if future demand is real, add separate, typed blocks for other behavior + families rather than collapsing everything into a single generic + "properties" bag. + +Examples of future families that could reuse the pattern: + +- generated/third-party/legacy posture +- stricter analysis zones +- rule-scoped policy declarations + +But those are future design exercises. V1 should ship only the trust-backed +declaration surface above. diff --git a/tests/unit/core/test_sarif.py b/tests/unit/core/test_sarif.py index e4ce4615..b80d1186 100644 --- a/tests/unit/core/test_sarif.py +++ b/tests/unit/core/test_sarif.py @@ -121,8 +121,6 @@ def test_metric_findings_excluded_from_sarif() -> None: assert "PY-WL-101" in rule_ids assert "WLN-ENGINE-UNKNOWN-IMPORT" in rule_ids - - res = build_sarif([_f(suppressed=SuppressionState.JUDGED, reason="over-taint floor")])["runs"][0]["results"][0] assert res["suppressions"][0]["kind"] == "external" assert res["suppressions"][0]["justification"] == "over-taint floor"