Task nodes carry wait_kind (external | human | both | unqualified, null unless wait): the hub field wins, otherwise derived from needs_human and dependency edges (task-level depends_on, or a workplan dependency row whose target the task cites). Workplan nodes carry blocked_kind (human | external | none, null unless blocked). Task-level depends_on becomes a task -> workplan/task depends_on edge; depends_on edges carry edge_kind commitment (cyan #0891b2), human gates are marked on the node (humanGate, rose #be123c). A needs_human=true query / "Needs human" toggle / --needs-human flag keeps only blocked_kind=human workplans and their human-wait tasks; the parameter survives in the shareable URL and mode switch like include_residuals. Fabric stays a read of State Hub. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: sonnet Assistant-Process: 237582@bnt-lap001 Assistant-Session: f2b3d9f1-8fb9-4b9c-bc2b-837ec5dfc826
337 lines
12 KiB
Python
337 lines
12 KiB
Python
from __future__ import annotations
|
|
|
|
from railiance_fabric.coordination_graph import coordination_graph_payload
|
|
from railiance_fabric.graph_explorer import fabric_graph_explorer_manifest
|
|
from railiance_fabric.schema_validation import draft202012_validator
|
|
|
|
|
|
def _validate_schema(name: str, document: dict) -> None:
|
|
from pathlib import Path
|
|
|
|
schema_path = Path("schemas") / name
|
|
validator = draft202012_validator(schema_path)
|
|
validator.validate(document)
|
|
|
|
|
|
def test_coordination_payload_filters_open_records_and_cites_wait() -> None:
|
|
workplans = [
|
|
{"id": "wp-a", "slug": "cust-wp-0071", "title": "Sizing", "status": "active"},
|
|
{"id": "wp-b", "slug": "rapps-wp-0014", "title": "Pilot", "status": "active"},
|
|
{"id": "wp-c", "slug": "done-wp", "title": "Finished", "status": "finished"},
|
|
]
|
|
tasks = [
|
|
{
|
|
"id": "t1",
|
|
"workplan_id": "wp-a",
|
|
"title": "Measure after RAPPS-WP-0014",
|
|
"status": "wait",
|
|
"description": "Await RAPPS-WP-0014 bind",
|
|
"needs_human": False,
|
|
},
|
|
{
|
|
"id": "t2",
|
|
"workplan_id": "wp-a",
|
|
"title": "Closed",
|
|
"status": "done",
|
|
},
|
|
{
|
|
"id": "t3",
|
|
"workplan_id": "wp-c",
|
|
"title": "orphan open on finished wp",
|
|
"status": "todo",
|
|
},
|
|
]
|
|
payload = coordination_graph_payload(workplans, tasks)
|
|
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
|
ids = {el["data"]["id"] for el in payload["elements"]}
|
|
assert "workplan:wp-a" in ids
|
|
assert "workplan:wp-b" in ids
|
|
assert "workplan:wp-c" not in ids
|
|
assert "task:t1" in ids
|
|
assert "task:t2" not in ids
|
|
assert "task:t3" not in ids
|
|
waits = [el for el in payload["elements"] if el["data"].get("edgeType") == "waits_on"]
|
|
assert waits
|
|
assert waits[0]["data"]["target"] == "workplan:wp-b"
|
|
assert payload["metrics"]["open_workplans"] == 2
|
|
assert "coordination" in {mode["id"] for mode in fabric_graph_explorer_manifest()["modes"]}
|
|
assert payload["metrics"]["wait_or_human_tasks"] == 1
|
|
assert waits[0]["data"]["edgeSource"] == "citation"
|
|
assert payload["metrics"]["depends_on_edges"] == 0
|
|
assert payload["metrics"]["residual_open_workplans"] == 0
|
|
|
|
|
|
def test_residuals_omitted_unless_included_and_titles_are_not_consulted() -> None:
|
|
workplans = [
|
|
{
|
|
"id": "wp-rel",
|
|
"slug": "cust-wp-0072",
|
|
"title": "Backfill",
|
|
"status": "active",
|
|
"flavor": "planning",
|
|
},
|
|
{
|
|
"id": "wp-res",
|
|
"slug": "fin-wp-0006",
|
|
"title": "Consume settlement",
|
|
"status": "proposed",
|
|
"flavor": "residual",
|
|
},
|
|
{
|
|
"id": "wp-named",
|
|
"slug": "upc-wp-0002",
|
|
"title": "Company vessel close-out (G1 + residual G2/G8)",
|
|
"status": "active",
|
|
"flavor": "implementation",
|
|
},
|
|
]
|
|
tasks = [
|
|
{
|
|
"id": "t-res",
|
|
"workplan_id": "wp-res",
|
|
"title": "Leftover task",
|
|
"status": "todo",
|
|
"flavor": "residual",
|
|
}
|
|
]
|
|
hidden = coordination_graph_payload(workplans, tasks)
|
|
ids = {el["data"]["id"] for el in hidden["elements"]}
|
|
assert "workplan:wp-rel" in ids
|
|
assert "workplan:wp-named" in ids
|
|
assert "workplan:wp-res" not in ids
|
|
assert "task:t-res" not in ids
|
|
assert hidden["metrics"]["residual_open_workplans"] == 1
|
|
assert hidden["metrics"]["include_residuals"] is False
|
|
|
|
shown = coordination_graph_payload(workplans, tasks, include_residuals=True)
|
|
shown_ids = {el["data"]["id"] for el in shown["elements"]}
|
|
assert "workplan:wp-res" in shown_ids
|
|
assert "task:t-res" in shown_ids
|
|
residual_node = next(
|
|
el for el in shown["elements"] if el["data"]["id"] == "workplan:wp-res"
|
|
)
|
|
assert residual_node["data"]["flavor"] == "residual"
|
|
assert residual_node["data"]["nodeClass"] == "residual"
|
|
|
|
|
|
def test_indexed_depends_on_outranks_citation_fallback() -> None:
|
|
workplans = [
|
|
{
|
|
"id": "wp-a",
|
|
"slug": "cust-wp-0072",
|
|
"title": "Backfill",
|
|
"status": "active",
|
|
"flavor": "planning",
|
|
"depends_on": [{"workplan_id": "wp-b", "workplan_slug": "state-wp-0092"}],
|
|
},
|
|
{
|
|
"id": "wp-b",
|
|
"slug": "state-wp-0092",
|
|
"title": "Flavor schema",
|
|
"status": "active",
|
|
"flavor": "planning",
|
|
},
|
|
]
|
|
tasks = [
|
|
{
|
|
"id": "t1",
|
|
"workplan_id": "wp-a",
|
|
"title": "Wait for STATE-WP-0092",
|
|
"status": "wait",
|
|
"description": "Blocked on STATE-WP-0092",
|
|
}
|
|
]
|
|
payload = coordination_graph_payload(workplans, tasks)
|
|
edges = [el["data"] for el in payload["elements"] if el["data"].get("kind") == "Edge"]
|
|
depends = [e for e in edges if e.get("edgeType") == "depends_on"]
|
|
waits = [e for e in edges if e.get("edgeType") == "waits_on"]
|
|
assert len(depends) == 1
|
|
assert depends[0]["source"] == "workplan:wp-a"
|
|
assert depends[0]["target"] == "workplan:wp-b"
|
|
assert depends[0]["edgeSource"] == "indexed"
|
|
assert waits == []
|
|
assert payload["metrics"]["depends_on_edges"] == 1
|
|
assert payload["metrics"]["citation_edges"] == 0
|
|
chokepoint = next(
|
|
el["data"]["chokepoint"]
|
|
for el in payload["elements"]
|
|
if el["data"]["id"] == "workplan:wp-b"
|
|
)
|
|
assert chokepoint == 1
|
|
sizes = {
|
|
el["data"]["id"]: el["data"]["visualSize"]
|
|
for el in payload["elements"]
|
|
if el["data"]["id"].startswith("workplan:")
|
|
}
|
|
assert sizes["workplan:wp-b"] > sizes["workplan:wp-a"]
|
|
assert all(e["edgeWidth"] > 0 for e in edges)
|
|
|
|
|
|
def test_canonical_id_depends_on_resolves_to_hub_uuid() -> None:
|
|
workplans = [
|
|
{
|
|
"id": "uuid-a",
|
|
"slug": "rail-fab-wp-0030",
|
|
"title": "Graph",
|
|
"status": "proposed",
|
|
"depends_on": ["STATE-WP-0092"],
|
|
},
|
|
{
|
|
"id": "uuid-b",
|
|
"slug": "state-wp-0092",
|
|
"title": "Schema",
|
|
"status": "active",
|
|
},
|
|
]
|
|
payload = coordination_graph_payload(workplans, [])
|
|
depends = [
|
|
el["data"]
|
|
for el in payload["elements"]
|
|
if el["data"].get("edgeType") == "depends_on"
|
|
]
|
|
assert depends
|
|
assert depends[0]["target"] == "workplan:uuid-b"
|
|
|
|
|
|
def _wait_fixture() -> tuple[list[dict], list[dict]]:
|
|
workplans = [
|
|
{
|
|
"id": "wp-h",
|
|
"slug": "rail-wp-0015",
|
|
"title": "Backups",
|
|
"status": "blocked",
|
|
"depends_on": [{"workplan_id": "wp-ext", "workplan_slug": "flex-wp-0020"}],
|
|
},
|
|
{"id": "wp-ext", "slug": "flex-wp-0020", "title": "Rename", "status": "active"},
|
|
{"id": "wp-e", "slug": "key-wp-0035", "title": "Keys", "status": "blocked"},
|
|
{"id": "wp-u", "slug": "cust-wp-0060", "title": "Loose", "status": "blocked"},
|
|
]
|
|
tasks = [
|
|
{
|
|
"id": "t-human",
|
|
"workplan_id": "wp-h",
|
|
"title": "Operator provisions creds",
|
|
"status": "wait",
|
|
"needs_human": True,
|
|
"blocking_reason": "operator must provision NC_WEBDAV/AGE creds to OpenBao",
|
|
},
|
|
{
|
|
"id": "t-both",
|
|
"workplan_id": "wp-h",
|
|
"title": "Both",
|
|
"status": "wait",
|
|
"needs_human": True,
|
|
"depends_on": ["FLEX-WP-0020"],
|
|
},
|
|
{
|
|
"id": "t-ext-cited",
|
|
"workplan_id": "wp-h",
|
|
"title": "External by indexed citation",
|
|
"status": "wait",
|
|
"blocking_reason": "FLEX-WP-0020 rename must land first",
|
|
},
|
|
{
|
|
"id": "t-ext",
|
|
"workplan_id": "wp-e",
|
|
"title": "External by task depends_on",
|
|
"status": "wait",
|
|
"depends_on": ["t-both"],
|
|
},
|
|
{"id": "t-todo", "workplan_id": "wp-e", "title": "Open", "status": "todo"},
|
|
{
|
|
"id": "t-unq",
|
|
"workplan_id": "wp-u",
|
|
"title": "Nothing qualifies",
|
|
"status": "wait",
|
|
"blocking_reason": "waiting, see nowhere",
|
|
},
|
|
]
|
|
return workplans, tasks
|
|
|
|
|
|
def _by_id(payload: dict) -> dict[str, dict]:
|
|
return {el["data"]["id"]: el["data"] for el in payload["elements"]}
|
|
|
|
|
|
def test_wait_kind_derived_from_needs_human_and_dependency_edges() -> None:
|
|
workplans, tasks = _wait_fixture()
|
|
payload = coordination_graph_payload(workplans, tasks)
|
|
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
|
data = _by_id(payload)
|
|
assert data["task:t-human"]["wait_kind"] == "human"
|
|
assert data["task:t-human"]["humanGate"] is True
|
|
assert data["task:t-both"]["wait_kind"] == "both"
|
|
assert data["task:t-both"]["humanGate"] is True
|
|
assert data["task:t-ext-cited"]["wait_kind"] == "external"
|
|
assert data["task:t-ext"]["wait_kind"] == "external"
|
|
assert data["task:t-ext"]["humanGate"] is False
|
|
assert data["task:t-unq"]["wait_kind"] == "unqualified"
|
|
assert data["task:t-todo"]["wait_kind"] is None
|
|
assert payload["metrics"]["wait_both_tasks"] == 1
|
|
assert payload["metrics"]["wait_external_tasks"] == 2
|
|
assert payload["metrics"]["wait_human_tasks"] == 1
|
|
assert payload["metrics"]["wait_unqualified_tasks"] == 1
|
|
|
|
|
|
def test_hub_provided_wait_kind_and_blocked_kind_win_over_derivation() -> None:
|
|
workplans, tasks = _wait_fixture()
|
|
tasks[0]["wait_kind"] = "external"
|
|
tasks[5]["wait_kind"] = "waiting-external"
|
|
workplans[3]["blocked_kind"] = "blocked-human"
|
|
payload = coordination_graph_payload(workplans, tasks)
|
|
data = _by_id(payload)
|
|
assert data["task:t-human"]["wait_kind"] == "external"
|
|
assert data["task:t-unq"]["wait_kind"] == "external"
|
|
assert data["workplan:wp-u"]["blocked_kind"] == "human"
|
|
tasks[5]["wait_kind"] = "bogus"
|
|
data = _by_id(coordination_graph_payload(workplans, tasks))
|
|
assert data["task:t-unq"]["wait_kind"] == "unqualified"
|
|
|
|
|
|
def test_blocked_kind_aggregates_wait_tasks_and_is_null_unless_blocked() -> None:
|
|
workplans, tasks = _wait_fixture()
|
|
data = _by_id(coordination_graph_payload(workplans, tasks))
|
|
assert data["workplan:wp-h"]["blocked_kind"] == "human"
|
|
assert data["workplan:wp-e"]["blocked_kind"] == "external"
|
|
assert data["workplan:wp-u"]["blocked_kind"] == "none"
|
|
assert data["workplan:wp-ext"]["blocked_kind"] is None
|
|
|
|
|
|
def test_dependency_rows_qualify_cited_wait_as_external() -> None:
|
|
workplans, tasks = _wait_fixture()
|
|
workplans[0].pop("depends_on")
|
|
data = _by_id(coordination_graph_payload(workplans, tasks))
|
|
assert data["task:t-ext-cited"]["wait_kind"] == "unqualified"
|
|
rows = [{"id": "d1", "from_workplan_id": "wp-h", "to_workplan_id": "wp-ext", "to_task_id": None}]
|
|
data = _by_id(coordination_graph_payload(workplans, tasks, dependencies=rows))
|
|
assert data["task:t-ext-cited"]["wait_kind"] == "external"
|
|
|
|
|
|
def test_edge_kind_marks_commitments_and_task_depends_on_edges() -> None:
|
|
workplans, tasks = _wait_fixture()
|
|
payload = coordination_graph_payload(workplans, tasks)
|
|
data = _by_id(payload)
|
|
assert data["edge:depends:wp-h:wp-ext"]["edge_kind"] == "commitment"
|
|
task_edge = data["edge:task-depends:t-both:workplan:wp-ext"]
|
|
assert task_edge["edgeType"] == "depends_on"
|
|
assert task_edge["edgeSource"] == "task"
|
|
assert task_edge["edge_kind"] == "commitment"
|
|
assert data["edge:task-depends:t-ext:task:t-both"]["targetLayer"] == "task"
|
|
assert payload["metrics"]["task_depends_on_edges"] == 2
|
|
assert data["workplan:wp-ext"]["chokepoint"] == 2
|
|
assert not any(k.startswith("edge:cites:") for k in data)
|
|
|
|
|
|
def test_needs_human_only_keeps_human_blocked_workplans_and_their_human_waits() -> None:
|
|
workplans, tasks = _wait_fixture()
|
|
payload = coordination_graph_payload(workplans, tasks, needs_human_only=True)
|
|
_validate_schema("graph-explorer-payload.schema.yaml", payload)
|
|
data = _by_id(payload)
|
|
nodes = {k for k, v in data.items() if v["kind"] != "Edge"}
|
|
assert nodes == {"workplan:wp-h", "task:t-human", "task:t-both"}
|
|
edges = {k for k, v in data.items() if v["kind"] == "Edge"}
|
|
assert edges == {"edge:belongs:t-human", "edge:belongs:t-both"}
|
|
assert payload["metrics"]["needs_human_only"] is True
|
|
assert payload["metrics"]["open_workplans"] == 1
|
|
assert payload["metrics"]["open_tasks"] == 2
|
|
assert coordination_graph_payload(workplans, tasks)["metrics"]["needs_human_only"] is False
|