Skip to content

feat: warpline delta scan (wardline scan --affected)#44

Merged
tachyon-beep merged 4 commits into
mainfrom
feat/warpline-delta-scan
Jun 18, 2026
Merged

feat: warpline delta scan (wardline scan --affected)#44
tachyon-beep merged 4 commits into
mainfrom
feat/warpline-delta-scan

Conversation

@tachyon-beep

Copy link
Copy Markdown
Collaborator

Summary

Producer-agnostic "scan only these entities" delta capability (Edge 1 / B, wardline-4e08ab9ce6). wardline consumes a warpline reverify_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.md

Surface

  • CLI: wardline scan --affected <file|-> (file or stdin).
  • MCP: scan gains an affected param (inline worklist/entity-list or a path under root).
  • Honesty scope block (mode, gate authority, file counts, unresolved/fell-back/stale-SEI accounting, boundary caveat) on every output format (agent-summary JSON, SARIF run.properties, MCP structured content).
  • warpline_e2e live 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. --affected is therefore rejected together with --fail-on (CLI exit 2 / MCP ToolError) — use --new-since for 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)

  • 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 and materializing the unfiltered analyzed population as the gate population.
  • INV-5: delta emits on both CLI and MCP force mark_unseen=False so a scoped run never poisons Filigree reconciliation.
  • DoS: --affected payload byte cap enforced before unbounded read()/json.loads().
  • MCP fail_on/where.* schemas gained case-insensitive patterns (reject BOGUS/urgent).

Also in this branch

  • fix: TYPE_CHECKING reachability made scope-aware (independent pre-existing engine bug — false PY-WL-102 on a TYPE_CHECKING param rebind + false-negative on a function-local typing import).
  • docs: doc/site rot (version, suppression_state field name, machine-local file:// links); honest delta-gate framing across CHANGELOG / cli.md / glossary.

Verification

  • Full suite 4186 passed / 18 deselected; ruff + ruff format --check clean; mypy --strict success (510 files); dogfood wardline scan src/ --fail-on ERROR exit 0.
  • Built and hardened via multi-agent review; one blocking forge-green defect (INV-4 under trust-suppressions) and the advisory-exit honesty gap were both caught and fixed before this PR.

Follow-on (not in this PR)

  • Spec A (wardline-c0563eee74, blocked_by this): published drift-checked warpline.reverify_worklist.v1 contract + the stale-vendored-golden gap.
  • Four non-blocking review nits: shared _relpath helper, SeiResolver._client access, entities_requested pre-drop count, fail_on_unanalyzed delta-scope doc note.

🤖 Generated with Claude Code

John Morrissey and others added 4 commits June 18, 2026 17:30
- 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>
@tachyon-beep
tachyon-beep merged commit 17cbcb9 into main Jun 18, 2026
8 checks passed
@tachyon-beep
tachyon-beep deleted the feat/warpline-delta-scan branch June 18, 2026 08:07

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/wardline/cli/scan.py
config_path=config_path,
cache_dir=cache_dir,
new_since=new_since,
affected=affected,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread src/wardline/cli/scan.py
# 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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/wardline/core/run.py
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)$",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant