feat: implement memory and evidence administration with guided repairs
Publish documentation / publish (push) Successful in 1m27s
Publish documentation / publish (push) Successful in 1m27s
Add PostgreSQL-backed memory, editable evidence with source review and activation, and human-approved archive repairs across the harness, API, and UI. Include migrations, deployment support, regression coverage, and validation documentation. Refresh permissions from validated session roles so existing administrator logins can access newly deployed archive management features.
This commit is contained in:
@@ -25,6 +25,7 @@ from tht.evidence.canonical import (
|
||||
EvidenceKind,
|
||||
EvidencePurpose,
|
||||
EvidenceScope,
|
||||
ManualEvidenceProvenance,
|
||||
ReviewItem,
|
||||
StrictModel,
|
||||
dump_curated_markdown,
|
||||
@@ -189,6 +190,12 @@ def _restore_exact_source_excerpts(
|
||||
})
|
||||
|
||||
|
||||
def authoring_skill_path() -> Path:
|
||||
"""The installed wheel and the deployment's Pi resources live in different roots."""
|
||||
root = Path(os.environ.get("THT_HARNESS_DIR", str(Path(__file__).resolve().parents[2])))
|
||||
return root / ".pi" / "skills" / "tht-evidence-authoring" / "SKILL.md"
|
||||
|
||||
|
||||
class PiEvidenceRestructurer:
|
||||
"""Invoke Pi once, without tools or session state, for one changed source."""
|
||||
|
||||
@@ -389,6 +396,14 @@ def dump_manifest(manifest: EvidenceManifest) -> str:
|
||||
def validate_workspace_evidence(workspace_root: Path) -> ValidationReport:
|
||||
"""Validate the curated corpus without writing the workspace."""
|
||||
evidence_root = workspace_root / "evidence"
|
||||
if (evidence_root / ".local" / "state.yaml").is_file():
|
||||
from .local_archive import LocalEvidenceArchive
|
||||
try:
|
||||
LocalEvidenceArchive(workspace_root).validate()
|
||||
return ValidationReport(())
|
||||
except (OSError, ValueError) as error:
|
||||
return ValidationReport((ValidationFinding("error", "local_evidence_invalid",
|
||||
"evidence/curated", str(error)),))
|
||||
findings: list[ValidationFinding] = []
|
||||
manifest_path = evidence_root / "manifest.yaml"
|
||||
if not manifest_path.is_file():
|
||||
@@ -485,6 +500,9 @@ def _validate_manifest_source(
|
||||
def _validate_unit(
|
||||
manifest: EvidenceManifest, evidence: CuratedEvidence, source: str | None,
|
||||
) -> list[ValidationFinding]:
|
||||
if isinstance(evidence.provenance, ManualEvidenceProvenance):
|
||||
return [ValidationFinding("error", "unresolved_review_item", evidence.id, item.message)
|
||||
for item in evidence.review_items]
|
||||
if evidence.id in manifest.orphans:
|
||||
return []
|
||||
path = evidence.provenance.source_file
|
||||
@@ -560,6 +578,8 @@ def prepare_workspace_evidence(
|
||||
raise EvidencePreparationError("authoring_workers_invalid")
|
||||
workspace_root = workspace_root.resolve()
|
||||
evidence_root = workspace_root / "evidence"
|
||||
if (evidence_root / ".local" / "state.yaml").exists():
|
||||
raise EvidencePreparationError("local_archive_requires_explicit_source_refresh")
|
||||
_reject_dirty_authoring_state(workspace_root, git_status or _git_status)
|
||||
manifest_path = evidence_root / "manifest.yaml"
|
||||
try:
|
||||
@@ -695,10 +715,9 @@ def migrate_workspace_evidence(
|
||||
*,
|
||||
git_status: Callable[[Path], tuple[str, ...]] | None = None,
|
||||
) -> EvidenceMigrationReport:
|
||||
"""Rewrite legacy Curated units as table-free v3 Markdown without changing semantics."""
|
||||
"""Convert legacy Curated units to editable v4 and preserve a local baseline."""
|
||||
workspace_root = workspace_root.resolve()
|
||||
evidence_root = workspace_root / "evidence"
|
||||
_reject_dirty_authoring_state(workspace_root, git_status or _git_status)
|
||||
try:
|
||||
manifest = load_manifest(evidence_root / "manifest.yaml")
|
||||
documents = load_curated_tree(evidence_root / "curated")
|
||||
@@ -709,7 +728,7 @@ def migrate_workspace_evidence(
|
||||
if len(documents_by_id) != len(documents):
|
||||
raise EvidencePreparationError("duplicate_evidence_id")
|
||||
upgraded = {
|
||||
evidence_id: document.model_copy(update={"schema_version": 3})
|
||||
evidence_id: document.model_copy(update={"schema_version": 4})
|
||||
for evidence_id, document in documents_by_id.items()
|
||||
}
|
||||
migrated_ids: list[str] = []
|
||||
@@ -728,12 +747,16 @@ def migrate_workspace_evidence(
|
||||
migrated = tuple(sorted(migrated_ids))
|
||||
unchanged = tuple(sorted(unchanged_ids))
|
||||
if not migrated:
|
||||
from .local_archive import LocalEvidenceArchive
|
||||
LocalEvidenceArchive(workspace_root).initialize()
|
||||
return EvidenceMigrationReport(
|
||||
migrated=(),
|
||||
unchanged=unchanged,
|
||||
findings=validate_workspace_evidence(workspace_root).findings,
|
||||
)
|
||||
findings = _stage_and_apply_authoring_tree(workspace_root, upgraded, manifest)
|
||||
from .local_archive import LocalEvidenceArchive
|
||||
LocalEvidenceArchive(workspace_root).initialize()
|
||||
return EvidenceMigrationReport(
|
||||
migrated=migrated,
|
||||
unchanged=unchanged,
|
||||
@@ -762,6 +785,8 @@ def resolve_workspace_evidence(
|
||||
|
||||
workspace_root = workspace_root.resolve()
|
||||
evidence_root = workspace_root / "evidence"
|
||||
if (evidence_root / ".local/state.yaml").exists():
|
||||
raise EvidencePreparationError("local_archive_requires_explicit_local_resolution")
|
||||
_reject_dirty_worktree(workspace_root, git_status or _git_status)
|
||||
try:
|
||||
manifest = load_manifest(evidence_root / "manifest.yaml")
|
||||
@@ -1017,7 +1042,7 @@ def _candidate_to_evidence(
|
||||
mode="json",
|
||||
exclude={"schema_version", "existing_id", "supporting_excerpts"},
|
||||
)
|
||||
data["schema_version"] = 3
|
||||
data["schema_version"] = 4
|
||||
data["id"] = evidence_id
|
||||
data["provenance"] = {
|
||||
"source_file": source_file,
|
||||
@@ -1038,7 +1063,7 @@ def _unsupported_unit(
|
||||
message="The current source no longer supports this Evidence unit.",
|
||||
),)
|
||||
return evidence.model_copy(update={
|
||||
"schema_version": 3,
|
||||
"schema_version": 4,
|
||||
"provenance": evidence.provenance.model_copy(update={
|
||||
"source_file": source_file,
|
||||
"source_sha256": source_hash,
|
||||
|
||||
Reference in New Issue
Block a user