hyf

Context-aware query service for Radroots
git clone https://radroots.dev/git/hyf.git
Log | Files | Refs | README | LICENSE

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:
Asrc/hyf_assist/question_plan.mojo | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_assist.mojo | 44++++++++++++++++++++++++++++++++++++++++++++
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]())