Feat v0.3.4 team roles + multi-party validation (#18)

Custom team roles: removed CHECK constraint on team_members.role,
roles stored in teams.settings, roles API, stage_config.required_role
enforced on claim. Multi-party signoff: workflow_signoffs table,
validation gate in advanceInternal, SubmitSignoff engine method,
signoff HTTP API. Frontend: dynamic role management, stage config
validation UI, signoff panel. Design docs for extension lifecycle
and trigger composition. 20 store tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 00:22:04 +00:00
parent dba718b914
commit 748da6c2b4
22 changed files with 952 additions and 19 deletions

View File

@@ -660,6 +660,48 @@ func (s *WorkflowStore) ListAssignmentsByUser(ctx context.Context, userID string
return s.queryAssignments(ctx, q, args...)
}
// ── Signoffs (v0.3.4) ─────────────────────────
func (s *WorkflowStore) CreateSignoff(ctx context.Context, so *models.WorkflowSignoff) error {
return DB.QueryRowContext(ctx, `
INSERT INTO workflow_signoffs (instance_id, stage, user_id, decision, comment)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, created_at`,
so.InstanceID, so.Stage, so.UserID, so.Decision, so.Comment,
).Scan(&so.ID, &so.CreatedAt)
}
func (s *WorkflowStore) ListSignoffs(ctx context.Context, instanceID, stage string) ([]models.WorkflowSignoff, error) {
rows, err := DB.QueryContext(ctx, `
SELECT id, instance_id, stage, user_id, decision, comment, created_at
FROM workflow_signoffs
WHERE instance_id = $1 AND stage = $2
ORDER BY created_at`, instanceID, stage)
if err != nil {
return nil, err
}
defer rows.Close()
var result []models.WorkflowSignoff
for rows.Next() {
var so models.WorkflowSignoff
if err := rows.Scan(&so.ID, &so.InstanceID, &so.Stage, &so.UserID,
&so.Decision, &so.Comment, &so.CreatedAt); err != nil {
return nil, err
}
result = append(result, so)
}
return result, rows.Err()
}
func (s *WorkflowStore) CountSignoffs(ctx context.Context, instanceID, stage, decision string) (int, error) {
var count int
err := DB.QueryRowContext(ctx, `
SELECT COUNT(*) FROM workflow_signoffs
WHERE instance_id = $1 AND stage = $2 AND decision = $3`,
instanceID, stage, decision).Scan(&count)
return count, err
}
func (s *WorkflowStore) queryAssignments(ctx context.Context, q string, args ...interface{}) ([]models.WorkflowAssignment, error) {
rows, err := DB.QueryContext(ctx, q, args...)
if err != nil {