From e6eeb2ae3e275afd0d902a050edc4cb888262282 Mon Sep 17 00:00:00 2001 From: mptyl Date: Fri, 26 Jun 2026 23:04:22 +0200 Subject: [PATCH] =?UTF-8?q?test(harness):=20free-text=20rationale-capture?= =?UTF-8?q?=20contract=20(D13,=20=C2=A74.6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D13 instructs the model to evaluate Altro/Rifiuta/steering free text in context, act on it, re-ask if ambiguous, and record the user's words in the decision rationale. The actual interpretation is model behavior enforced by the skill prose + gate, validated at L2; this test pins the RECORDING contract the gate relies on: free text from 'Altro' round-trips into the decision rationale and survives persistence, never silently discarded. L1: test_freetext_interpretation (4 tests) -- Altro text preserved, persistence roundtrip (exact), multiline steering intact, empty rationale allowed. The skill prose ('Interpretazione del testo libero') ports with the .pi/ skill in Phase D. --- harness/tests/test_freetext_interpretation.py | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 harness/tests/test_freetext_interpretation.py diff --git a/harness/tests/test_freetext_interpretation.py b/harness/tests/test_freetext_interpretation.py new file mode 100644 index 00000000..211fa3a2 --- /dev/null +++ b/harness/tests/test_freetext_interpretation.py @@ -0,0 +1,62 @@ +"""L1: free-text interpretation -- rationale capture contract (spec D13, ยง4.6). + +D13 instructs the model to evaluate Altro/Rifiuta/steering free text in context, +act on it, re-ask if ambiguous (instead of defaulting), and record the user's +words in the decision rationale. The actual interpretation is model behavior +enforced by the skill prose + gate, validated at L2; this test pins the RECORDING +contract the gate relies on: free text from "Altro" must round-trip into the +decision rationale and survive persistence, never silently discarded. +""" +from pathlib import Path + +from nsp.decisions import append_decision, list_decisions + + +def test_altro_freetext_recorded_in_rationale(tmp_path): + s = tmp_path / "sess" + s.mkdir() + # The gate, on receiving Altro text, appends a decision whose rationale carries + # the user's words verbatim (not discarded, not paraphrased away). + append_decision( + s, + type="table_promoted", + subject="phase:4", + detail="ablazione", + rationale="Utente (Altro): 'ablazione' va cercato anche in patologia, non solo nel flag", + ) + decisions = list_decisions(s) + assert len(decisions) == 1 + assert "patologia" in decisions[0].rationale # user text preserved + assert "Altro" in decisions[0].rationale + + +def test_freetext_survives_persistence_roundtrip(tmp_path): + s = tmp_path / "sess" + s.mkdir() + user_text = "Rifiuto: la join corretta e' su id_episodio, non id_ricovero" + append_decision(s, type="join_modified", subject="phase:4", + detail="t1.id_ricovero", rationale=user_text) + append_decision(s, type="table_excluded", subject="phase:4", + detail="dimessi", rationale="ok") # a second, unrelated decision + reloaded = list_decisions(s) + assert reloaded[0].rationale == user_text # exact preservation + assert reloaded[1].rationale == "ok" + + +def test_multiline_steering_text_preserved(tmp_path): + s = tmp_path / "sess" + s.mkdir() + steering = "Considera solo il 2025.\nEscludi i day-hospital.\nUsa la data di dimissione." + append_decision(s, type="concept_clarified", subject="phase:1", + detail="periodo", rationale=steering) + assert list_decisions(s)[0].rationale == steering + # the multi-line steering is intact, including newlines + assert "\n" in list_decisions(s)[0].rationale + + +def test_empty_rationale_is_allowed(tmp_path): + # a decision can legitimately have no free text (e.g. a clean promote). + s = tmp_path / "sess" + s.mkdir() + append_decision(s, type="table_promoted", subject="phase:4", detail="pazienti") + assert list_decisions(s)[0].rationale == ""