feat: warpline delta scan (wardline scan --affected)#44
Conversation
- site/src/pages/index.astro: version 1.0.0rc4 -> 1.0.3 - docs/getting-started.md: field is `suppression_state`, not `suppressed` - docs/audits/2026-06-08-comprehensive-audit.md: replace machine-local file:///home/john/... links with repo-relative paths Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_is_type_checking_guard` honored any `if TYPE_CHECKING:` whose name resolved to typing.TYPE_CHECKING via the module-scoped alias map, ignoring closer bindings in the function scope. Two failure modes: - false PY-WL-102: a parameter `TYPE_CHECKING=True` shadowing the import made a runtime-reachable rejection branch read as the dead typing branch, so the rejection looked missing. - false negative: a function-local `from typing import TYPE_CHECKING` was not in the module alias map, so its dead branch wrongly rescued the boundary. Fix: a per-function effective-alias-map correction applied at the scope entry choke point — drop names shadowed by a parameter/local assignment, restore the genuine constant for function-local typing imports. The legitimate module-level `if TYPE_CHECKING:` dead-branch handling is preserved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Producer-agnostic "scan only these entities" capability (Edge 1 / B, wardline-4e08ab9ce6). wardline consumes a warpline reverify-worklist or bare entity list and analyzes only the affected (caller-closure-expanded) files; the full scan stays the gate of record. wardline never calls warpline. Surface: - CLI `wardline scan --affected <file|->`; MCP `scan` gains an `affected` param. - core/delta_scope.py (parse 3 envelope shapes + DeltaScopeReport honesty block), core/delta_resolve.py (SEI + qualname-fallback resolution, caller-closure expansion, finding filter), run.py scoping wiring, sei_resolution.py python:method: locator fix, SARIF run.properties channel. - `warpline_e2e` live oracle + marker + ci.yml matrix row + meta-guard. - Hermetic golden + INV-1..INV-5 invariant tests. Security/soundness (INV-4 / THREAT-001): the delta finding filter narrows only the displayed findings, never the gate population — an attacker-influenceable scope cannot hide an in-analyzed-file finding from the gate. The trust- suppressions path is closed by decoupling gate suppression posture from the gate_findings sentinel (ScanResult.gate_honors_suppressions) and materializing the unfiltered analyzed population as the gate population. Review fixes folded in: - MCP delta emit forces mark_unseen=False (INV-5; was CLI-only). - --affected payload byte cap enforced before unbounded read()/json.loads(). - MCP fail_on/where.* schemas gain case-insensitive patterns (reject BOGUS). - CHANGELOG/doc claim corrected: the gate covers the analyzed-file population; a delta --fail-on pass is advisory, the full scan is the gate of record. - cli.md reconciled with `scan --help`; glossary anchors re-bound. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A delta scan analyzes only the scoped (caller-closure-expanded) subset of the tree, so `--affected --fail-on ERROR` could exit 0 while a discovered-but- unanalyzed file held an ERROR — an unearned green. Documenting it as "advisory" (prior commit) did not fix the behavior: the process exit still read green. A trust gate must never emit a green it did not earn, and a gate over a partial scope cannot be earned. So refuse the combination at both surfaces: - CLI: `--affected` + `--fail-on` -> WardlineError -> exit 2. - MCP: `affected` + `fail_on` -> ToolError. Both messages point to `--new-since` (the authoritative change-scoped gate: full analysis, gates only the changed subset) or a full scan for the gate of record. Mirrors the existing `--affected` <-> `--new-since` rejection. The engine (run_scan + gate_decision) is unchanged and still accepts an affected scope with a threshold — the INV-4 guarantee that the gate population is never narrowed (even under trust_suppressions) remains enforced and tested at the engine level (test_run_affected.py, test_affected_invariants.py). The rejection is a surface policy; the MCP trust-suppressions surface test is replaced by a rejection test since the combination is no longer reachable. Help text, CHANGELOG, and cli.md reframed: --affected is advisory, not a gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2e5167c589
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| config_path=config_path, | ||
| cache_dir=cache_dir, | ||
| new_since=new_since, | ||
| affected=affected, |
There was a problem hiding this comment.
Reject delta scopes for legis artifacts
When --affected is combined with --format legis, this call builds a delta ScanResult and the later build_legis_artifact() serializes it without any delta scope block; _scan_scope() also writes result.scanned_paths, which this feature intentionally leaves as the full discovery list even though only scope.files_analyzed were analyzed. In that context a signed legis artifact can claim the full set of scanned paths while carrying only the delta/analyzed population, so this path should be rejected or taught to encode the actual delta scope before emitting a governance artifact.
Useful? React with 👍 / 👎.
| # would go unseen — exit 0 would be an unearned green). Refuse to gate it. Use | ||
| # --new-since for an authoritative change-scoped gate (full analysis, gates the | ||
| # changed subset), or a full scan for the gate of record. | ||
| if fail_on is not None: |
There was a problem hiding this comment.
Reject unanalyzed gates for affected scans
This guard blocks --affected with the severity gate but still allows --affected --fail-on-unanalyzed. In delta mode only the resolved subset is analyzed and summary.unanalyzed is computed after the delta display filter, so parse/skipped/missing-root diagnostics outside affected_files are absent and this gate can exit 0 even though a full --fail-on-unanalyzed scan would fail. Treat this gate like --fail-on and reject the combination.
Useful? React with 👍 / 👎.
| if trust_suppressions and gate_findings is None: | ||
| gate_findings = list(findings) | ||
| gate_honors_suppressions = True | ||
| findings = filter_to_affected(findings, affected_qualnames, affected_files) |
There was a problem hiding this comment.
Preserve full findings before Loomweave writes
When a delta scan analyzes a file that also contains out-of-scope entities, this filter removes their findings from result.findings, but the CLI/MCP Loomweave write path still passes that same ScanResult into build_taint_facts(). With a configured Loomweave URL, per-entity fact replacement can therefore overwrite facts for co-located or caller-closure entities with an empty/partial findings list; skip Loomweave writes for scope.mode == "delta" or build facts from an unfiltered finding population.
Useful? React with 👍 / 👎.
| # case-insensitive (runtime lowercases) so a plain upper-case enum would wrongly reject "error". | ||
| _FAIL_ON_INPUT_SCHEMA = { | ||
| "type": "string", | ||
| "pattern": "(?i)^(critical|error|warn|info)$", |
There was a problem hiding this comment.
Use JSON-Schema-compatible regexes
These input schemas are advertised to MCP clients as JSON Schema, whose pattern values are commonly compiled as ECMA/JavaScript regular expressions; the new inline flag form (?i) is invalid there (for example, new RegExp("(?i)^(critical|error|warn|info)$") throws). Clients that validate the advertised schema can reject or fail to load the tools even though the server-side Python validator accepts it; use portable character classes or an anyOf/enum shape instead.
Useful? React with 👍 / 👎.
| # exactly that, so the cast bridges the concrete-Entity annotation without building | ||
| # real AST-backed Entity objects in this taint-free pass. | ||
| affected_qualnames = get_affected_entities( | ||
| set(base_files), cast("Mapping[str, Entity]", entity_map), index.project_edges |
There was a problem hiding this comment.
Seed caller closure with affected qualnames
When the resolved file contains both the requested entity and unrelated entities, seeding get_affected_entities() with base_files marks every entity in those files as affected before walking reverse callers. For example, a worklist naming a.alpha also pulls in callers of unrelated a.gamma just because both live in a.py, so delta scans can analyze and report context for files outside the requested caller closure; seed the BFS from the resolved affected qualnames instead of all entities in the same files.
Useful? React with 👍 / 👎.
| assert should_fail_live_oracle_skip(["rust_e2e"], "skipped") is False | ||
|
|
||
|
|
||
| @pytest.mark.warpline_e2e |
There was a problem hiding this comment.
Remove live marker from skip-only unit test
The live-oracles workflow now runs pytest -m warpline_e2e with WARDLINE_LIVE_ORACLE_REQUIRED=1, and tests/conftest.py turns any skip on a live-oracle marker into a failure. This unit test is selected by the same marker and always calls pytest.skip(), so the scheduled/manual Warpline oracle job fails even when the real test_warpline_e2e.py round-trip would pass; keep the marker-registration assertion unmarked or avoid the unconditional skip under required mode.
Useful? React with 👍 / 👎.
Summary
Producer-agnostic "scan only these entities" delta capability (Edge 1 / B,
wardline-4e08ab9ce6). wardline consumes a warplinereverify_worklist.v1(or a bare entity list) and analyzes only the affected, caller-closure-expanded files — the agent inner-loop fast path. wardline never calls warpline; it consumes a scope. The full scan stays the gate of record.Spec:
docs/superpowers/specs/2026-06-18-warpline-delta-scan-design.md· Plan:docs/superpowers/plans/2026-06-18-warpline-delta-scan.mdSurface
wardline scan --affected <file|->(file or stdin).scangains anaffectedparam (inline worklist/entity-list or a path under root).scopeblock (mode, gate authority, file counts, unresolved/fell-back/stale-SEI accounting, boundary caveat) on every output format (agent-summary JSON, SARIFrun.properties, MCP structured content).warpline_e2elive oracle + CI matrix row + meta-guard.Advisory, not a gate
A delta scan analyzes only the scoped subset, so it cannot certify files it never analyzed.
--affectedis therefore rejected together with--fail-on(CLI exit 2 / MCPToolError) — use--new-sincefor an authoritative change-scoped gate (full analysis, gates the changed subset) or a full scan for the gate of record. An empty / all-unresolvable scope falls back to a full scan.Security & soundness (INV-1..INV-5)
--trust-suppressionspath is closed by decoupling gate suppression posture from thegate_findingssentinel and materializing the unfiltered analyzed population as the gate population.mark_unseen=Falseso a scoped run never poisons Filigree reconciliation.--affectedpayload byte cap enforced before unboundedread()/json.loads().fail_on/where.*schemas gained case-insensitive patterns (rejectBOGUS/urgent).Also in this branch
TYPE_CHECKINGreachability made scope-aware (independent pre-existing engine bug — falsePY-WL-102on aTYPE_CHECKINGparam rebind + false-negative on a function-local typing import).suppression_statefield name, machine-localfile://links); honest delta-gate framing across CHANGELOG /cli.md/ glossary.Verification
ruff format --checkclean;mypy --strictsuccess (510 files); dogfoodwardline scan src/ --fail-on ERRORexit 0.Follow-on (not in this PR)
wardline-c0563eee74, blocked_by this): published drift-checkedwarpline.reverify_worklist.v1contract + the stale-vendored-golden gap._relpathhelper,SeiResolver._clientaccess,entities_requestedpre-drop count,fail_on_unanalyzeddelta-scope doc note.🤖 Generated with Claude Code