feat(epistemic): Q1-C strictly single-slot — refuse multi-slot ASK
The template asserts 'one more value is still needed' (exactly one). Rendering the first of several missing slots and ignoring the rest makes that claim subtly false. Refuse multi-slot assessments with multi_slot_not_supported (slot=None) rather than render-first-drop-rest; one-question-per-slot fan-out is a later rung. Replaces the now-contradicting first-of-many test with a test that pins the honest refusal.
This commit is contained in:
parent
b186f07382
commit
5f74351729
2 changed files with 32 additions and 12 deletions
|
|
@ -92,6 +92,7 @@ _TEMPLATE = (
|
|||
_REASON_RENDERED = "rendered"
|
||||
_REASON_NOT_ASK = "not_ask"
|
||||
_REASON_NO_SLOT = "no_missing_slot"
|
||||
_REASON_MULTI_SLOT = "multi_slot_not_supported"
|
||||
_REASON_RENDERABILITY_GAP = "renderability_gap"
|
||||
_REASON_FABRICATION_GUARD = "fabrication_guard"
|
||||
|
||||
|
|
@ -148,17 +149,29 @@ def _names_only_grounded(text: str, grounded_terms: tuple[str, ...]) -> bool:
|
|||
def render_question(assessment: LimitationAssessment) -> EpistemicQuestion:
|
||||
"""Render a single-slot generic ASK question, or refuse to render.
|
||||
|
||||
Single-slot only: the *first* missing slot is rendered; any others are
|
||||
ignored (later rungs may emit one question per slot). The renderer refuses
|
||||
(``question_unrenderable``) when the assessment is not an ASK, carries no
|
||||
slot, or the slot's structural type is outside the closed phrase map. It
|
||||
NEVER fabricates a natural-language entity name — see the module docstring
|
||||
for the policy and the wrong=0 rationale.
|
||||
Strictly single-slot: Q1-C renders only when the assessment carries
|
||||
*exactly one* missing slot. The fixed template asserts "one more value is
|
||||
still needed" — a globally-quantified claim that exactly one value is
|
||||
missing — so rendering the first of several slots and ignoring the rest
|
||||
would make that sentence subtly false (it would imply the single rendered
|
||||
value closes the gap when others remain). Rather than weaken the template to
|
||||
an honest-but-vaguer plural, a multi-slot assessment refuses with
|
||||
``multi_slot_not_supported`` (slot ``None``); one-question-per-slot fan-out
|
||||
is a later rung. The renderer also refuses (``question_unrenderable``) when
|
||||
the assessment is not an ASK, carries no slot, or the slot's structural type
|
||||
is outside the closed phrase map. It NEVER fabricates a natural-language
|
||||
entity name — see the module docstring for the policy and the wrong=0
|
||||
rationale.
|
||||
"""
|
||||
if assessment.resolution_action != "ask_question":
|
||||
return _unrenderable(_REASON_NOT_ASK)
|
||||
if not assessment.missing_slots:
|
||||
return _unrenderable(_REASON_NO_SLOT)
|
||||
if len(assessment.missing_slots) > 1:
|
||||
# Strictly single-slot: the template claims exactly one value is
|
||||
# missing, which is false when several slots remain. Refuse rather than
|
||||
# render the first and silently drop the rest.
|
||||
return _unrenderable(_REASON_MULTI_SLOT, slot=None)
|
||||
|
||||
slot = assessment.missing_slots[0]
|
||||
phrase = _CLOSED_TYPE_PHRASES.get(slot.expected_unit_or_type)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from core.epistemic_disclosure.limitation import (
|
|||
)
|
||||
from core.epistemic_questions import EpistemicQuestion, render_question
|
||||
from core.epistemic_questions.render import (
|
||||
_REASON_MULTI_SLOT,
|
||||
_REASON_NO_SLOT,
|
||||
_REASON_NOT_ASK,
|
||||
_REASON_RENDERABILITY_GAP,
|
||||
|
|
@ -138,7 +139,14 @@ def test_ask_with_zero_slots_is_unrenderable() -> None:
|
|||
assert q.reason == _REASON_NO_SLOT
|
||||
|
||||
|
||||
def test_multi_slot_renders_exactly_the_first_slot() -> None:
|
||||
def test_multi_slot_does_not_claim_all_missing_information_was_asked() -> None:
|
||||
"""Two missing slots → refuse with ``multi_slot_not_supported``.
|
||||
|
||||
The template asserts "one more value is still needed" (exactly one). With two
|
||||
slots that claim is false, so the renderer must NOT render the first and drop
|
||||
the second — it refuses outright, naming no slot. This is the wrong=0-honest
|
||||
choice: Q1-C is strictly single-slot, not first-of-many.
|
||||
"""
|
||||
second = MissingSlot(
|
||||
slot_name="weighted_total",
|
||||
expected_unit_or_type="measured_unit_int",
|
||||
|
|
@ -146,11 +154,10 @@ def test_multi_slot_renders_exactly_the_first_slot() -> None:
|
|||
)
|
||||
q = render_question(_ask_assessment((_TOTAL_COUNT_SLOT, second)))
|
||||
|
||||
# Single-slot result: bound to the first slot only.
|
||||
assert q.slot == _TOTAL_COUNT_SLOT
|
||||
assert not q.unrenderable
|
||||
assert q.text is not None
|
||||
assert "whole-number count" in q.text # first slot's type, not the second's
|
||||
assert q.unrenderable
|
||||
assert q.text is None
|
||||
assert q.slot is None
|
||||
assert q.reason == _REASON_MULTI_SLOT
|
||||
|
||||
|
||||
def test_unmapped_structural_type_degrades_to_renderability_gap() -> None:
|
||||
|
|
|
|||
Loading…
Reference in a new issue