feat(evidence): prepare curated evidence incrementally
This commit is contained in:
@@ -42,6 +42,7 @@ from tht.cli.datamart_cmd import datamart_app
|
||||
from tht.cli.db_cmd import db_app
|
||||
from tht.cli.decision_cmd import decision_app
|
||||
from tht.cli.doctor_cmd import doctor
|
||||
from tht.cli.evidence_cmd import evidence_app
|
||||
from tht.cli.memory_cmd import memory_app
|
||||
from tht.cli.ollama_cmd import ollama_app
|
||||
from tht.cli.phase_cmd import phase_app
|
||||
@@ -54,6 +55,7 @@ from tht.cli.vector_cmd import vector_app
|
||||
|
||||
app.add_typer(phase_app, name="phase")
|
||||
app.add_typer(preprocess_app, name="preprocess")
|
||||
app.add_typer(evidence_app, name="evidence")
|
||||
app.add_typer(config_app, name="config")
|
||||
app.add_typer(schema_app, name="schema")
|
||||
app.add_typer(session_app, name="session")
|
||||
|
||||
@@ -1,5 +1,105 @@
|
||||
"""Curator-facing Evidence authoring commands, separate from runtime preprocessing."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from tht.evidence import (
|
||||
EvidencePreparationError,
|
||||
PiEvidenceRestructurer,
|
||||
prepare_workspace_evidence,
|
||||
validate_workspace_evidence,
|
||||
)
|
||||
|
||||
evidence_app = typer.Typer(help="Prepare and validate workspace Evidence", no_args_is_help=True)
|
||||
|
||||
|
||||
def evidence_root(cfg) -> Path:
|
||||
return cfg.paths.artifacts / "evidence"
|
||||
def _canonical_worktree(workspace_root: Path) -> Path:
|
||||
if workspace_root.is_symlink():
|
||||
raise typer.BadParameter("workspace-root must not be a symlink")
|
||||
root = workspace_root.resolve()
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
cwd=root,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0 or Path(result.stdout.strip()).resolve() != root:
|
||||
raise typer.BadParameter("workspace-root must be the root of a canonical Git worktree")
|
||||
return root
|
||||
|
||||
|
||||
def _emit(payload: dict[str, object], json_output: bool) -> None:
|
||||
if json_output:
|
||||
typer.echo(json.dumps(payload, ensure_ascii=False, sort_keys=True))
|
||||
return
|
||||
for key, value in payload.items():
|
||||
typer.echo(f"{key}: {value}")
|
||||
|
||||
|
||||
def _preparation_payload(report) -> dict[str, object]:
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"operation": "evidence_prepare",
|
||||
"status": "prepared",
|
||||
"changed": list(report.changed),
|
||||
"unchanged": list(report.unchanged),
|
||||
"created": list(report.created),
|
||||
"orphaned": list(report.orphaned),
|
||||
"findings": _findings_payload(report.findings),
|
||||
"modelCalls": report.model_calls,
|
||||
}
|
||||
|
||||
|
||||
def _findings_payload(findings) -> list[dict[str, object]]:
|
||||
return [finding.__dict__ for finding in findings]
|
||||
|
||||
|
||||
@evidence_app.command("prepare")
|
||||
def prepare_cmd(
|
||||
workspace_root: Path,
|
||||
upgrade: Annotated[bool, typer.Option(help="Reprocess every source with the installed pipeline.")] = False,
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Write machine JSON to stdout.")] = False,
|
||||
) -> None:
|
||||
"""Prepare changed Source Evidence without committing or publishing it."""
|
||||
root = _canonical_worktree(workspace_root)
|
||||
skill_path = Path(__file__).resolve().parents[2] / ".pi" / "skills" / "tht-evidence-authoring" / "SKILL.md"
|
||||
restructurer = PiEvidenceRestructurer(os.environ.get("THT_PI_EXECUTABLE", "pi"), skill_path)
|
||||
try:
|
||||
report = prepare_workspace_evidence(root, restructurer=restructurer, upgrade=upgrade)
|
||||
except EvidencePreparationError as error:
|
||||
_emit({"schemaVersion": 1, "operation": "evidence_prepare", "status": "failed", "code": error.code}, json_output)
|
||||
raise typer.Exit(code=1) from error
|
||||
_emit(_preparation_payload(report), json_output)
|
||||
if report.findings:
|
||||
raise typer.Exit(code=3)
|
||||
|
||||
|
||||
@evidence_app.command("validate")
|
||||
def validate_cmd(
|
||||
workspace_root: Path,
|
||||
json_output: Annotated[bool, typer.Option("--json", help="Write machine JSON to stdout.")] = False,
|
||||
) -> None:
|
||||
"""Validate the authoring tree without writing, committing, or publishing it."""
|
||||
root = _canonical_worktree(workspace_root)
|
||||
report = validate_workspace_evidence(root)
|
||||
payload = {
|
||||
"schemaVersion": 1,
|
||||
"operation": "evidence_validate",
|
||||
"status": "valid" if report.publishable else "review_required",
|
||||
"publishable": report.publishable,
|
||||
"findings": _findings_payload(report.findings),
|
||||
}
|
||||
_emit(payload, json_output)
|
||||
if report.publishable:
|
||||
return
|
||||
if all(finding.code in {"orphaned_unit", "unresolved_review_item"} for finding in report.findings):
|
||||
raise typer.Exit(code=3)
|
||||
raise typer.Exit(code=1)
|
||||
|
||||
Reference in New Issue
Block a user