[feature] wire cell-task wave chain + by-osmosis edge (sequencing S3)

Kind 2 (cell-task wave chain): a new cell-task under root-subtask UT_n
depends on every cell-task under every root-subtask in UT_n.dependency_ids
(the kind-1 wave-chain edges), so its branch carries the previous wave's
merged cell work. Re-derived from the root-subtask's deps, not the cell-task's
own dependency_ids (which also carry UX/product-fanout edges the by-osmosis
edge must not pick up). A root may fan to several cell-tasks (different cells),
so the previous wave's cell-task is a SET.

Kind 4 (by-osmosis): the first dev task (sequence 0) under a cell-task depends
on each predecessor cell-task's tail (max-sequence) dev task, so the new wave's
first branch carries the previous wave's fully-merged tail. Subsequent dev
tasks inherit the tail via kind 3 or the merged base.

Both wired from _create_subtask_from_inputs, dispatched on parent.team
(MAIN_PM -> kind 2; cell team -> kind 4). Pure helpers
(cell_task_wave_chain_depends_on, by_osmosis_tail_dev_tasks) unit-tested in
test_sequencing.py; TaskService methods integration-tested. Idempotent +
best-effort throughout (add_dependency dedupes; missing predecessors are
no-ops). Also fixes a latent mypy-tests gap (estimated_complexity required on
direct TaskCreateRequest calls in the S2 tests).
This commit is contained in:
Renn F
2026-06-28 04:22:27 +02:00
parent 12621a3608
commit 9927d248ea
5 changed files with 376 additions and 1 deletions
+83 -1
View File
@@ -17,7 +17,12 @@ from roboco.foundation.policy.sequencing.models import (
DraftSurface,
SequencingError,
)
from roboco.services.sequencing import SequencingService, dev_task_collision_edges
from roboco.services.sequencing import (
SequencingService,
by_osmosis_tail_dev_tasks,
cell_task_wave_chain_depends_on,
dev_task_collision_edges,
)
def _backend(_i: int) -> str:
@@ -283,3 +288,80 @@ def test_dev_collision_returns_depends_on_first_pairs() -> None:
[(dep, task)] = dev_task_collision_edges([first, second])
assert dep == first.id
assert task == second.id
# ---------------------------------------------------------------------------
# cell_task_wave_chain_depends_on — the cell-task wave chain (edge kind 2).
# Pure glue: a new cell-task under root-subtask UT_n depends on every cell-task
# under every root-subtask UT_n itself depends on (the kind-1 wave-chain edges).
# ---------------------------------------------------------------------------
def test_wave_chain_collects_all_predecessor_cell_tasks() -> None:
# Two predecessor root-subtasks: one fans to two cell-tasks, the other to one.
ct_a1, ct_a2, ct_b1 = _Sib(uuid4()), _Sib(uuid4()), _Sib(uuid4())
root_a, root_b = object(), object()
deps = cell_task_wave_chain_depends_on(
[root_a, root_b], {root_a: [ct_a1, ct_a2], root_b: [ct_b1]}
)
assert set(deps) == {ct_a1.id, ct_a2.id, ct_b1.id}
def test_wave_chain_empty_when_no_predecessor_roots() -> None:
assert cell_task_wave_chain_depends_on([], {}) == []
def test_wave_chain_skips_root_with_no_cell_tasks() -> None:
root = object()
assert cell_task_wave_chain_depends_on([root], {root: []}) == []
# A predecessor root absent from the map contributes nothing (no KeyError).
assert cell_task_wave_chain_depends_on([object()], {}) == []
def test_wave_chain_preserves_predecessor_order() -> None:
# Edges are appended in predecessor-root order then cell-task order — stable
# so add_dependency (which dedupes) sees a deterministic sequence.
ct_a, ct_b = _Sib(uuid4()), _Sib(uuid4())
root_a, root_b = object(), object()
deps = cell_task_wave_chain_depends_on(
[root_a, root_b], {root_a: [ct_a], root_b: [ct_b]}
)
assert deps == [ct_a.id, ct_b.id]
# ---------------------------------------------------------------------------
# by_osmosis_tail_dev_tasks — the by-osmosis edge (edge kind 4).
# Pure glue: the first dev task of a cell-task depends on each predecessor
# cell-task's tail (highest-sequence) dev task. Only sequence 0 carries it.
# ---------------------------------------------------------------------------
def test_by_osmosis_skips_non_first_dev_task() -> None:
tail = _Sib(uuid4(), sequence=2)
# is_first_dev_task=False -> no edges, regardless of predecessor groups.
assert by_osmosis_tail_dev_tasks(False, [[tail]]) == []
def test_by_osmosis_picks_max_sequence_per_group() -> None:
t0 = _Sib(uuid4(), sequence=0)
t1 = _Sib(uuid4(), sequence=1)
t2 = _Sib(uuid4(), sequence=2)
assert by_osmosis_tail_dev_tasks(True, [[t0, t1, t2]]) == [t2.id]
def test_by_osmosis_one_tail_per_predecessor_group() -> None:
a_tail = _Sib(uuid4(), sequence=2)
b_tail = _Sib(uuid4(), sequence=4)
a_group = [_Sib(uuid4(), sequence=0), _Sib(uuid4(), sequence=1), a_tail]
b_group = [_Sib(uuid4(), sequence=3), b_tail]
assert by_osmosis_tail_dev_tasks(True, [a_group, b_group]) == [a_tail.id, b_tail.id]
def test_by_osmosis_skips_empty_predecessor_group() -> None:
# A predecessor cell-task with no dev tasks contributes no edge.
tail = _Sib(uuid4(), sequence=1)
assert by_osmosis_tail_dev_tasks(True, [[], [tail]]) == [tail.id]
def test_by_osmosis_no_edges_when_no_predecessor_groups() -> None:
assert by_osmosis_tail_dev_tasks(True, []) == []