2026-07-27 11:27:06 +02:00
|
|
|
"""Proposal provider registry — K-Ext-1 / P6."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-07-27 11:48:10 +02:00
|
|
|
from dataclasses import dataclass, field
|
2026-07-27 11:27:06 +02:00
|
|
|
from typing import Any, Callable
|
|
|
|
|
|
|
|
|
|
from steering.eval_context import SteeringEvalContext
|
|
|
|
|
|
|
|
|
|
ProposalCompute = Callable[[SteeringEvalContext, dict[str, Any]], Any]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class ProposalProvider:
|
|
|
|
|
key: str
|
|
|
|
|
requires_elements: frozenset[str]
|
|
|
|
|
requires_data_slices: frozenset[str]
|
|
|
|
|
compute: ProposalCompute
|
2026-07-27 11:48:10 +02:00
|
|
|
excludes_elements: frozenset[str] = field(default_factory=frozenset)
|
2026-07-27 11:27:06 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
_PROVIDERS: list[ProposalProvider] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_proposal(provider: ProposalProvider) -> None:
|
|
|
|
|
_PROVIDERS.append(provider)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def compute_proposals(
|
|
|
|
|
ctx: SteeringEvalContext,
|
|
|
|
|
*,
|
|
|
|
|
read_models: dict[str, Any],
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
elements = ctx.steering_elements
|
|
|
|
|
slices = frozenset(ctx.data_slices)
|
|
|
|
|
result: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
for provider in _PROVIDERS:
|
|
|
|
|
if provider.requires_elements and not provider.requires_elements <= elements:
|
|
|
|
|
continue
|
2026-07-27 11:48:10 +02:00
|
|
|
if provider.excludes_elements and provider.excludes_elements & elements:
|
|
|
|
|
continue
|
2026-07-27 11:27:06 +02:00
|
|
|
if provider.requires_data_slices and not provider.requires_data_slices <= slices:
|
|
|
|
|
continue
|
|
|
|
|
result[provider.key] = provider.compute(ctx, read_models)
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _register_builtin_proposals() -> None:
|
2026-07-27 11:48:10 +02:00
|
|
|
from steering.proposals.gate_next_actions import propose_gate_next_actions
|
|
|
|
|
from steering.proposals.intake_triage import propose_intake_triage
|
2026-07-27 11:27:06 +02:00
|
|
|
from steering.proposals.sprint_commit import propose_sprint_commit
|
|
|
|
|
|
|
|
|
|
register_proposal(
|
|
|
|
|
ProposalProvider(
|
|
|
|
|
key="sprint_commit",
|
|
|
|
|
requires_elements=frozenset({"work_cycle_scope"}),
|
|
|
|
|
requires_data_slices=frozenset({"backlog", "work_cycles"}),
|
2026-07-27 11:48:10 +02:00
|
|
|
compute=lambda ctx, read_models: propose_sprint_commit(
|
|
|
|
|
ctx, read_models=read_models
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
register_proposal(
|
|
|
|
|
ProposalProvider(
|
|
|
|
|
key="gate_next_actions",
|
|
|
|
|
requires_elements=frozenset({"gate_fulfillment"}),
|
|
|
|
|
requires_data_slices=frozenset({"backlog", "roadmap", "actions"}),
|
|
|
|
|
compute=lambda ctx, read_models: propose_gate_next_actions(
|
|
|
|
|
ctx, read_models=read_models
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
register_proposal(
|
|
|
|
|
ProposalProvider(
|
|
|
|
|
key="intake_triage",
|
|
|
|
|
requires_elements=frozenset(),
|
|
|
|
|
requires_data_slices=frozenset({"backlog"}),
|
|
|
|
|
excludes_elements=frozenset({"work_cycle_scope"}),
|
|
|
|
|
compute=lambda ctx, read_models: propose_intake_triage(
|
|
|
|
|
ctx, read_models=read_models
|
|
|
|
|
),
|
2026-07-27 11:27:06 +02:00
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_register_builtin_proposals()
|