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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ All notable changes to vouch are documented here. Format follows
artifact the caller could not already retrieve, and it touches no write path.

### Fixed
- **`verify_all` / `doctor` treat missing externals like drift** (#622):
`vouch source verify` already marked `external_status=missing` as `!`,
but `verify_all`'s audit `failed` list and `health.doctor` only looked
for `drift`, so a deleted upstream file could leave doctor `ok: true`
while the CLI failed. missing now joins the failed set and emits a
`source_missing` warning.
- **salience reflex excludes retracted claims**: `compute_salience` scanned
every claim regardless of status, so the `_meta.vouch_salience` sidebar
counted `ARCHIVED` / `SUPERSEDED` / `REDACTED` claims in `claim_count` and
Expand Down
11 changes: 11 additions & 0 deletions src/vouch/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,17 @@ def doctor(store: KBStore) -> HealthReport:
[vr.source.id],
)
)
elif vr.external_status == "missing":
detail = f" ({vr.note})" if vr.note else ""
report.findings.append(
Finding(
"warning",
"source_missing",
f"external file {vr.source.locator} missing or unreadable"
f" since registration{detail}",
[vr.source.id],
)
)

# Config sanity.
if not store.config_path.exists():
Expand Down
2 changes: 1 addition & 1 deletion src/vouch/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def verify_all(store: KBStore, *, actor: str = "vouch-verify"
results = [verify_source(store, s) for s in store.list_sources()]
failed = [
r.source.id for r in results
if not r.stored_ok or r.external_status == "drift"
if not r.stored_ok or r.external_status in {"drift", "missing"}
]
audit.log_event(
store.kb_dir, event="source.verify", actor=actor,
Expand Down
19 changes: 19 additions & 0 deletions tests/test_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ def test_doctor_runs_full_sweep(store: KBStore) -> None:
assert report.ok is True


def test_doctor_warns_on_missing_external_file(store: KBStore, tmp_path: Path) -> None:
"""source verify marks missing externals as '!'; doctor must surface
them too (not only drift)."""
f = tmp_path / "doc.txt"
f.write_bytes(b"original")
src = store.put_source(
f.read_bytes(), title="doc",
locator=str(f.resolve()), source_type="file",
)
f.unlink()
report = health.doctor(store)
missing = [f for f in report.findings if f.code == "source_missing"]
assert missing, [f.code for f in report.findings]
assert missing[0].severity == "warning"
assert src.id in missing[0].object_ids
# warning-only — same posture as source_drift
assert report.ok is True


def test_lint_surfaces_legacy_uncited_claim_yaml_without_crashing(
store: KBStore,
) -> None:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,34 @@ def test_verify_detects_external_drift(store: KBStore, tmp_path: Path) -> None:
assert target.external_status == "drift"


def test_verify_all_counts_missing_external_as_failed(
store: KBStore, tmp_path: Path,
) -> None:
"""CLI source verify treats missing as '!'; verify_all's audit failed
list must include it too (not only drift)."""
from vouch import audit

f = tmp_path / "doc.txt"
f.write_bytes(b"original")
src = store.put_source(
f.read_bytes(), title="doc",
locator=str(f.resolve()), source_type="file",
)
f.unlink()
results = verify.verify_all(store)
target = next(r for r in results if r.source.id == src.id)
assert target.stored_ok is True
assert target.external_status == "missing"

events = [
e for e in audit.read_events(store.kb_dir)
if e.event == "source.verify"
]
assert events
assert src.id in events[-1].object_ids
assert events[-1].data["failed"] >= 1


def test_verify_refuses_off_root_file_locator(store: KBStore, tmp_path: Path) -> None:
outside = tmp_path.parent / f"{tmp_path.name}-outside.txt"
outside.write_bytes(b"secret")
Expand Down
Loading