Files
ThothII/scripts/evidence-restructuring-acceptance.sh

225 lines
9.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
python="$repo_root/harness/.venv/bin/python"
fixture="$repo_root/harness/tests/fixtures/evidence_authoring/poorly_structured.md"
[[ -x "$python" ]] || { printf 'missing harness virtualenv: %s\n' "$python" >&2; exit 127; }
[[ -f "$fixture" ]] || { printf 'missing authoring fixture: %s\n' "$fixture" >&2; exit 2; }
before_status="$(git -C "$repo_root" status --porcelain)"
temporary_root="$(mktemp -d "${TMPDIR:-/tmp}/thothii-evidence-restructuring.XXXXXXXX")"
trap 'rm -rf -- "$temporary_root"' EXIT HUP INT TERM
# The authoring target is created under mktemp and passed directly to the Python API.
# This runner has no PSD workspace argument or configured external authoring path.
TASK12_WORKSPACE="$temporary_root/workspace" TASK12_FIXTURE="$fixture" \
PYTHONPATH="$repo_root/harness" "$python" - <<'PY'
import os
import shutil
import subprocess
from pathlib import Path
from tht.evidence import (
EvidencePreparationError,
EvidenceRestructurer,
RestructureCandidate,
dump_manifest,
load_curated_tree,
load_manifest,
prepare_workspace_evidence,
validate_workspace_evidence,
)
workspace = Path(os.environ["TASK12_WORKSPACE"]).resolve()
fixture = Path(os.environ["TASK12_FIXTURE"]).resolve()
source_root = workspace / "evidence" / "source"
source_root.mkdir(parents=True)
fixture_target = source_root / "notes" / "poorly-structured.md"
fixture_target.parent.mkdir()
shutil.copyfile(fixture, fixture_target)
independent_target = source_root / "notes" / "independent.md"
independent_target.write_text("La codifica ADT è usata soltanto per il triage.\n", encoding="utf-8")
class FakeRestructurer(EvidenceRestructurer):
"""Hermetic, deterministic substitute for the one-call-per-source Pi boundary."""
def __init__(self):
self.requests = []
def restructure(self, request):
self.requests.append(request)
existing = {unit.title: unit.id for unit in request.previous_units}
def candidate(title, kind, purposes, excerpt, payload, review_items=()):
return RestructureCandidate.model_validate({
"schema_version": 1,
"existing_id": existing.get(title),
"title": title,
"kind": kind,
"purposes": purposes,
"applies_to": {
"concepts": ["fascia pediatrica"],
"tables": ["clinical.patient"],
"columns": ["clinical.patient.birth_date"],
},
"language": "it",
"supporting_excerpts": [excerpt],
"review_items": review_items,
"payload": payload,
})
if request.source_file.endswith("independent.md"):
return (candidate(
"Codifica ADT", "domain", ["schema_linking"],
"La codifica ADT è usata soltanto per il triage.",
{"rule": "Usare ADT solo per il triage."},
),)
return (
candidate(
"Fascia pediatrica", "formula", ["sql_generation"],
"Formula candidata: `CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END`.",
{
"concept": "fascia pediatrica",
"columns": ["clinical.patient.birth_date"],
"sql": "CASE WHEN age < 18 THEN 'pediatrica' ELSE 'adulta' END",
},
),
candidate(
"Stato dimissione", "enum", ["schema_linking"],
"Lo stato di dimissione usa i codici: D = dimesso, T = trasferito.",
{"column": "clinical.episode.discharge_status", "values": {"D": "dimesso", "T": "trasferito"}},
),
candidate(
"Linea guida dimissione", "reference", ["rewriting"],
"https://example.test/linee-guida-dimissione",
{
"url": "https://example.test/linee-guida-dimissione",
"label": "Linea guida dimissione",
"description": "Riferimento clinico per la dimissione.",
},
),
candidate(
"Regola età", "domain", ["disambiguation"],
"La fascia pediatrica riguarda i pazienti con età minore di 18 anni; il testo non chiarisce se l'età debba essere calcolata alla data di ricovero o alla data odierna.",
{"rule": "La fascia pediatrica comprende i pazienti con meno di 18 anni."},
[{
"code": "ambiguous_source_statement",
"message": "La data di calcolo dell'età non è specificata.",
"field": "domain.rule",
}],
),
)
def run_git(*arguments):
return subprocess.run(["git", *arguments], cwd=workspace, check=True, capture_output=True, text=True)
fake = FakeRestructurer()
first = prepare_workspace_evidence(workspace, restructurer=fake, git_status=lambda _: ())
assert first.model_calls == 2 and len(first.created) == 5
documents = load_curated_tree(workspace / "evidence" / "curated")
assert {document.kind for document in documents} == {"domain", "enum", "formula", "reference"}
assert len({document.id for document in documents}) == 5
assert {document.provenance.source_file for document in documents} == {
"source/notes/poorly-structured.md", "source/notes/independent.md",
}
manifest = load_manifest(workspace / "evidence" / "manifest.yaml")
assert not set(manifest.sources["source/notes/poorly-structured.md"].units).intersection(
manifest.sources["source/notes/independent.md"].units
)
assert "unresolved_review_item" in {finding.code for finding in validate_workspace_evidence(workspace).findings}
run_git("init", "-q")
run_git("config", "user.email", "acceptance@example.test")
run_git("config", "user.name", "Acceptance")
run_git("add", "evidence")
run_git("commit", "-qm", "baseline human-reviewed curated evidence")
baseline_formula = run_git("show", "HEAD:evidence/curated/formula/fascia-pediatrica.md").stdout
fixture_target.write_text(fixture_target.read_text(encoding="utf-8") + "\nNota revisionata.\n", encoding="utf-8")
second = prepare_workspace_evidence(workspace, restructurer=fake)
assert second.model_calls == 1 and second.changed == ("source/notes/poorly-structured.md",)
changed_paths = set(run_git("diff", "--name-only").stdout.splitlines())
assert "evidence/manifest.yaml" in changed_paths
assert "evidence/curated/formula/fascia-pediatrica.md" in changed_paths
assert run_git("show", "HEAD:evidence/curated/formula/fascia-pediatrica.md").stdout == baseline_formula
run_git("add", "evidence")
run_git("commit", "-qm", "proposed fake restructuring")
snapshot = {
path.relative_to(workspace): path.read_bytes()
for path in (workspace / "evidence").rglob("*") if path.is_file()
}
no_op = prepare_workspace_evidence(workspace, restructurer=fake)
assert no_op.model_calls == 0 and no_op.changed == ()
assert snapshot == {
path.relative_to(workspace): path.read_bytes()
for path in (workspace / "evidence").rglob("*") if path.is_file()
}
curated_path = workspace / "evidence" / "curated" / "formula" / "fascia-pediatrica.md"
curated_path.write_text(curated_path.read_text(encoding="utf-8") + "\n", encoding="utf-8")
try:
prepare_workspace_evidence(workspace, restructurer=fake)
except EvidencePreparationError as failure:
assert failure.code == "authoring_worktree_dirty"
else:
raise AssertionError("dirty curated state was not refused")
run_git("checkout", "--", "evidence/curated")
manifest_path = workspace / "evidence" / "manifest.yaml"
manifest_path.write_text(
manifest_path.read_text(encoding="utf-8").replace("evidence-authoring-v1", "evidence-authoring-v2"),
encoding="utf-8",
)
run_git("add", "evidence/manifest.yaml")
run_git("commit", "-qm", "incompatible authoring pipeline")
incompatible = manifest_path.read_bytes()
try:
prepare_workspace_evidence(workspace, restructurer=fake)
except EvidencePreparationError as failure:
assert failure.code == "pipeline_upgrade_required"
else:
raise AssertionError("incompatible pipeline version was not refused")
assert manifest_path.read_bytes() == incompatible
upgrade = prepare_workspace_evidence(workspace, restructurer=fake, upgrade=True)
assert upgrade.model_calls == 2 and set(upgrade.changed) == {
"source/notes/independent.md", "source/notes/poorly-structured.md",
}
manifest = load_manifest(manifest_path).model_copy(update={"orphans": ("evidence:fascia-pediatrica",)})
manifest_path.write_text(dump_manifest(manifest), encoding="utf-8")
assert "orphaned_unit" in {finding.code for finding in validate_workspace_evidence(workspace).findings}
print("PASS hermetic fake-restructurer: split, review, Git recovery/diff, no-op, dirty, upgrade, orphan")
print("PASS isolation: only a temporary workspace was supplied; no external PSD path was read or written")
PY
(
cd "$repo_root/harness"
.venv/bin/pytest -q \
tests/test_evidence_restructuring_fixture.py \
tests/test_evidence_candidate_publication.py \
tests/test_evidence_authoring.py \
tests/test_evidence_canonical.py \
tests/test_evidence_pi_restructurer.py \
tests/test_evidence_evaluation.py \
tests/test_evidence_formula_migration.py \
tests/test_evidence_facade_contract.py \
tests/test_corpus_chunk.py \
tests/test_corpus_pipeline.py \
tests/test_qdrant_vector_store.py \
tests/l0/test_qdrant_bm25_inference.py
)
after_status="$(git -C "$repo_root" status --porcelain)"
[[ "$before_status" == "$after_status" ]] || {
printf 'acceptance runner unexpectedly changed the ThothII worktree\n' >&2
exit 1
}
printf 'PASS evidence restructuring automated acceptance\n'