[object Object]

← back to Cli Printing Press

refactor(pipeline): use stable-numbered PlanPath instead of name-only

630cd0b5e13f9795bc4497d739047a68e59226fc · 2026-03-27 20:02:21 -0700 · Trevin Chow

Switch from name-only PlanPath (preflight-plan.md) to stable-numbered
format with gaps (00-preflight-plan.md, 55-agent-readiness-plan.md).
Preserves visual scan order in directory listings while allowing future
phase insertions without renaming existing files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 630cd0b5e13f9795bc4497d739047a68e59226fc
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri Mar 27 20:02:21 2026 -0700

    refactor(pipeline): use stable-numbered PlanPath instead of name-only
    
    Switch from name-only PlanPath (preflight-plan.md) to stable-numbered
    format with gaps (00-preflight-plan.md, 55-agent-readiness-plan.md).
    Preserves visual scan order in directory listings while allowing future
    phase insertions without renaming existing files.
    
    Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
 internal/pipeline/state.go      | 40 ++++++++++++++++++++++++++++++----------
 internal/pipeline/state_test.go | 12 ++++++++----
 2 files changed, 38 insertions(+), 14 deletions(-)

diff --git a/internal/pipeline/state.go b/internal/pipeline/state.go
index 07904feb..97212be1 100644
--- a/internal/pipeline/state.go
+++ b/internal/pipeline/state.go
@@ -34,6 +34,26 @@ var PhaseOrder = []string{
 	PhaseShip,
 }
 
+// phaseNumber assigns a stable prefix for plan filenames. Numbers use
+// gaps (0, 10, 20 …) so future phases can be inserted without renaming
+// existing files.
+var phaseNumber = map[string]int{
+	PhasePreflight:      0,
+	PhaseResearch:       10,
+	PhaseScaffold:       20,
+	PhaseEnrich:         30,
+	PhaseRegenerate:     40,
+	PhaseReview:         50,
+	PhaseAgentReadiness: 55,
+	PhaseComparative:    60,
+	PhaseShip:           70,
+}
+
+// PlanFilename returns the stable plan filename for a phase.
+func PlanFilename(phase string) string {
+	return fmt.Sprintf("%02d-%s-plan.md", phaseNumber[phase], phase)
+}
+
 const (
 	StatusPending   = "pending"
 	StatusPlanned   = "planned"   // plan.md exists but not yet executed
@@ -86,7 +106,7 @@ func NewState(apiName, outputDir string) *PipelineState {
 	for _, name := range PhaseOrder {
 		phases[name] = PhaseState{
 			Status:   StatusPending,
-			PlanPath: filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name)),
+			PlanPath: filepath.Join(PipelineDir(apiName), PlanFilename(name)),
 		}
 	}
 	state := &PipelineState{
@@ -124,7 +144,8 @@ func LoadState(apiName string) (*PipelineState, error) {
 	if err := json.Unmarshal(data, &s); err != nil {
 		return nil, fmt.Errorf("parsing state: %w", err)
 	}
-	// Migrate: add missing phases and update PlanPath from index-based to name-based format.
+	// Migrate: add missing phases, update PlanPath to stable-numbered format,
+	// and backfill PlanStatus for completed phases that predate the field.
 	if s.Version < currentStateVersion {
 		for _, name := range PhaseOrder {
 			if _, ok := s.Phases[name]; !ok {
@@ -133,14 +154,13 @@ func LoadState(apiName string) (*PipelineState, error) {
 				s.Phases[name] = PhaseState{
 					Status:     StatusCompleted,
 					PlanStatus: PlanStatusCompleted,
-					PlanPath:   filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name)),
+					PlanPath:   filepath.Join(PipelineDir(apiName), PlanFilename(name)),
 				}
 			} else {
-				// Migrate existing phases: update PlanPath from index-based to
-				// name-based format, and backfill PlanStatus for completed phases
-				// that predate the PlanStatus field.
+				// Migrate existing phases to stable-numbered PlanPath and
+				// backfill PlanStatus for completed phases that predate the field.
 				p := s.Phases[name]
-				p.PlanPath = filepath.Join(PipelineDir(apiName), fmt.Sprintf("%s-plan.md", name))
+				p.PlanPath = filepath.Join(PipelineDir(apiName), PlanFilename(name))
 				if p.Status == StatusCompleted && p.PlanStatus == "" {
 					p.PlanStatus = PlanStatusCompleted
 				}
@@ -149,9 +169,9 @@ func LoadState(apiName string) (*PipelineState, error) {
 		}
 		s.Version = currentStateVersion
 		// Persist the migration so it doesn't re-run on every load.
-		// Note: plan files on disk keep their old index-based names.
-		// Completed phases' plans are not re-read; pending phases get
-		// new seeds written at the new paths by Init.
+		// Note: plan files on disk keep their old sequential-index names
+		// (00-, 01-, …). Completed phases' plans are not re-read; pending
+		// phases get new seeds written at the stable-numbered paths by Init.
 		if err := s.Save(); err != nil {
 			return nil, fmt.Errorf("saving migrated state: %w", err)
 		}
diff --git a/internal/pipeline/state_test.go b/internal/pipeline/state_test.go
index 31651e75..3d7709c3 100644
--- a/internal/pipeline/state_test.go
+++ b/internal/pipeline/state_test.go
@@ -147,12 +147,16 @@ func TestNextPhaseReturnsAgentReadiness(t *testing.T) {
 	assert.Equal(t, PhaseAgentReadiness, s.NextPhase())
 }
 
-func TestNewStatePlanPathNameBased(t *testing.T) {
+func TestNewStatePlanPathStableNumbered(t *testing.T) {
 	s := NewState("path-test", "/tmp/test")
 	for _, name := range PhaseOrder {
-		expected := "docs/plans/path-test-pipeline/" + name + "-plan.md"
+		expected := "docs/plans/path-test-pipeline/" + PlanFilename(name)
 		assert.Equal(t, expected, s.Phases[name].PlanPath, "PlanPath for %s", name)
 	}
+	// Spot-check a few to verify the numbering scheme.
+	assert.Contains(t, s.Phases[PhasePreflight].PlanPath, "00-preflight-plan.md")
+	assert.Contains(t, s.Phases[PhaseAgentReadiness].PlanPath, "55-agent-readiness-plan.md")
+	assert.Contains(t, s.Phases[PhaseShip].PlanPath, "70-ship-plan.md")
 }
 
 func TestLoadStateMigratesV1ToV2(t *testing.T) {
@@ -193,9 +197,9 @@ func TestLoadStateMigratesV1ToV2(t *testing.T) {
 	assert.Equal(t, StatusCompleted, ar.Status)
 	assert.Equal(t, PlanStatusCompleted, ar.PlanStatus)
 
-	// All phases have name-based PlanPaths.
+	// All phases have stable-numbered PlanPaths.
 	for _, name := range PhaseOrder {
-		expected := dir + "/" + name + "-plan.md"
+		expected := dir + "/" + PlanFilename(name)
 		assert.Equal(t, expected, loaded.Phases[name].PlanPath, "migrated PlanPath for %s", name)
 	}
 

← 0d4e711c fix(cli): propagate ReadDir error in explicitOutput collisio  ·  back to Cli Printing Press  ·  docs: update specs to reflect stable-numbered PlanPath appro fe6a3634 →