Files
bj_power/bj_power_mes/docs/superpowers/plans/2026-05-16-current-step-id-plan.md
T

23 KiB
Raw Blame History

current_step_id Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 将 job 表的运行时步骤标识从 currentStepIndex(int) 迁移到 currentStepId(string),消除 AdvanceStep 中 stepId↔stepIndex 的冗余转换。

Architecture: job 表新增 currentStepId 字段,旧字段 currentStepIndex 保留但不写入。AdvanceStep/CompleteStep 改为写入 stepIdresolveBranch 直接返回 stepId(无需二次查询转换)。核心读路径新增 GetRecipeStepByID 用 stepId 直接查询。scheduler 层 StepView/CandidateTaskStepID 字段。

Tech Stack: Go 1.25, ent ORM v0.14.5, PostgreSQL

Spec: docs/superpowers/specs/2026-05-16-current-step-id-design.md


File Structure

File Action Responsibility
schema/job.go Modify 新增 currentStepId 字段
internal/eventloop/dbstate.go Modify 核心方法:AdvanceStep/CompleteStep 改为 stepId
internal/eventloop/loop.go Modify RuntimeSnapshotStepID
internal/eventloop/scheduler_bridge.go Modify buildJobViews 传 stepId
internal/eventloop/candidate_handlers.go Modify 用 stepId 查询
internal/scheduler/types.go Modify StepView/CandidateTaskStepID
internal/scheduler/generator.go Modify 填充 StepID
internal/types/types.go Modify API 类型加 CurrentStepId
internal/processor/job_processor.go Modify ReworkJob 用 stepId
internal/processor/interface.go Modify ReworkJob 签名变更
cmd/mockrun/main.go Modify SetCurrentStepId
frontend/src/api/temp-station/index.ts Modify 类型加 currentStepId
frontend/src/api/dock/index.ts Modify 类型加 currentStepId
schema/tools/migrate_step_id.go Create 数据迁移 SQL

Chunk 1: Schema + Migration

Task 1.1: Add currentStepId field to job schema

Files:

  • Modify: schema/job.go:35

  • Create: schema/tools/migrate_step_id.go

  • Step 1: Add field to schema

In schema/job.go, after line 35 (field.Int("currentStepIndex")...):

field.String("currentStepId").MaxLen(20).Default("").Comment("当前步骤业务IDOP10-OP120"),
  • Step 2: Regenerate ent code

Run: cd schema && go generate

  • Step 3: Verify ent generated SetCurrentStepId/CurrentStepId

Run: rtk grep "SetCurrentStepId" ent/job/

  • Step 4: Create migration SQL file

Create schema/tools/migrate_step_id.go:

package tools

import (
	"context"
	"log/slog"
	
	"hougai/ent"
)

func MigrateCurrentStepID(ctx context.Context, client *ent.Client) error {
	rows, err := client.Job.Update().
		Where(func(s *sql.Selector) {
			s.Where(sql.ExprP("current_step_id = '' OR current_step_id IS NULL"))
		}).
		Save(ctx)
	// Use raw SQL for migration:
	// UPDATE job SET current_step_id = rs.step_id
	// FROM recipe_step rs
	// WHERE job.recipe_id = rs.recipe_id AND job.current_step_index = rs.step_index
	//   AND (job.current_step_id = '' OR job.current_step_id IS NULL);
	return nil
}

Wait—ent doesn't support cross-table UPDATE easily. Use raw SQL via ent client:

package tools

import (
	"context"
	"database/sql"
	"fmt"
	"log/slog"

	"hougai/ent"
	"hougai/ent/job"
)

