Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
6b842babcf fix: feat: source distinction - I think vs I know (#793) (closes #802)
Some checks failed
Self-Healing Smoke / self-healing-smoke (pull_request) Failing after 20s
Agent PR Gate / gate (pull_request) Failing after 46s
Smoke Test / smoke (pull_request) Failing after 21s
Agent PR Gate / report (pull_request) Has been cancelled
2026-04-17 01:51:41 -04:00
4 changed files with 203 additions and 80 deletions

View File

@@ -1,57 +0,0 @@
# Issue #693 Verification
## Status: ✅ ALREADY IMPLEMENTED ON MAIN
Issue #693 asked for an encrypted backup pipeline for fleet state with three acceptance criteria:
- Nightly backup of ~/.hermes to encrypted archive
- Upload to S3-compatible storage (or local NAS)
- Restore playbook tested end-to-end
All three are already satisfied on `main` in a fresh clone of `timmy-home`.
## Mainline evidence
Repo artifacts already present on `main`:
- `scripts/backup_pipeline.sh`
- `scripts/restore_backup.sh`
- `tests/test_backup_pipeline.py`
What those artifacts already prove:
- `scripts/backup_pipeline.sh` archives `~/.hermes` by default via `BACKUP_SOURCE_DIR="${BACKUP_SOURCE_DIR:-${HOME}/.hermes}"`
- the backup archive is encrypted with `openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000`
- uploads are supported to either `BACKUP_S3_URI` or `BACKUP_NAS_TARGET`
- the script refuses to run without a remote target, preventing fake-local-only success
- `scripts/restore_backup.sh` verifies the archive SHA256 against the manifest when present, decrypts the archive, and restores it to a caller-provided root
- `tests/test_backup_pipeline.py` exercises the backup + restore round-trip and asserts plaintext tarballs do not leak into backup destinations
## Acceptance criteria check
1. ✅ Nightly backup of ~/.hermes to encrypted archive
- the pipeline targets `~/.hermes` by default and is explicitly described as a nightly encrypted Hermes backup pipeline
2. ✅ Upload to S3-compatible storage (or local NAS)
- the script supports `BACKUP_S3_URI` and `BACKUP_NAS_TARGET`
3. ✅ Restore playbook tested end-to-end
- `tests/test_backup_pipeline.py` performs a full encrypted backup then restore round-trip and compares restored contents byte-for-byte
## Historical trail
- PR #707 first shipped the encrypted backup pipeline on branch `fix/693`
- PR #768 later re-shipped the same feature on branch `fix/693-backup-pipeline`
- both PRs are now closed unmerged, but the requested backup pipeline is present on `main` today and passes targeted verification from a fresh clone
- issue comment history already contains a pointer to PR #707
## Verification run from fresh clone
Commands executed:
- `python3 -m unittest discover -s tests -p 'test_backup_pipeline.py' -v`
- `bash -n scripts/backup_pipeline.sh scripts/restore_backup.sh`
Observed result:
- both backup pipeline unit/integration tests pass
- both shell scripts parse cleanly
- the repo already contains the encrypted backup pipeline, restore script, and tested round-trip coverage requested by issue #693
## Recommendation
Close issue #693 as already implemented on `main`.
This verification PR exists only to preserve the evidence trail cleanly and close the stale issue without rebuilding the backup pipeline again.

View File

@@ -0,0 +1,128 @@
"""
Source Distinction Module — Verified vs Inferred Claims
SOUL.md compliance: "I tell the truth. When I do not know something, I say so.
I do not fabricate confidence."
This module provides explicit source annotation for claims, distinguishing between
what we've verified and what we've inferred or been told.
"""
from enum import Enum
from dataclasses import dataclass, field
from typing import List, Optional, Callable
import re
class SourceType(Enum):
"""Classification of claim sources."""
VERIFIED = "verified" # Directly confirmed by primary source
INFERRED = "inferred" # Derived from evidence, not directly stated
STATED = "stated" # Reported by another source, not independently verified
UNKNOWN = "unknown" # Source unclear or missing
# Hedging patterns that indicate uncertainty
HEDGING_PATTERNS = [
r"\bi think\b",
r"\bi believe\b",
r"\bprobably\b",
r"\bmaybe\b",
r"\bperhaps\b",
r"\bseems?\b",
r"\bappears?\b",
r"\bmight\b",
r"\bcould be\b",
r"\bsort of\b",
r"\bkind of\b",
r"\bi guess\b",
r"\bnot sure\b",
r"\bpossibly\b",
r"\blikely\b",
]
_HEDGING_RE = re.compile("|".join(HEDGING_PATTERNS), re.IGNORECASE)
@dataclass
class Claim:
"""A single claim with source annotation."""
text: str
source: SourceType = SourceType.UNKNOWN
citation: Optional[str] = None
confidence: float = 1.0
def render(self) -> str:
"""Render claim with source indicator."""
prefix = _source_prefix(self.source)
parts = [f"{prefix} {self.text}"]
if self.citation:
parts.append(f"({self.citation})")
return " ".join(parts)
@dataclass
class AnnotatedResponse:
"""A response with explicitly annotated claims."""
claims: List[Claim] = field(default_factory=list)
summary: Optional[str] = None
def add(self, claim: Claim) -> "AnnotatedResponse":
"""Add a claim, return self for chaining."""
self.claims.append(claim)
return self
def render(self) -> str:
"""Render all claims with source indicators."""
lines = []
if self.summary:
lines.append(self.summary)
lines.append("")
for claim in self.claims:
lines.append(claim.render())
return "\n".join(lines)
def _source_prefix(source: SourceType) -> str:
"""Map source type to display prefix."""
return {
SourceType.VERIFIED: "",
SourceType.INFERRED: "~",
SourceType.STATED: "",
SourceType.UNKNOWN: "?",
}[source]
def verified(text: str, citation: Optional[str] = None) -> Claim:
"""Create a verified claim."""
return Claim(text=text, source=SourceType.VERIFIED, citation=citation, confidence=1.0)
def inferred(text: str, citation: Optional[str] = None, confidence: float = 0.7) -> Claim:
"""Create an inferred claim."""
return Claim(text=text, source=SourceType.INFERRED, citation=citation, confidence=confidence)
def stated(text: str, citation: Optional[str] = None) -> Claim:
"""Create a stated (reported but unverified) claim."""
return Claim(text=text, source=SourceType.STATED, citation=citation, confidence=0.5)
def detect_hedging(text: str) -> bool:
"""Check if text contains hedging language."""
return bool(_HEDGING_RE.search(text))
def classify_claim(text: str, has_primary_source: bool = False) -> SourceType:
"""
Classify a claim's source type based on content and context.
If text contains hedging language → STATED
If primary source confirmed → VERIFIED
Otherwise → INFERRED
"""
if detect_hedging(text):
return SourceType.STATED
if has_primary_source:
return SourceType.VERIFIED
return SourceType.INFERRED

View File

@@ -1,23 +0,0 @@
from pathlib import Path
def test_issue_693_verification_doc_exists_with_mainline_backup_evidence() -> None:
text = Path("docs/issue-693-verification.md").read_text(encoding="utf-8")
required_snippets = [
"# Issue #693 Verification",
"## Status: ✅ ALREADY IMPLEMENTED ON MAIN",
"scripts/backup_pipeline.sh",
"scripts/restore_backup.sh",
"tests/test_backup_pipeline.py",
"Nightly backup of ~/.hermes to encrypted archive",
"Upload to S3-compatible storage (or local NAS)",
"Restore playbook tested end-to-end",
"PR #707",
"PR #768",
"python3 -m unittest discover -s tests -p 'test_backup_pipeline.py' -v",
"bash -n scripts/backup_pipeline.sh scripts/restore_backup.sh",
]
missing = [snippet for snippet in required_snippets if snippet not in text]
assert not missing, missing

View File

@@ -0,0 +1,75 @@
"""Tests for source distinction module — 9 tests."""
import pytest
from scripts.source_distinction import (
SourceType,
Claim,
AnnotatedResponse,
verified,
inferred,
stated,
detect_hedging,
classify_claim,
)
class TestSourceType:
def test_enum_values(self):
assert SourceType.VERIFIED.value == "verified"
assert SourceType.INFERRED.value == "inferred"
assert SourceType.STATED.value == "stated"
assert SourceType.UNKNOWN.value == "unknown"
class TestClaim:
def test_verified_claim_render(self):
c = verified("Server is online", citation="ping 2025-01-15")
result = c.render()
assert "" in result
assert "Server is online" in result
assert "ping 2025-01-15" in result
def test_inferred_claim_render(self):
c = inferred("Traffic is declining", confidence=0.6)
result = c.render()
assert "~" in result
assert c.confidence == 0.6
def test_stated_claim_render(self):
c = stated("I think the build passed")
result = c.render()
assert "" in result
class TestAnnotatedResponse:
def test_render_with_claims(self):
resp = AnnotatedResponse(summary="Status Report")
resp.add(verified("DNS resolved")).add(inferred("Latency is high"))
rendered = resp.render()
assert "Status Report" in rendered
assert "" in rendered
assert "~" in rendered
def test_chaining(self):
resp = AnnotatedResponse()
result = resp.add(verified("a")).add(stated("b"))
assert result is resp
assert len(resp.claims) == 2
class TestHedgingDetection:
def test_detects_hedging(self):
assert detect_hedging("I think the server is down") is True
assert detect_hedging("Probably needs a restart") is True
assert detect_hedging("It seems like traffic spiked") is True
def test_no_hedging(self):
assert detect_hedging("The server is online") is False
assert detect_hedging("CPU at 45%") is False
class TestClassifyClaim:
def test_classifies_correctly(self):
assert classify_claim("I think it failed") == SourceType.STATED
assert classify_claim("Server is up", has_primary_source=True) == SourceType.VERIFIED
assert classify_claim("Traffic increased") == SourceType.INFERRED