gabriel / musehub public
feat BREAKING proposals task/proposal-models-v2 #17 / 19
AI Agent gabriel · 127 days ago · May 8, 2026 · Diff

feat(proposals): Phase 2 — Dependency DAG engine (Kahn's algorithm, cycle detection, merge gating)

proposal_dag.py — pure DAG engine: - ProposalDag: adjacency graph with depends_on/required_by/merged_ids/number_by_id - build_dag: construct from (dependent, dependency) edge pairs - topological_sort: Kahn's algorithm; raises CycleError with cycle_ids on cycle - detect_cycle: bool test without side effects - blocked_by_numbers / blocks_numbers / is_blocked: live-edge queries (merged excluded) - load_dag / load_dag_for_proposals: async DB loaders (2 queries each) - create_dependency_edges: persist edges with pre-write cycle detection

musehub_proposals.py — service integration: - create_proposal: accepts proposal_type, is_draft, merge_strategy, merge_conditions, selective_domains, depends_on; writes dependency edges via create_dependency_edges - _ProposalPrefetch: adds dag field (ProposalDag) - _prefetch_for_batch: Query 3 — load partial DAG for current page - _enrich_one: populates blocked_by/blocks/is_blocked from DAG; respects require_dependency_merged in all_merge_conditions_met; reads domain summary fields from ORM; surfaces proposal_type/is_draft/agent_model/agent_spawned_by - merge_proposal: gates on unmerged hard dependencies before executing merge - _to_proposal_response: surfaces proposal_type, is_draft, merge_conditions, merge_strategy, selective_domains, risk_score, dimensional_risk

proposals.py (API route): forwards all new ProposalCreate fields to service; handles CycleError as 422

tests: 29 tests — pure DAG unit tests + 5 DB integration tests

sha256:9de2441c818e8b0fba09c0fe251e96c1889b9cb287f4317f902ae1868f1cd127 sha
+85 ~8 symbols
sha256:6841d103d32c259765c0b821f1a6be925d591b3b648bd08f334dac9bc17e3550 snapshot
+85
symbols added
~8
symbols modified
0
dead code introduced
Semantic Changes 93 symbols
~ musehub/services/proposal_dag.py .py 23 symbols added
+ CycleError class class CycleError L36–48
+ __init__ method method __init__ L44–48
+ ProposalDag class class ProposalDag L57–78
+ blocked_by_numbers function function blocked_by_numbers L168–175
+ blocks_numbers function function blocks_numbers L178–185
+ build_dag function function build_dag L81–111
+ create_dependency_edges function async_function create_dependency_edges L303–379
+ detect_cycle function function detect_cycle L159–165
+ AsyncSession import import AsyncSession L24–24
+ Iterable import import Iterable L21–21
+ annotations import import annotations L16–16
+ dataclass class import dataclass L20–20
+ db import import db L26–26
+ defaultdict import import defaultdict L19–19
+ deque import import deque L19–19
+ field import import field L20–20
+ logging import import logging L18–18
+ select import import select L23–23
+ is_blocked function function is_blocked L188–190
+ load_dag function async_function load_dag L198–242
+ load_dag_for_proposals function async_function load_dag_for_proposals L245–300
+ logger variable variable logger L28–28
+ topological_sort function function topological_sort L114–156
+ TestBlockedByAndBlocks class class TestBlockedByAndBlocks L251–292
+ test_blocked_by_sorted_ascending method method test_blocked_by_sorted_ascending L288–292
+ test_blocks_excludes_merged_waiters method method test_blocks_excludes_merged_waiters L283–286
+ test_blocks_numbers method method test_blocks_numbers L277–281
+ test_live_dependency_blocks method method test_live_dependency_blocks L258–262
+ test_merged_dependency_not_blocking method method test_merged_dependency_not_blocking L264–268
+ test_no_dependencies_not_blocked method method test_no_dependencies_not_blocked L252–256
+ test_partial_merge_still_blocked method method test_partial_merge_still_blocked L270–275
+ TestBuildDag class class TestBuildDag L124–148
+ test_empty_edges method method test_empty_edges L125–128
+ test_merged_ids_populated method method test_merged_ids_populated L138–142
+ test_number_by_id_populated method method test_number_by_id_populated L144–148
+ test_single_edge method method test_single_edge L130–136
+ TestDagIntegration class class TestDagIntegration L300–446
+ test_all_merge_conditions_false_while_dep_unmerged method async_method test_all_merge_conditions_false_while_dep_unmerged L405–427
+ test_create_proposal_persists_dependency_edges method async_method test_create_proposal_persists_dependency_edges L302–328
+ test_create_proposal_rejects_cycle method async_method test_create_proposal_rejects_cycle L345–363
+ test_create_proposal_rejects_unknown_dependency method async_method test_create_proposal_rejects_unknown_dependency L331–342
+ test_enrich_batch_populates_blocked_by method async_method test_enrich_batch_populates_blocked_by L366–401
+ test_merge_proposal_gated_by_unmerged_dep method async_method test_merge_proposal_gated_by_unmerged_dep L430–446
+ TestDetectCycle class class TestDetectCycle L228–243
+ test_cycle_returns_true method method test_cycle_returns_true L234–237
+ test_no_cycle_returns_false method method test_no_cycle_returns_false L229–232
+ test_no_side_effects_on_dag method method test_no_side_effects_on_dag L239–243
+ TestTopologicalSort class class TestTopologicalSort L156–220
+ test_cycle_error_message_contains_ids method method test_cycle_error_message_contains_ids L206–212
+ test_cycle_three_nodes_raises method method test_cycle_three_nodes_raises L199–204
+ test_cycle_two_nodes_raises method method test_cycle_two_nodes_raises L192–197
+ test_diamond_shape method method test_diamond_shape L167–175
+ test_empty_dag method method test_empty_dag L157–159
+ test_linear_chain_abc method method test_linear_chain_abc L161–165
+ test_merged_breaks_cycle method method test_merged_breaks_cycle L214–220
+ test_merged_nodes_excluded_from_output method method test_merged_nodes_excluded_from_output L185–190
+ test_multi_root method method test_multi_root L177–183
+ _create_proposal function async_function _create_proposal L98–116
+ _ids function function _ids L57–58
+ _make_branch function async_function _make_branch L85–95
+ _make_repo function async_function _make_repo L61–82
+ _now function function _now L49–50
+ _uid function function _uid L53–54
+ AsyncSession import import AsyncSession L30–30
+ CycleError import import CycleError L32–32
+ ProposalDag import import ProposalDag L32–32
+ annotations import import annotations L24–24
+ blocked_by_numbers import import blocked_by_numbers L32–32
+ blocks_numbers import import blocks_numbers L32–32
+ build_dag import import build_dag L32–32
+ datetime import import datetime L27–27
+ detect_cycle import import detect_cycle L32–32
+ is_blocked import import is_blocked L32–32
+ pytest import import pytest L29–29
+ timezone import import timezone L27–27
+ topological_sort import import topological_sort L32–32
+ uuid import import uuid L26–26
~ musehub/api/routes/musehub/proposals.py .py 1 symbol added, 1 symbol modified
+ CycleError import import CycleError L51–51
~ musehub/services/musehub_proposals.py .py 7 symbols added, 7 symbols modified
+ CycleError import import CycleError L31–31
+ ProposalDag import import ProposalDag L31–31
+ blocked_by_numbers import import blocked_by_numbers L31–31
+ blocks_numbers import import blocks_numbers L31–31
+ create_dependency_edges import import create_dependency_edges L31–31
+ is_blocked import import is_blocked L31–31
+ load_dag_for_proposals import import load_dag_for_proposals L31–31

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:9de2441c818e8b0fba09c0fe251e96c1889b9cb287f4317f902ae1868f1cd127 --body "your comment"