225 lines
9.6 KiB
Bash
Executable File
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'
|