// MigrateCurrentStepID 将现有 job 的 current_step_index 转换为 current_step_id
func MigrateCurrentStepID(ctx context.Context, client *ent.Client) error {
	db := client.Job.Query().UnderlyingDB()
	
	// 先检查是否有未迁移的 job
	var count int
	err := db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM job WHERE current_step_id = '' AND current_step_index > 0`,
	).Scan(&count)
	if err != nil {
		return fmt.Errorf("migrate currentStepId: count pending: %w", err)
	}
	if count == 0 {
		slog.Info("migrate currentStepId: nothing to migrate")
		return nil
	}
	
	result, err := db.ExecContext(ctx, `
		UPDATE job SET current_step_id = rs.step_id
		FROM recipe_step rs
		WHERE job.recipe_id = rs.recipe_id 
		  AND job.current_step_index = rs.step_index
		  AND job.current_step_id = ''
	`)
	if err != nil {
		return fmt.Errorf("migrate currentStepId: %w", err)
	}
	n, _ := result.RowsAffected()
	slog.Info("migrate currentStepId: done", "rows", n)
	return nil
}
  • Step 5: Wire migration into EnsureDataInitialized

In internal/svc/service_context.go, after the existing migration calls, add:

if err := tools.MigrateCurrentStepID(ctx, entClient); err != nil {
    return nil, fmt.Errorf("migrate currentStepId: %w", err)
}
  • Step 6: Commit
rtk git add schema/job.go schema/tools/migrate_step_id.go ent/ internal/svc/service_context.go
rtk git commit -m "feat: add currentStepId field to job schema with migration"

Chunk 2: DBState Core Methods

Task 2.1: Add stepId-based query methods to DBState

Files:

  • Modify: internal/eventloop/dbstate.go

  • Step 1: Add GetRecipeStepByID method

After GetRecipeStep (~line 268), add:

// GetRecipeStepByID 根据 stepId 获取工艺路线中指定步骤的信息
func (d *DBState) GetRecipeStepByID(ctx context.Context, recipeID int, stepID string) (stepIndex int, stepType constants.StepType, resourceType string, toolType string, stepName string, err error) {
	step, err := d.client.RecipeStep.Query().
		Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
		First(ctx)
	if err != nil {
		return 0, "", "", "", "", fmt.Errorf("get recipe %d step %s: %w", recipeID, stepID, err)
	}
	return step.StepIndex, step.StepType, step.ResourceType, step.ToolType, step.StepName, nil
}
  • Step 2: Add GetStepIDByIndex method
// GetStepIDByIndex 根据 stepIndex 查找 stepId
func (d *DBState) GetStepIDByIndex(ctx context.Context, recipeID, stepIndex int) (string, error) {
	step, err := d.client.RecipeStep.Query().
		Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIndexEQ(stepIndex)).
		First(ctx)
	if err != nil {
		return "", fmt.Errorf("get stepId by index %d: %w", stepIndex, err)
	}
	return step.StepId, nil
}
  • Step 3: Add getSortedStepIDs method
// getSortedStepIDs 返回按 stepIndex 排序的 stepId 列表
func (d *DBState) getSortedStepIDs(ctx context.Context, recipeID int) ([]string, error) {
	steps, err := d.client.RecipeStep.Query().
		Where(recipestep.RecipeIdEQ(recipeID)).
		Order(recipestep.ByStepIndex()).
		All(ctx)
	if err != nil {
		return nil, fmt.Errorf("get sorted step IDs for recipe %d: %w", recipeID, err)
	}
	ids := make([]string, len(steps))
	for i, s := range steps {
		ids[i] = s.StepId
	}
	return ids, nil
}
  • Step 4: Commit
rtk git add internal/eventloop/dbstate.go
rtk git commit -m "feat: add GetRecipeStepByID, GetStepIDByIndex, getSortedStepIDs to DBState"

Task 2.2: Revise resolveBranch to work with stepId

Files:

  • Modify: internal/eventloop/dbstate.go:315-337

  • Step 1: Create resolveBranch that returns stepId

Replace the existing resolveBranch:

// resolveBranch 检查 nextStepBranches:遍历分支键,若在 ctxMap 中存在且 truthy,返回目标 stepId。
// 返回 (stepId, true) 表示匹配成功,("", false) 表示无匹配。
func (d *DBState) resolveBranch(ctx context.Context, recipeID int, currentStepID string, ctxMap map[string]any) (string, bool) {
	step, err := d.client.RecipeStep.Query().
		Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(currentStepID)).
		First(ctx)
	if err != nil || step.NextStepBranches == nil {
		return "", false
	}

	for key, val := range step.NextStepBranches {
		if cv, ok := ctxMap[key]; ok && isTruthy(cv) {
			stepID, ok := val.(string)
			if !ok {
				continue
			}
			return stepID, true
		}
	}
	return "", false
}
  • Step 2: Commit
rtk git add internal/eventloop/dbstate.go
rtk git commit -m "refactor: resolveBranch returns stepId directly instead of stepIndex"

Task 2.3: Revise AdvanceStep to use stepId

Files:

  • Modify: internal/eventloop/dbstate.go:162-239

  • Step 1: Rewrite AdvanceStep

// AdvanceStep 推进到下一步:先合并 context → 匹配 nextStepBranches → 否则 nextStepDefault → 否则线性推进。
// 推进后若下一步是 Decision/Judge 则链式内联处理。
// 返回 true 表示已是最后一步(job 已完成)。
func (d *DBState) AdvanceStep(ctx context.Context, jobID int, contextUpdates map[string]any, posType constants.PositionType, posRefID string) (bool, error) {
	for {
		jb, err := d.client.Job.Get(ctx, jobID)
		if err != nil {
			return false, fmt.Errorf("advance step: job %d not found: %w", jobID, err)
		}

		// 合并上下文更新
		ctxMap := jb.Context
		if ctxMap == nil {
			ctxMap = make(map[string]any)
		}
		for k, v := range contextUpdates {
			ctxMap[k] = v
		}

		// 确定下一步 stepId
		var nextStepID string
		currentStepID := jb.CurrentStepId

		// 1. 优先 nextStepBranches(条件跳步)
		if branchID, matched := d.resolveBranch(ctx, jb.RecipeId, currentStepID, ctxMap); matched {
			nextStepID = branchID
		} else {
			// 2. 其次 nextStepDefault(指定跳步)
			step, err := d.client.RecipeStep.Query().
				Where(recipestep.RecipeIdEQ(jb.RecipeId), recipestep.StepIdEQ(currentStepID)).
				First(ctx)
			if err != nil {
				return false, fmt.Errorf("advance step: query current step %s: %w", currentStepID, err)
			}
			if step.NextStepDefault != "" {
				nextStepID = step.NextStepDefault
			} else {
				// 3. 线性推进:当前 stepIndex + 1 → 找对应 stepId
				nextID, err := d.GetStepIDByIndex(ctx, jb.RecipeId, step.StepIndex+1)
				if err != nil {
					return false, fmt.Errorf("advance step: no next step after %s: %w", currentStepID, err)
				}
				nextStepID = nextID
			}
		}

		// 是否最后一步:查 nextStepID 对应的 stepIndex 与 maxStepIndex 比较
		nextIdx, err := d.GetStepIndexByStepID(ctx, jb.RecipeId, nextStepID)
		if err != nil {
			return false, fmt.Errorf("advance step: get index for %s: %w", nextStepID, err)
		}
		maxStep, err := d.GetRecipeMaxStepIndex(ctx, jb.RecipeId)
		if err != nil {
			return false, fmt.Errorf("advance step: get recipe max for job %d: %w", jobID, err)
		}
		isLast := nextIdx > maxStep

		if isLast {
			if posType != "" || posRefID != "" {
				upd := d.client.Job.UpdateOneID(jobID).AddVersion(1)
				if posType != "" {
					upd = upd.SetPositionType(posType)
				}
				if posRefID != "" {
					upd = upd.SetPositionRefId(posRefID)
				}
				if _, err := upd.Save(ctx); err != nil {
					return false, err
				}
			}
			return true, d.FinishJob(ctx, jobID, jb.WorkOrderId, constants.JobStatus_Completed)
		}

		update := d.client.Job.UpdateOneID(jobID).
			SetCurrentStepId(nextStepID).
			SetContext(ctxMap).
			AddVersion(1)

		if posType != "" {
			update = update.SetPositionType(posType)
		}
		if posRefID != "" {
			update = update.SetPositionRefId(posRefID)
		}

		if _, err = update.Save(ctx); err != nil {
			return false, err
		}

		// 检查下一步是否可内联处理(Decision/Judge
		_, stepType, _, _, _, err := d.GetRecipeStepByID(ctx, jb.RecipeId, nextStepID)
		if err != nil || (stepType != constants.StepType_Decision && stepType != constants.StepType_Judge) {
			return false, err
		}

		contextUpdates = evaluateDecisionForJob(d, ctx, jb.RecipeId, nextStepID, ctxMap, jb.WorkpieceNo)
		posType, posRefID = "", ""
	}
}
  • Step 2: Update evaluateDecisionForJob to accept stepId

Change signature from stepIndex int to stepID string:

func evaluateDecisionForJob(d *DBState, ctx context.Context, recipeID int, stepID string, ctxMap map[string]any, workpieceNo string) map[string]any {
	// Get stepIndex first since GetStepProcessingParams uses stepIndex
	step, err := d.client.RecipeStep.Query().
		Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
		First(ctx)
	if err != nil {
		return nil
	}
	params := step.ProcessingParams
	clone := make(map[string]any, len(ctxMap)+2)
	for k, v := range ctxMap {
		clone[k] = v
	}
	evaluateDecision(params, clone, workpieceNo)
	result := make(map[string]any)
	for k, v := range clone {
		if _, ok := ctxMap[k]; !ok {
			result[k] = v
		}
	}
	return result
}
  • Step 3: Commit
rtk git add internal/eventloop/dbstate.go
rtk git commit -m "refactor: AdvanceStep uses stepId instead of stepIndex"

Task 2.4: Revise CompleteStep to use stepId

Files:

  • Modify: internal/eventloop/dbstate.go:34-55

  • Step 1: Change CompleteStep signature

// CompleteStep 完成当前步骤,推进到下一步 stepId
func (d *DBState) CompleteStep(ctx context.Context, jobID int, nextStepID string, contextPatch map[string]any) error {
	update := d.client.Job.UpdateOneID(jobID).
		SetCurrentStepId(nextStepID).
		AddVersion(1)
	if contextPatch != nil {
		jb, err := d.client.Job.Get(ctx, jobID)
		if err != nil {
			return fmt.Errorf("complete step: get job %d: %w", jobID, err)
		}
		ctxMap := jb.Context
		if ctxMap == nil {
			ctxMap = make(map[string]any)
		}
		for k, v := range contextPatch {
			ctxMap[k] = v
		}
		update = update.SetContext(ctxMap)
	}
	_, err := update.Save(ctx)
	return err
}
  • Step 2: Update callers in loop.go

At loop.go:409:

// Before:
l.db.CompleteStep(ctx, jobID, jb.CurrentStepIndex, map[string]any{"inspectionPass": true})
// After:
l.db.CompleteStep(ctx, jobID, jb.CurrentStepId, map[string]any{"inspectionPass": true})

At loop.go:432:

// Before:
l.db.CompleteStep(ctx, jobID, job.CurrentStepIndex, map[string]any{"inspectionPass": false})
// After:
l.db.CompleteStep(ctx, jobID, job.CurrentStepId, map[string]any{"inspectionPass": false})
  • Step 3: Commit
rtk git add internal/eventloop/dbstate.go internal/eventloop/loop.go
rtk git commit -m "refactor: CompleteStep uses stepId instead of stepIndex"

Chunk 3: Runtime Snapshot + Scheduler Bridge

Task 3.1: Add StepID to RuntimeSnapshot and scheduler types

Files:

  • Modify: internal/eventloop/loop.go:65-84

  • Modify: internal/scheduler/types.go:25-32,86-92

  • Step 1: Add StepID to RuntimeSnapshot

In internal/eventloop/loop.go, add after StepIndex:

type RuntimeSnapshot struct {
	// ... existing fields ...
	StepIndex     int
	StepID        string // 当前步骤业务IDOP10-OP120
	// ... rest ...
}
  • Step 2: Populate StepID in trySchedule

In scheduler_bridge.go:49-63, after loading recipe step info:

st, rt, tt, sn, sid, err := l.db.GetRecipeStep(ctx, j.RecipeId, j.CurrentStepIndex)
if err == nil {
	snap.StepType = st
	snap.ResourceType = rt
	snap.ToolType = tt
	snap.StepName = sn
	snap.StepID = sid
}

Also read from the job's own CurrentStepId field as backup:

snap.StepID = j.CurrentStepId

Note: GetRecipeStep already returns stepId as the 6th return value.

  • Step 3: Add StepID to scheduler StepView

In internal/scheduler/types.go:

type StepView struct {
	Index        int
	StepID       string // 业务步骤IDOP10-OP120
	Name         string
	Type         constants.StepType
	ResourceType string
	ToolType     string
	TargetID     int
}
  • Step 4: Add StepID to CandidateTask
type CandidateTask struct {
	JobID     int
	OrderID   int
	StepIndex int
	StepID    string // 业务步骤IDOP10-OP120
	Action    RobotAction
	TargetID  int
	Priority  Priority
	Meta      map[string]any
}
  • Step 5: Wire StepID in buildJobViews

In scheduler_bridge.go:220-227:

CurrentStep: scheduler.StepView{
	Index:        snap.StepIndex,
	StepID:       snap.StepID,
	// ... rest ...
},
  • Step 6: Wire StepID in generator.go

In internal/scheduler/generator.go, each CandidateTask creation adds StepID: step.StepID:

// Example: line 74
CandidateTask{
	JobID:     j.JobID,
	OrderID:   j.OrderID,
	StepIndex: step.Index,
	StepID:    step.StepID,
	Action:    ...,
	// ...
}
  • Step 7: Commit
rtk git add internal/eventloop/loop.go internal/eventloop/scheduler_bridge.go internal/scheduler/types.go internal/scheduler/generator.go
rtk git commit -m "feat: add StepID to RuntimeSnapshot, StepView, CandidateTask"

Task 3.2: Update candidate_handlers to use stepId

Files:

  • Modify: internal/eventloop/candidate_handlers.go

  • Step 1: Use GetRecipeStepByID

At line 64:

// Before:
params, err := l.db.GetStepProcessingParams(ctx, jb.RecipeId, jb.CurrentStepIndex)
// After:
params, err := l.db.GetStepProcessingParams(ctx, jb.RecipeId, jb.CurrentStepId)

But GetStepProcessingParams takes stepIndex. Change to look up stepIndex from stepId first, or add a stepId version. Since this is the only caller outside AdvanceStep, keep it simple and add a stepId-based version:

// In candidate_handlers.go:
// Get the step index from the recipe step for processing params lookup
_, _, _, _, _, err := l.db.GetRecipeStepByID(ctx, jb.RecipeId, jb.CurrentStepId)
// Actually just pass stepId and add stepId support to GetStepProcessingParams

Simpler: add GetStepProcessingParamsByID to dbstate.go:

func (d *DBState) GetStepProcessingParamsByID(ctx context.Context, recipeID int, stepID string) (map[string]any, error) {
	step, err := d.client.RecipeStep.Query().
		Where(recipestep.RecipeIdEQ(recipeID), recipestep.StepIdEQ(stepID)).
		First(ctx)
	if err != nil {
		return nil, fmt.Errorf("get recipe %d step %s processing params: %w", recipeID, stepID, err)
	}
	if step.ProcessingParams == nil {
		return nil, nil
	}
	return step.ProcessingParams, nil
}
  • Step 2: Commit
rtk git add internal/eventloop/candidate_handlers.go internal/eventloop/dbstate.go
rtk git commit -m "refactor: candidate_handlers uses GetStepProcessingParamsByID"

Chunk 4: Processor + Handler + Types + Mockrun

Task 4.1: Update API types with CurrentStepId

Files:

  • Modify: internal/types/types.go:352,519,693

  • Step 1: Add CurrentStepId to all structs

Add CurrentStepId string \json:"currentStepId"`to each struct that hasCurrentStepIndex`:

// Line ~352 (TempStationJobBrief or similar):
CurrentStepIndex int    `json:"currentStepIndex"` // deprecated
CurrentStepId    string `json:"currentStepId"`

// Line ~519:
CurrentStepIndex int    `json:"currentStepIndex"` // deprecated
CurrentStepId    string `json:"currentStepId"`

// Line ~693:
CurrentStepIndex int    `json:"currentStepIndex"` // deprecated
CurrentStepId    string `json:"currentStepId"`
  • Step 2: Regenerate types

Run: cd apis && go generate

  • Step 3: Update handler/logic to populate CurrentStepId

In response builder logic, set CurrentStepId from job.CurrentStepId.

  • Step 4: Commit
rtk git add internal/types/types.go apis/ internal/handler/ internal/logic/
rtk git commit -m "feat: add currentStepId to API response types"

Task 4.2: Update ReworkJob to use stepId

Files:

  • Modify: internal/processor/interface.go:31

  • Modify: internal/processor/job_processor.go:251-260

  • Modify: internal/logic/job/rework_job_logic.go:26

  • Step 1: Change ReworkJob signature

In interface.go:

// Before:
ReworkJob(ctx context.Context, jobID int, targetStepIndex int) error
// After:
ReworkJob(ctx context.Context, jobID int, targetStepID string) error
  • Step 2: Update job_processor.go implementation
func (jp *JobProcessor) ReworkJob(ctx context.Context, jobID int, targetStepID string) error {
	_, err := jp.entClient.Job.UpdateOneID(jobID).
		SetStatus(constants.JobStatus_Processing).
		SetCurrentStepId(targetStepID).
		SetSuspendedReason("").
		Save(ctx)
	if err != nil {
		return fmt.Errorf("rework job %d to step %s: %w", jobID, targetStepID, err)
	}
	slog.Info("job processor: job reworked", "jobId", jobID, "targetStepId", targetStepID)
	return nil
}
  • Step 3: Update stub

In interface.go:

func (s *StubProcessor) ReworkJob(ctx context.Context, jobID int, targetStepID string) error {
	return nil
}
  • Step 4: Update rework logic/handler

The ReworkJobReq already has TargetStepId string. Update the logic to pass it directly:

func (l *ReworkJobLogic) ReworkJob(req *types.ReworkJobReq) error {
	return l.svcCtx.JobProcessor.ReworkJob(l.ctx, req.ID, req.TargetStepId)
}
  • Step 5: Commit
rtk git add internal/processor/interface.go internal/processor/job_processor.go internal/logic/job/rework_job_logic.go
rtk git commit -m "refactor: ReworkJob uses stepId instead of stepIndex"

Task 4.3: Update mockrun

Files:

  • Modify: cmd/mockrun/main.go:110

  • Step 1: Use SetCurrentStepId

// Before:
SetCurrentStepIndex(initialStepIdx)
// After:
SetCurrentStepId(initialStepID) // lookup from recipe by stepIndex

Need to query recipe_step to get the stepId for initialStepIdx. Add a helper:

stepID := lookupStepID(ctx, entClient, recipe.RecipeID, initialStepIdx)
// ...
SetCurrentStepId(stepID)
  • Step 2: Commit
rtk git add cmd/mockrun/main.go
rtk git commit -m "refactor: mockrun uses SetCurrentStepId"

Chunk 5: Frontend

Task 5.1: Add currentStepId to frontend API types

Files:

  • Modify: frontend/src/api/temp-station/index.ts:9

  • Modify: frontend/src/api/dock/index.ts:9

  • Step 1: Add currentStepId to JobBrief

export interface JobBrief {
  id: number
  workOrderId: number
  workpieceNo: string
  productTypeId: number
  status: string
  currentStepIndex: number   // deprecated
  currentStepId: string
  currentStepName: string
}

Same change in both files.

  • Step 2: Commit
rtk git add frontend/src/api/temp-station/index.ts frontend/src/api/dock/index.ts
rtk git commit -m "feat: add currentStepId to frontend API types"

Chunk 6: Deprecation Cleanup

Task 6.1: Mark deprecated methods

Files:

  • Modify: internal/eventloop/dbstate.go

  • Modify: internal/processor/recipe_runtime.go

  • Step 1: Mark GetNextStepIndex deprecated

Add comment:

// Deprecated: use AdvanceStep with stepId instead. nextStepDefault is already a stepId.
func (d *DBState) GetNextStepIndex(...) {...}
  • Step 2: Verify no remaining callers of SetCurrentStepIndex

Run: rtk grep "SetCurrentStepIndex" --files-with-matches

  • Step 3: Commit
rtk git add internal/eventloop/dbstate.go
rtk git commit -m "chore: mark GetNextStepIndex deprecated"

Verification Checklist

  • go build ./... passes
  • go vet ./... passes
  • go test ./internal/eventloop/... passes
  • go test ./internal/scheduler/... passes
  • go test ./internal/processor/... passes
  • cd frontend && pnpm build passes
  • Migration runs successfully on existing database
  • No remaining SetCurrentStepIndex callers (except in generated ent code)