feat(ask): split question artifact path from proposal path
Adds dedicated ContemplationResult.question_path for off-serving ASK artifacts and stops storing QUESTION_NEEDED question paths in proposal_path. Proposal artifacts remain under proposal_path. No chat/runtime wiring, served ASK surface, Q1B carve-out retirement, registry flip, or CLAIMS/metric movement.
This commit is contained in:
parent
b687f62cf0
commit
9cff9755cd
2 changed files with 49 additions and 4 deletions
|
|
@ -78,6 +78,7 @@ class ContemplationResult:
|
||||||
answer: int | None = None
|
answer: int | None = None
|
||||||
family: str | None = None
|
family: str | None = None
|
||||||
proposal_path: str | None = None
|
proposal_path: str | None = None
|
||||||
|
question_path: str | None = None
|
||||||
message: str | None = None
|
message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -116,7 +117,8 @@ def _handle_ask_delivery(
|
||||||
return ContemplationResult(
|
return ContemplationResult(
|
||||||
Terminal.QUESTION_NEEDED, tuple(findings), attempts,
|
Terminal.QUESTION_NEEDED, tuple(findings), attempts,
|
||||||
selected_organ=selected_organ, family=family_name,
|
selected_organ=selected_organ, family=family_name,
|
||||||
proposal_path=str(path),
|
proposal_path=None,
|
||||||
|
question_path=str(path),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
findings.append(Finding("ask", f"unrenderable ask: {outcome.fallback_reason}"))
|
findings.append(Finding("ask", f"unrenderable ask: {outcome.fallback_reason}"))
|
||||||
|
|
|
||||||
|
|
@ -84,15 +84,19 @@ def test_pass_manager_does_not_import_or_call_render_question_directly() -> None
|
||||||
)
|
)
|
||||||
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
# Ensure render_question is not imported
|
# Ensure render_question is not imported, and chat/chat.runtime is not imported
|
||||||
if isinstance(node, ast.ImportFrom):
|
if isinstance(node, ast.ImportFrom):
|
||||||
assert node.module != "core.epistemic_questions.render"
|
assert node.module != "core.epistemic_questions.render"
|
||||||
|
assert node.module != "chat.runtime"
|
||||||
|
assert node.module != "chat"
|
||||||
if node.names:
|
if node.names:
|
||||||
for alias in node.names:
|
for alias in node.names:
|
||||||
assert alias.name != "render_question"
|
assert alias.name != "render_question"
|
||||||
elif isinstance(node, ast.Import):
|
elif isinstance(node, ast.Import):
|
||||||
for alias in node.names:
|
for alias in node.names:
|
||||||
assert alias.name != "core.epistemic_questions.render"
|
assert alias.name != "core.epistemic_questions.render"
|
||||||
|
assert alias.name != "chat.runtime"
|
||||||
|
assert alias.name != "chat"
|
||||||
|
|
||||||
# Ensure render_question is not called directly
|
# Ensure render_question is not called directly
|
||||||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
||||||
|
|
@ -152,6 +156,9 @@ def test_renderable_ask_path_returns_question_needed_under_exercise_ask(monkeypa
|
||||||
question_root=question_root,
|
question_root=question_root,
|
||||||
)
|
)
|
||||||
assert res_normal.terminal == Terminal.PROPOSAL_EMITTED
|
assert res_normal.terminal == Terminal.PROPOSAL_EMITTED
|
||||||
|
assert res_normal.proposal_path is not None
|
||||||
|
assert res_normal.question_path is None
|
||||||
|
assert proposal_root in Path(res_normal.proposal_path).parents
|
||||||
|
|
||||||
# 2. Assert that with exercise_ask=True, it returns QUESTION_NEEDED
|
# 2. Assert that with exercise_ask=True, it returns QUESTION_NEEDED
|
||||||
calls = []
|
calls = []
|
||||||
|
|
@ -171,8 +178,9 @@ def test_renderable_ask_path_returns_question_needed_under_exercise_ask(monkeypa
|
||||||
assert len(calls) == 1 # Verify it is called exactly once! No double delivery / double render!
|
assert len(calls) == 1 # Verify it is called exactly once! No double delivery / double render!
|
||||||
|
|
||||||
# Verify the question artifact path
|
# Verify the question artifact path
|
||||||
assert res_ask.proposal_path is not None
|
assert res_ask.proposal_path is None
|
||||||
artifact_path = Path(res_ask.proposal_path)
|
assert res_ask.question_path is not None
|
||||||
|
artifact_path = Path(res_ask.question_path)
|
||||||
assert artifact_path.exists()
|
assert artifact_path.exists()
|
||||||
|
|
||||||
# Assert question artifact is under question_root
|
# Assert question artifact is under question_root
|
||||||
|
|
@ -216,6 +224,7 @@ def test_unrenderable_ask_falls_back_in_pass_manager(monkeypatch, tmp_path) -> N
|
||||||
assert res.terminal is not Terminal.QUESTION_NEEDED
|
assert res.terminal is not Terminal.QUESTION_NEEDED
|
||||||
assert res.proposal_path is not None
|
assert res.proposal_path is not None
|
||||||
assert Path(res.proposal_path).exists()
|
assert Path(res.proposal_path).exists()
|
||||||
|
assert res.question_path is None
|
||||||
|
|
||||||
|
|
||||||
def test_family_none_does_not_crash_ask_branch(monkeypatch, tmp_path) -> None:
|
def test_family_none_does_not_crash_ask_branch(monkeypatch, tmp_path) -> None:
|
||||||
|
|
@ -289,5 +298,39 @@ def test_boundary_wins_over_ask_in_pass_manager(monkeypatch, tmp_path) -> None:
|
||||||
)
|
)
|
||||||
# Assert terminal remains REFUSED_UNSUPPORTED_FAMILY or REFUSED_KNOWN_BOUNDARY, not QUESTION_NEEDED
|
# Assert terminal remains REFUSED_UNSUPPORTED_FAMILY or REFUSED_KNOWN_BOUNDARY, not QUESTION_NEEDED
|
||||||
assert res.terminal == Terminal.REFUSED_UNSUPPORTED_FAMILY
|
assert res.terminal == Terminal.REFUSED_UNSUPPORTED_FAMILY
|
||||||
|
assert res.question_path is None
|
||||||
|
assert res.proposal_path is None
|
||||||
# Ensure no question is written under question_root
|
# Ensure no question is written under question_root
|
||||||
assert not question_root.exists() or len(list(question_root.glob("**/*"))) == 0
|
assert not question_root.exists() or len(list(question_root.glob("**/*"))) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_unrenderable_ask_falls_back_to_no_progress_in_pass_manager(monkeypatch, tmp_path) -> None:
|
||||||
|
from core.comprehension_attempt import ComprehensionAttempt, RouteResult
|
||||||
|
import generate.contemplation.pass_manager as pm
|
||||||
|
from core.epistemic_questions.delivery import DeliveryOutcome
|
||||||
|
|
||||||
|
attempt = ComprehensionAttempt(
|
||||||
|
organ="r2_constraints",
|
||||||
|
outcome="setup_refused",
|
||||||
|
refusal_reason="missing_total_count",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(pm, "route_setup", lambda text, case_id=None: RouteResult((attempt,), None, "all_refused"))
|
||||||
|
monkeypatch.setattr(
|
||||||
|
pm,
|
||||||
|
"_delivery_outcome_for_limitation",
|
||||||
|
lambda assessment: DeliveryOutcome(Terminal.NO_PROGRESS, None, "some_fallback_reason")
|
||||||
|
)
|
||||||
|
|
||||||
|
question_root = tmp_path / "teaching" / "questions"
|
||||||
|
proposal_root = tmp_path / "teaching" / "proposals"
|
||||||
|
|
||||||
|
res = contemplate(
|
||||||
|
"chickens",
|
||||||
|
proposal_root=proposal_root,
|
||||||
|
question_root=question_root,
|
||||||
|
exercise_ask=True,
|
||||||
|
)
|
||||||
|
assert res.terminal == Terminal.NO_PROGRESS
|
||||||
|
assert res.proposal_path is None
|
||||||
|
assert res.question_path is None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue