The single most important architectural fix vs ChironeWp3: ALL helpers consult effective_decisions() instead of raw list_decisions(), so the reopen-aware view is consistent everywhere (fixes the bug where approved_ctes/advance_problems conflated stale pre-reopen decisions with new ones). Model (corrected during TDD): - current_phase folds the audit (excluding retracted) with guard 'n == cur' -- already reopen-aware (old phase_approved:N after reopen to M<N don't advance). - effective_decisions = decisions whose phase <= current_phase. A sql_approved at phase 7 is stale when current_phase=4 after a rollback to F4, even if in the ledger. Rollback to F4 does NOT invalidate decisions of phases 1-3 (they stay effective). - decision_retracted markers excluded (audit-only). Also: session/models.py ported (SchemaLinking + Candidate with grounded_values D14a + concept_formulas D14b). MAX_PHASE/PHASE_NAMES read from workflow.yaml via load_workflow() (no duplication). Strada 2: ladder if-phase-N kept for now, generic prerequisites evaluator (F2 full) deferred. 7 phase tests + 20 total passing.
106 lines
4.4 KiB
Python
106 lines
4.4 KiB
Python
from nsp.decisions import append_decision, DecisionRecord
|
|
from nsp.phase import current_phase, effective_decisions
|
|
|
|
|
|
def _d(session, dtype, subject, **kw) -> DecisionRecord:
|
|
"""Helper: appendi una decisione e ritorna la record (per leggere .seq)."""
|
|
return append_decision(
|
|
session, type=dtype, subject=subject,
|
|
detail=kw.get("detail", ""), rationale=kw.get("rationale", ""),
|
|
retracts=kw.get("retracts"),
|
|
)
|
|
|
|
|
|
def test_effective_decisions_excludes_stale_high_phase(tmp_path):
|
|
"""Dopo un phase_reopened a fase bassa, una decisione di fase alta e' stale (excluded).
|
|
|
|
Sequenza: approva 1, approva 2, reopen a 1, poi table_promoted:4.
|
|
current_phase diventa 1 (il reopen). La table_promoted:4 e' di fase 4 > 1 -> stale.
|
|
"""
|
|
s = tmp_path / "s"; s.mkdir()
|
|
_d(s, "phase_approved", "phase:1")
|
|
_d(s, "phase_approved", "phase:2")
|
|
_d(s, "phase_reopened", "phase:1")
|
|
_d(s, "table_promoted", "phase:4") # stale: fase 4 > current_phase(1)
|
|
assert current_phase(s) == 1
|
|
eff = effective_decisions(s)
|
|
types = [d.type for d in eff]
|
|
assert "table_promoted" not in types # la stale di fase 4 e' esclusa
|
|
|
|
|
|
def test_current_phase_after_reopen(tmp_path):
|
|
"""Il fold su audit-excluding-retracted gestisce correttamente il reopen e le ri-approvazioni."""
|
|
s = tmp_path / "s"; s.mkdir()
|
|
_d(s, "phase_approved", "phase:1")
|
|
_d(s, "phase_approved", "phase:2")
|
|
assert current_phase(s) == 3 # dopo 2 approvazioni -> fase 3
|
|
_d(s, "phase_reopened", "phase:1")
|
|
assert current_phase(s) == 1
|
|
_d(s, "phase_approved", "phase:1") # ri-approva dopo reopen (legittima, NON stale)
|
|
assert current_phase(s) == 2
|
|
|
|
|
|
def test_retracted_decision_excluded_from_effective(tmp_path):
|
|
"""Una decisione ritirata (decision_retracted) e' esclusa dalla vista effective."""
|
|
s = tmp_path / "s"; s.mkdir()
|
|
d1 = _d(s, "table_promoted", "phase:4", detail="t1")
|
|
_d(s, "decision_retracted", "phase:4", retracts=d1.seq)
|
|
# senza approvazioni di fase, current_phase=1; table_promoted:4 e' gia' > 1.
|
|
# Ma anche a fase 4 raggiunta, la ritirata NON deve ricomparire.
|
|
_d(s, "phase_approved", "phase:1")
|
|
_d(s, "phase_approved", "phase:2")
|
|
_d(s, "phase_approved", "phase:3")
|
|
_d(s, "phase_approved", "phase:4")
|
|
assert current_phase(s) == 5
|
|
eff = effective_decisions(s)
|
|
seqs = [d.seq for d in eff]
|
|
assert d1.seq not in seqs # la ritirata e' esclusa
|
|
types = [d.type for d in eff]
|
|
assert "decision_retracted" not in types # il marker stesso non conta
|
|
|
|
|
|
def test_current_phase_starts_at_1(tmp_path):
|
|
s = tmp_path / "s"; s.mkdir()
|
|
assert current_phase(s) == 1
|
|
|
|
|
|
def test_current_phase_clamps_at_max_plus_1(tmp_path):
|
|
"""Dopo tutte le approvazioni, current_phase = max_phase + 1."""
|
|
s = tmp_path / "s"; s.mkdir()
|
|
for n in range(1, 9):
|
|
_d(s, "phase_approved", f"phase:{n}")
|
|
assert current_phase(s) == 9 # max_phase(8) + 1
|
|
|
|
|
|
def test_effective_keeps_low_phase_after_high_phase_rollback(tmp_path):
|
|
"""Rollback a F4 NON invalida le decisioni delle fasi 1-3 (che restano <= current_phase)."""
|
|
s = tmp_path / "s"; s.mkdir()
|
|
_d(s, "concept_clarified", "phase:1", detail="x")
|
|
_d(s, "question_rewritten", "phase:3", detail="q")
|
|
_d(s, "table_promoted", "phase:4", detail="t")
|
|
_d(s, "sql_approved", "phase:7", detail="sql") # sara' stale dopo rollback
|
|
for n in range(1, 8):
|
|
_d(s, "phase_approved", f"phase:{n}")
|
|
assert current_phase(s) == 8
|
|
# ora rollback a F4
|
|
_d(s, "phase_reopened", "phase:4")
|
|
assert current_phase(s) == 4
|
|
eff = effective_decisions(s)
|
|
types_subjects = [(d.type, d.subject) for d in eff]
|
|
# le decisioni di fase <= 4 restano
|
|
assert ("concept_clarified", "phase:1") in types_subjects
|
|
assert ("question_rewritten", "phase:3") in types_subjects
|
|
assert ("table_promoted", "phase:4") in types_subjects
|
|
# la decisione di fase 7 (sql_approved) e' ora stale -> esclusa
|
|
assert ("sql_approved", "phase:7") not in types_subjects
|
|
|
|
|
|
def test_effective_decisions_no_reopen_returns_all_non_retracted(tmp_path):
|
|
"""Senza reopen e senza retract, effective = tutte le decisioni (della fase corrente)."""
|
|
s = tmp_path / "s"; s.mkdir()
|
|
_d(s, "concept_clarified", "phase:1", detail="x")
|
|
_d(s, "phase_approved", "phase:1")
|
|
assert current_phase(s) == 2
|
|
eff = effective_decisions(s)
|
|
assert len(eff) == 2
|