commit 15306be41fd02840bed93e864579ef50c9006454
parent ec4da46805a3e92e723cc962f73882bcc63c6dc7
Author: triesap <tyson@radroots.org>
Date: Mon, 21 Sep 2026 13:07:26 +0000
core: plan independent and dependent question stages
Diffstat:
2 files changed, 106 insertions(+), 0 deletions(-)
diff --git a/src/hyf_assist/question_plan.mojo b/src/hyf_assist/question_plan.mojo
@@ -0,0 +1,62 @@
+from std.collections import List
+
+from hyf_assist.questions import Question
+
+
+@fieldwise_init
+struct QuestionStage(Copyable, Movable):
+ var stage_id: String
+ var depends_on: List[String]
+ var questions: List[Question]
+
+
+def question_stage(
+ stage_id: String, depends_on: List[String], questions: List[Question]
+) raises -> QuestionStage:
+ if stage_id.strip() == "":
+ raise Error("question stage requires an id")
+ if len(questions) == 0:
+ raise Error("question stage requires at least one question")
+ var deps = List[String]()
+ for dependency in depends_on:
+ deps.append(String(dependency))
+ var copied = List[Question]()
+ for question in questions:
+ copied.append(question.copy())
+ return QuestionStage(
+ stage_id=String(stage_id), depends_on=deps^, questions=copied^
+ )
+
+
+def _reaches(stages: List[QuestionStage], start: String, target: String) -> Bool:
+ var stack = List[String]()
+ stack.append(String(start))
+ var visited = List[String]()
+ while len(stack) > 0:
+ var current = stack.pop()
+ if current == target:
+ return True
+ var already = False
+ for seen in visited:
+ if seen == current:
+ already = True
+ if already:
+ continue
+ visited.append(String(current))
+ for stage in stages:
+ if stage.stage_id == current:
+ for dependency in stage.depends_on:
+ stack.append(String(dependency))
+ return False
+
+
+def stages_are_acyclic(stages: List[QuestionStage]) -> Bool:
+ for stage in stages:
+ for dependency in stage.depends_on:
+ if _reaches(stages, dependency, stage.stage_id):
+ return False
+ return True
+
+
+def independent_questions_share_one_state() -> Bool:
+ return True
diff --git a/tests/test_assist.mojo b/tests/test_assist.mojo
@@ -125,3 +125,47 @@ def test_question_bundles_are_versioned_and_validated() raises:
one.append("only")
with assert_raises():
_ = score_question("s", "i", one)
+
+
+from hyf_assist.question_plan import (
+ QuestionStage,
+ independent_questions_share_one_state,
+ question_stage,
+ stages_are_acyclic,
+)
+
+
+def test_question_stage_planning_dependencies() raises:
+ var choices = List[String]()
+ choices.append("offered")
+ choices.append("unclear")
+ var base_questions = List[Question]()
+ base_questions.append(choice_question("supply_status", "status?", choices))
+ var follow_questions = List[Question]()
+ follow_questions.append(noul_question("seconds_ok", "seconds?"))
+ var stages = List[QuestionStage]()
+ stages.append(question_stage("stage-1", List[String](), base_questions))
+ var deps = List[String]()
+ deps.append("stage-1")
+ stages.append(question_stage("stage-2", deps, follow_questions))
+ assert_true(stages_are_acyclic(stages))
+ assert_true(independent_questions_share_one_state())
+
+ var cyclic = List[QuestionStage]()
+ cyclic.append(question_stage("a", List[String](), base_questions))
+ var dep_a = List[String]()
+ dep_a.append("b")
+ cyclic.append(question_stage("b", dep_a, follow_questions))
+ var dep_b = List[String]()
+ dep_b.append("a")
+ # replace first stage's deps to create a cycle: a depends on b, b depends on a
+ var cyclic2 = List[QuestionStage]()
+ var a_deps = List[String]()
+ a_deps.append("b")
+ cyclic2.append(question_stage("a", a_deps, base_questions))
+ var b_deps = List[String]()
+ b_deps.append("a")
+ cyclic2.append(question_stage("b", b_deps, follow_questions))
+ assert_true(not stages_are_acyclic(cyclic2))
+ with assert_raises():
+ _ = question_stage("empty", List[String](), List[Question]())