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
26 changes: 23 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [1.3.0] - 2026-07-03

### Added
- **Concurrent-writer detection**: the scan now stat-snapshots the discovered file
inventory at discovery and re-checks it after analysis. If any scanned file was
Expand Down Expand Up @@ -60,13 +62,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **A configured gate over zero scanned files is `NOT_EVALUATED`, not a vacuous
`PASSED`** (`no_files_scanned`): an empty-but-existing source root or an exclude-all
config no longer reads as an authoritative green.
- **Partial federation mutations are no longer reported as clean failures on the human
surfaces** (wardline-7924d67d3b). A mid-emit failure after chunks landed used to
print `could not reach Filigree …; findings written locally only` (CLI) and persist
`filigree unreachable` as the scan-job terminal `error` — flatly false once Filigree
had ingested earlier chunks and run their mark_unseen sweeps. The CLI unreachable
branch now renders the partial truth (chunks landed, created/updated counts, findings
that did not land, producer warnings) on every soft-failure rung — auth, server error,
and transport alike — and the transport rung says `connection dropped mid-emit`
instead of claiming first-contact unreachability; `filigree_disabled_reason` appends
the landed-chunk count on mid-batch failures so the scan-job `error` field and
agent-summary `disabled_reason` agree with the counts beside them. Likewise a partial
Loomweave taint-fact write (a later chunk hit an outage/403 after earlier chunks
committed) now reports `taint store partially written … N taint fact(s) committed
before the failure` instead of the flat `taint store not written`.

