Files
kandev/apps/backend/internal/workflow/adapters/adapters.go
T
Carlos Florêncio 2d73a3d1db fix: keep completed office comments editable (#1548)
* fix: keep completed office comments editable

Keep the office task detail composer editable after completed sessions load,

including projectless office tasks.

Update verification tests for SHA-pinned GitHub actions. Extract the

dockview main area to satisfy the existing max-lines lint rule.

* fix: preserve upload artifact version guard

* fix: clarify upload artifact pin check

* fix: wake completed office sessions on comments

* fix: queue completed office comment runs

* fix: preserve dispatcher session lookup errors

* fix: resolve office comment wake review issues

* fix: stabilize comment wake idempotency

* fix: salt custom comment wake keys

* fix: address comment wake review cleanup

* fix: avoid comment payload allocation overflow pattern

* fix: salt comment wake action variants

* fix: resolve cross-task primary by target step

* fix: resolve cross-task participant roles

* fix: surface cross-task comment run status

* fix: use target steps for cross-task queue runs

* fix: avoid comment run lookup scan regression

* fix: cover cross-task queue run edge cases

* fix: index comment payload run lookup

* fix: support postgres comment run index

* fix: tighten queue run target test coverage

* fix: route source comment run updates

* fix: preserve cross-task comment wakes

* fix: tighten comment wake identity

* fix: align comment wake routing

* fix: remove stale primary resolver

* fix: dedupe comment run status

* fix: tighten legacy comment wake dedupe

* fix: remove unreachable comment wake guard

* fix: skip duplicate comment reactivity wakes

* fix: route comment wakes synchronously

* fix: share comment engine dispatch contracts

* fix: preserve comment wake fallbacks

* fix: preserve done comment runner

* fix: guard empty task runner fallback

* fix: handle empty task runner fallback

* fix: stabilize completed comment wakes

* fix: index comment runner fallback

---------

Co-authored-by: root <root@kandev.ts.net>
2026-06-30 13:10:40 +01:00

164 lines
5.9 KiB
Go

// Package adapters implements the workflow engine's participant, decision,
// and primary-agent adapter interfaces against the workflow repository.
//
// The engine declares the interfaces in internal/workflow/engine/adapters.go
// and internal/workflow/engine/phase2_callbacks.go; this package wires them
// to the workflow_step_participants, workflow_step_decisions, and
// workflow_steps tables so callers (cmd/kandev/main.go) can compose an
// office-grade engine without leaking model imports into the engine package.
package adapters
import (
"context"
"fmt"
"github.com/kandev/kandev/internal/workflow/engine"
"github.com/kandev/kandev/internal/workflow/models"
"github.com/kandev/kandev/internal/workflow/repository"
)
// WorkflowRepo captures the subset of *repository.Repository methods the
// adapters need. Defined as an interface so tests can swap in fakes.
type WorkflowRepo interface {
ListStepParticipantsForTask(ctx context.Context, stepID, taskID string) ([]*models.WorkflowStepParticipant, error)
ListStepDecisions(ctx context.Context, taskID, stepID string) ([]*models.WorkflowStepDecision, error)
RecordStepDecision(ctx context.Context, d *models.WorkflowStepDecision) error
ClearStepDecisions(ctx context.Context, taskID, stepID string) (int64, error)
ResolveCurrentRunner(ctx context.Context, stepID, taskID string) (string, error)
GetTaskWorkflowStepID(ctx context.Context, taskID string) (string, error)
}
// Compile-time check that *repository.Repository satisfies WorkflowRepo.
var _ WorkflowRepo = (*repository.Repository)(nil)
// ParticipantAdapter implements engine.ParticipantStore.
type ParticipantAdapter struct {
Repo WorkflowRepo
}
// NewParticipantAdapter builds a ParticipantAdapter wrapping the workflow repo.
func NewParticipantAdapter(repo WorkflowRepo) *ParticipantAdapter {
return &ParticipantAdapter{Repo: repo}
}
// ListStepParticipants satisfies engine.ParticipantStore. The Wave 8 (ADR-0004)
// dual-scoped table — template-level rows (task_id=”) merged with per-task
// overrides — is resolved by the repository's ListStepParticipantsForTask.
func (a *ParticipantAdapter) ListStepParticipants(
ctx context.Context, stepID, taskID string,
) ([]engine.ParticipantInfo, error) {
rows, err := a.Repo.ListStepParticipantsForTask(ctx, stepID, taskID)
if err != nil {
return nil, fmt.Errorf("list step participants: %w", err)
}
out := make([]engine.ParticipantInfo, 0, len(rows))
for _, p := range rows {
out = append(out, engine.ParticipantInfo{
ID: p.ID,
StepID: p.StepID,
TaskID: p.TaskID,
Role: string(p.Role),
AgentProfileID: p.AgentProfileID,
DecisionRequired: p.DecisionRequired,
Position: p.Position,
})
}
return out, nil
}
// WorkflowStepIDForTask satisfies engine.TargetTaskStepResolver.
func (a *ParticipantAdapter) WorkflowStepIDForTask(ctx context.Context, taskID string) (string, error) {
return a.Repo.GetTaskWorkflowStepID(ctx, taskID)
}
// DecisionAdapter implements engine.DecisionStore.
type DecisionAdapter struct {
Repo WorkflowRepo
}
// NewDecisionAdapter builds a DecisionAdapter wrapping the workflow repo.
func NewDecisionAdapter(repo WorkflowRepo) *DecisionAdapter {
return &DecisionAdapter{Repo: repo}
}
// ListStepDecisions satisfies engine.DecisionStore.
func (a *DecisionAdapter) ListStepDecisions(
ctx context.Context, taskID, stepID string,
) ([]engine.DecisionInfo, error) {
rows, err := a.Repo.ListStepDecisions(ctx, taskID, stepID)
if err != nil {
return nil, fmt.Errorf("list step decisions: %w", err)
}
out := make([]engine.DecisionInfo, 0, len(rows))
for _, d := range rows {
out = append(out, engine.DecisionInfo{
ID: d.ID,
TaskID: d.TaskID,
StepID: d.StepID,
ParticipantID: d.ParticipantID,
Decision: d.Decision,
Note: d.Note,
})
}
return out, nil
}
// RecordStepDecision satisfies engine.DecisionStore.
func (a *DecisionAdapter) RecordStepDecision(
ctx context.Context, d engine.DecisionInfo,
) error {
row := &models.WorkflowStepDecision{
ID: d.ID,
TaskID: d.TaskID,
StepID: d.StepID,
ParticipantID: d.ParticipantID,
Decision: d.Decision,
Note: d.Note,
}
return a.Repo.RecordStepDecision(ctx, row)
}
// ClearStepDecisions satisfies engine.DecisionStore.
func (a *DecisionAdapter) ClearStepDecisions(
ctx context.Context, taskID, stepID string,
) (int64, error) {
return a.Repo.ClearStepDecisions(ctx, taskID, stepID)
}
// PrimaryAgentAdapter implements engine.PrimaryAgentResolver. The "primary"
// agent for a task is its current runner participant, falling back to the
// workflow step's agent_profile_id when no task-specific runner exists.
type PrimaryAgentAdapter struct {
Repo WorkflowRepo
}
// NewPrimaryAgentAdapter builds a PrimaryAgentAdapter wrapping the workflow repo.
func NewPrimaryAgentAdapter(repo WorkflowRepo) *PrimaryAgentAdapter {
return &PrimaryAgentAdapter{Repo: repo}
}
// PrimaryAgentProfileID satisfies engine.PrimaryAgentResolver.
func (a *PrimaryAgentAdapter) PrimaryAgentProfileID(
ctx context.Context, stepID, taskID string,
) (string, error) {
agentID, err := a.Repo.ResolveCurrentRunner(ctx, stepID, taskID)
if err != nil {
return "", fmt.Errorf("resolve current runner for step %s task %s: %w", stepID, taskID, err)
}
return agentID, nil
}
// WorkflowStepIDForTask satisfies engine.TargetTaskStepResolver.
func (a *PrimaryAgentAdapter) WorkflowStepIDForTask(ctx context.Context, taskID string) (string, error) {
return a.Repo.GetTaskWorkflowStepID(ctx, taskID)
}
// Compile-time interface assertions.
var (
_ engine.ParticipantStore = (*ParticipantAdapter)(nil)
_ engine.TargetTaskStepResolver = (*ParticipantAdapter)(nil)
_ engine.DecisionStore = (*DecisionAdapter)(nil)
_ engine.PrimaryAgentResolver = (*PrimaryAgentAdapter)(nil)
_ engine.TargetTaskStepResolver = (*PrimaryAgentAdapter)(nil)
)