### Changed
- **Filigree emit accounting is honest about mid-batch failures**: `EmitResult` now
carries the landed chunk count, per-finding `partial` failures, and a
"connection dropped mid-emit: K of N chunk(s) landed" warning when the connection
drops after chunks landed (machine envelope; the CLI text surface is tracked in
wardline-7924d67d3b).
drops after chunks landed (machine envelope; the CLI and scan-job text surfaces
ship in this release — see Fixed, wardline-7924d67d3b).
- **Loomweave resolve stops on auth rejection**: a hinted 401/403 halts the batch and
is carried as `ResolveResult.auth_status` so surfaces can steer the operator to the
token rather than a phantom unresolved entity (consumer threading tracked in
Expand Down Expand Up @@ -1556,7 +1572,11 @@ for Python — enterprise-class trust-boundary analysis at small-team weight.
- **Packaging** — MIT-licensed; optional extras `scanner` (config + CLI) and
`weft` (HTTP integrations).

[Unreleased]: https://github.com/foundryside-dev/wardline/compare/v1.0.6...HEAD
[Unreleased]: https://github.com/foundryside-dev/wardline/compare/v1.3.0...HEAD
[1.3.0]: https://github.com/foundryside-dev/wardline/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/foundryside-dev/wardline/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/foundryside-dev/wardline/compare/v1.0.7...v1.1.0
[1.0.7]: https://github.com/foundryside-dev/wardline/compare/v1.0.6...v1.0.7
[1.0.6]: https://github.com/foundryside-dev/wardline/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/foundryside-dev/wardline/compare/v1.0.4...v1.0.5
[1.0.4]: https://github.com/foundryside-dev/wardline/compare/v1.0.3...v1.0.4
Expand Down
22 changes: 21 additions & 1 deletion UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,27 @@

Migration notes for changes that can alter a previously-green run. Newest first.

## To the next release (recommended v1.2) — preview rules now gate (soundness)
## To v1.3 — federation transports refuse redirects; signing fails closed

**Redirects are never followed on credential-bearing transports**
(`wardline-f68c483d92`). Previously urllib followed a 3xx, re-sending
`Authorization`/`X-Weft-*` headers to the redirect target (cross-origin included)
and rewriting a redirected POST into a body-less GET whose 200 read as a clean
emit — a silent false green. If your `--filigree-url` / `--loomweave-url` /
OpenRouter endpoint resolves through a redirect (an `http→https` upgrade, a host
alias), the transport now refuses the first 3xx fail-closed: emits report
failure, `verify_token` reports inconclusive, and the promote/judge legs reject
loudly. **What to do:** point the URL at the final, post-redirect target. The
taint gate itself is unaffected — federation stays non-gating enrichment.

**Signing paths fail closed on indeterminate git state.** A failed `git status`
no longer coerces to *clean*: the legis artifact signs only on a proven-clean
tree, and `attest` signs an indeterminate tree only under explicit
`allow_dirty` (recording `dirty: null` uncoerced). A CI job that signed from a
checkout where git state could not be read now fails instead of producing a
falsely-clean signed bundle.

## To v1.2 — preview rules now gate (soundness)

`wardline-4ada23bb09`. The `--fail-on` gate previously **ignored** any rule whose
`maturity` is `preview`, so a scan could pass green while an active ERROR defect
Expand Down
12 changes: 6 additions & 6 deletions docs/reference/finding-lifecycle-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The Filigree metadata only carries the key when the state is not `active`

**"suppressed"** survives only as the umbrella *word* for "any state other than
`active`": `baselined` + `waived` + `judged`. The CLI prints this sum as the
`suppressed` count (`src/wardline/cli/scan.py:616`).
`suppressed` count (`src/wardline/cli/scan.py:654`).

## `active` is the one word for "non-suppressed defect"

Expand All @@ -72,7 +72,7 @@ consistently, on every surface:
| --- | --- | --- |
| Enum | `src/wardline/core/finding.py:72` | `SuppressionState.ACTIVE = "active"` |
| Summary field | `src/wardline/core/run.py:100`, built at `src/wardline/core/run.py:659` | `ScanSummary.active` |
| CLI summary line | `src/wardline/cli/scan.py:617` | `… {s.active} active` |
| CLI summary line | `src/wardline/cli/scan.py:655` | `… {s.active} active` |
| MCP scan response | `src/wardline/mcp/server.py:944` | `summary.active` |
| Agent-summary JSON | `src/wardline/core/agent_summary.py:130` | `summary.active_defects` |
| `wardline:loop` prompt | `src/wardline/mcp/prompts.py:13` | "Read `summary.active`" |
Expand Down Expand Up @@ -166,7 +166,7 @@ The MCP `scan` gate block exposes `gate.tripped` (`src/wardline/mcp/server.py:95
armed gate passes (so a hook-captured log self-diagnoses — a harness-side hook
failure is distinguishable from a wardline failure, wardline-eef3d30c7d), or a
`gate: NOT_EVALUATED — …` line for a bare scan
(`src/wardline/cli/scan.py:676`).
(`src/wardline/cli/scan.py:714`).

`--new-since` scopes **both** populations identically: any `active` defect
outside the delta is re-marked `baselined` in both the emitted and gate lists
Expand All @@ -182,7 +182,7 @@ still legitimately means three different things depending on the surface:
| --- | --- | --- |
| Filigree store | An **unseen fingerprint** — first time this finding identity is seen for a `(file, scan_source)`. | **Filigree-owned** lifecycle (`src/wardline/core/filigree_emit.py:68-76`) |
| `wardline scan --new-since <ref>` | **Delta-scope**: the gate fires only on defects in files/entities changed since a git ref; everything else is re-marked `baselined`. | `src/wardline/core/run.py:598`; help text `src/wardline/cli/scan.py` (`--new-since`) |
| (historical) CLI summary | Formerly relabelled the `active` count as "N new". **Corrected to "N active"**. | `src/wardline/cli/scan.py:617` |
| (historical) CLI summary | Formerly relabelled the `active` count as "N new". **Corrected to "N active"**. | `src/wardline/cli/scan.py:655` |

The first-seen Filigree sense and the delta-scope `--new-since` sense are
genuinely distinct concepts; neither is "active".
Expand All @@ -194,8 +194,8 @@ How each concept appears on each surface:
| Concept | CLI summary text | `ScanSummary` field | MCP `summary` key | Agent-summary key | Filigree store |
| --- | --- | --- | --- | --- | --- |
| every finding | `N finding(s)` | `total` (`run.py:99`) | `total` (`server.py:943`) | `total_findings` (`agent_summary.py:129`) | one finding per wire entry |
| live defect | `N active` (`scan.py:617`) | `active` (`run.py:100,596`) | `active` (`server.py:944`) | `active_defects` (`agent_summary.py:130`) | no `suppression_state` key (`finding.py:295`) |
| suppressed (sum) | `N suppressed` (`scan.py:616`) | `baselined+waived+judged` | the three keys | `suppressed_findings` (`agent_summary.py:131`) | `metadata.wardline.suppression_state` (`finding.py:295`) |
| live defect | `N active` (`scan.py:655`) | `active` (`run.py:100,596`) | `active` (`server.py:944`) | `active_defects` (`agent_summary.py:130`) | no `suppression_state` key (`finding.py:295`) |
| suppressed (sum) | `N suppressed` (`scan.py:654`) | `baselined+waived+judged` | the three keys | `suppressed_findings` (`agent_summary.py:131`) | `metadata.wardline.suppression_state` (`finding.py:295`) |
| baselined | `N baseline` | `baselined` (`run.py:102`) | `baselined` (`server.py:945`) | `baselined` (`agent_summary.py:133`) | `suppression_state: "baselined"` |
| waived | `N waiver` | `waived` (`run.py:103`) | `waived` (`server.py:946`) | `waived` (`agent_summary.py:134`) | `suppression_state: "waived"` |
| judged | `N judged` | `judged` (`run.py:104`) | `judged` (`server.py:947`) | `judged` (`agent_summary.py:135`) | `suppression_state: "judged"` |
Expand Down
2 changes: 1 addition & 1 deletion src/wardline/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.2.0"
__version__ = "1.3.0"
58 changes: 48 additions & 10 deletions src/wardline/cli/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -523,15 +523,31 @@ def confirm_cb(rel_path: str, orig: str, replacement: str, f: Finding) -> bool:
if emit_result is not None:
logged_filigree_url = _redact_url_for_log(filigree_url)
if not emit_result.reachable:
# PDR-0023 at the human surface: a mid-batch failure is NOT first-contact
# unreachability. chunks_landed > 0 means Filigree already ingested earlier
# chunks (rows created/updated, their mark_unseen sweeps ran), so "findings
# written locally only" would misreport a real remote mutation. Render the
# partial counts + warnings in EVERY soft-failure sub-branch (auth / server
# error / transport alike) — the agent-facing status blocks already carry
# this truth; the operator line must not contradict them.
if emit_result.chunks_landed:
locality = (
f"Partial emit: {emit_result.chunks_landed} chunk(s) landed before the failure — "
f"{emit_result.created} created / {emit_result.updated} updated remotely; "
f"{emit_result.failed} finding(s) did not land — re-emit to reconcile."
)
if emit_result.warnings:
locality += f" {len(emit_result.warnings)} warning(s): " + "; ".join(emit_result.warnings)
else:
locality = "Findings written locally only."
if emit_result.auth_rejected:
# Reachable but refused — actionable, NOT "could not reach" (dogfood #5).
# Split 401 (no/bad token → set one) from 403 (token present but lacks
# access / blocked → setting a token won't help) so the remedy fits.
if emit_result.status == 403:
click.echo(
f"warning: Filigree returned 403 (forbidden) at {logged_filigree_url}; the token is "
"present but lacks access (scope/permission) or the request is blocked. "
"Findings written locally only.",
f"present but lacks access (scope/permission) or the request is blocked. {locality}",
err=True,
)
elif emit_result.token_sent:
Expand All @@ -540,19 +556,27 @@ def confirm_cb(rel_path: str, orig: str, replacement: str, f: Finding) -> bool:
click.echo(
f"warning: Filigree rejected the token (401) at {logged_filigree_url}; a token WAS sent but "
"its value is wrong — align WEFT_FEDERATION_TOKEN (env or .env) to the canonical "
"federation token. Findings written locally only.",
f"federation token. {locality}",
err=True,
)
else:
click.echo(
f"warning: Filigree returned 401 (auth rejected) at {logged_filigree_url}; no token was sent — "
"set WEFT_FEDERATION_TOKEN (env or .env) to the project token. Findings written locally only.",
f"set WEFT_FEDERATION_TOKEN (env or .env) to the project token. {locality}",
err=True,
)
elif emit_result.status is not None:
click.echo(
f"warning: Filigree returned {emit_result.status} (server error) at {logged_filigree_url}; "
"findings written locally only.",
f"warning: Filigree returned {emit_result.status} (server error) at {logged_filigree_url}. "
f"{locality}",
err=True,
)
elif emit_result.chunks_landed:
# status=None + chunks landed = the connection dropped MID-emit. "could
# not reach" is only the first-contact truth; naming it here would send
# the operator down the wrong (connectivity-from-zero) path.
click.echo(
f"warning: Filigree connection dropped mid-emit at {logged_filigree_url}. {locality}",
err=True,
)
else:
Expand Down Expand Up @@ -583,10 +607,24 @@ def confirm_cb(rel_path: str, orig: str, replacement: str, f: Finding) -> bool:
logged_loomweave_url = _redact_url_for_log(loomweave_url)
if not loomweave_result.reachable:
reason = loomweave_result.disabled_reason or "unreachable"
click.echo(
f"warning: Loomweave taint store not written at {logged_loomweave_url} ({reason}); scan unaffected.",
err=True,
)
if loomweave_result.written:
# Mid-batch soft failure (outage/403 on a later chunk): earlier chunks
# ARE committed (per-entity replace; `written` counts only chunks that
# landed before the failure — the write_taint_facts contract), so the
# flat "not written" would hide a real store mutation. The write is
# whole-batch idempotent — a full re-run reconciles.
click.echo(
f"warning: Loomweave taint store partially written at {logged_loomweave_url} ({reason}): "
f"{loomweave_result.written} taint fact(s) committed before the failure — "
"re-run the scan to reconcile; scan unaffected.",
err=True,
)
else:
click.echo(
f"warning: Loomweave taint store not written at {logged_loomweave_url} ({reason}); "
"scan unaffected.",
err=True,
)
else:
line = f"wrote {loomweave_result.written} taint fact(s) to {logged_loomweave_url}"
if loomweave_result.unresolved_qualnames:
Expand Down
5 changes: 4 additions & 1 deletion src/wardline/core/federation_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,15 @@ def filigree_emit_status(
"failures": [f.to_wire() for f in result.failures],
"warnings": list(result.warnings),
# The shared 401/403-vs-5xx-vs-transport ladder (dogfood #5) instead of flattening
# every soft failure to "filigree unreachable".
# every soft failure to "filigree unreachable". chunks_landed keeps the string
# honest on a MID-BATCH failure (wardline-7924d67d3b) — scan_jobs persists it
# verbatim as the terminal job `error`, so it must not contradict the counts.
"disabled_reason": filigree_disabled_reason(
reachable=result.reachable,
status=result.status,
token_sent=result.token_sent,
url=result.url,
chunks_landed=result.chunks_landed,

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 Thread landed chunk counts through MCP status

This only passes chunks_landed on the result-based status path; the MCP scan path still builds a raw block that drops er.chunks_landed (src/wardline/mcp/server.py:116-134), and filigree_emit_status_from_block() then calls filigree_disabled_reason() with the default zero. In an MCP scan where the Filigree connection drops after one or more chunks landed, the response can show nonzero created/updated counts and partial failures but still report disabled_reason: "filigree unreachable ...", recreating the misdiagnosis this change is meant to remove for the agent-facing surface.

Useful? React with 👍 / 👎.

),
}
if include_destination:
Expand Down
19 changes: 14 additions & 5 deletions src/wardline/core/filigree_emit.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ def _record_pending_partial_failures(


def filigree_disabled_reason(
*, reachable: bool, status: int | None, token_sent: bool = False, url: str | None = None
*, reachable: bool, status: int | None, token_sent: bool = False, url: str | None = None, chunks_landed: int = 0
) -> str | None:
"""The ``disabled_reason`` for an emit attempt, or None when Filigree was reached.

Expand All @@ -624,26 +624,35 @@ def filigree_disabled_reason(
disagrees with the status (the inconsistent triple the standalone signature once allowed).
``reachable`` remains an input because ``status is None`` is ambiguous on its own — it
means EITHER a 2xx success (reachable) OR a transport failure (unreachable).

``chunks_landed > 0`` marks a MID-BATCH failure (wardline-7924d67d3b): earlier chunks
were ingested (their mark_unseen sweeps ran), so a flat "filigree unreachable" — which
scan_jobs persists verbatim as the terminal job ``error`` — would contradict the counts
beside it. The transport rung becomes "connection dropped mid-emit"; the status rungs
keep their diagnosis and append the landed-chunk truth.
"""
if reachable:
return None
diagnostic_url = redact_url_for_diagnostics(url)
at = f" at {diagnostic_url}" if diagnostic_url else ""
partial = f"; {chunks_landed} chunk(s) landed before the failure" if chunks_landed else ""
if status in (401, 403):
# 403 → token present but lacks access (a token won't help). 401 → split by whether a
# token was actually SENT: absent (set one) vs rejected (the value is wrong). The old
# flat "set WEFT_FEDERATION_TOKEN" implied absence even when a token was sent and
# rejected — the C-7 misdiagnosis (weft-23574069a1).
if status == 403:
return f"filigree forbidden (403){at}; token present but lacks access / blocked"
return f"filigree forbidden (403){at}; token present but lacks access / blocked{partial}"
if token_sent:
return (
f"filigree rejected the token (401){at}; a token WAS sent but its value is wrong — "
"align WEFT_FEDERATION_TOKEN (env or .env) to the canonical federation token"
f"align WEFT_FEDERATION_TOKEN (env or .env) to the canonical federation token{partial}"
)
return f"filigree auth-rejected (401){at}; no token sent — set WEFT_FEDERATION_TOKEN (env or .env)"
return f"filigree auth-rejected (401){at}; no token sent — set WEFT_FEDERATION_TOKEN (env or .env){partial}"
if status is not None:
return f"filigree server error ({status}){at}"
return f"filigree server error ({status}){at}{partial}"
if chunks_landed:
return f"filigree connection dropped mid-emit ({chunks_landed} chunk(s) landed before the failure){at}"
return f"filigree unreachable{at}"


Expand Down
Loading