Problem. The walk that turns a render.RenderPlan + state + on-disk destinations into classified items exists four times, hand-maintained:
buildStatusModel (internal/cli/status.go:283-348)
diff's inline RunE loop (internal/cli/diff.go:141-198)
collectItems (internal/cli/reconcile.go:610-677)
- a variant in
internal/cli/explain_model.go (buildExplainModel, key-merge branch)
All four do: plan.PerAgent → key-merge vs whole-file branch → json.Unmarshal(op.Content) / readDestFile → render.CollectPointers → hash triple → drift.Classify, plus per-path dedupe for whole-file ops and NO dedupe for key-merge ops. Each copy carries a comment warning it must match the others ("matching status's key loop and the apply pipeline", "Deduping by path here dropped the second section's drift…") — synchronization by comment. Orphan enumeration (render.OrphanFiles) is similarly repeated in status + reconcile.
Fix. One iterator in internal/render (or internal/drift) — e.g. WalkPlanItems(plan, reg, state, sc, projectRoot, userHome) yielding {agent, op, ptr, class, srcHash/destHash, srcText/dstText} — with orphan items included behind a flag. Every export it needs (IsKeyMerge, CollectPointers, OrphanFiles, the hash helpers) already exists; only the composition is copy-pasted. status/diff/reconcile/explain then only render.
Payoff. The next change to classification semantics (e.g. mode drift, which today only status layers on at status.go:312) lands in one place instead of one-of-four.
Problem. The walk that turns a
render.RenderPlan+ state + on-disk destinations into classified items exists four times, hand-maintained:buildStatusModel(internal/cli/status.go:283-348)diff's inline RunE loop (internal/cli/diff.go:141-198)collectItems(internal/cli/reconcile.go:610-677)internal/cli/explain_model.go(buildExplainModel, key-merge branch)All four do:
plan.PerAgent→ key-merge vs whole-file branch →json.Unmarshal(op.Content)/readDestFile→render.CollectPointers→ hash triple →drift.Classify, plus per-path dedupe for whole-file ops and NO dedupe for key-merge ops. Each copy carries a comment warning it must match the others ("matching status's key loop and the apply pipeline", "Deduping by path here dropped the second section's drift…") — synchronization by comment. Orphan enumeration (render.OrphanFiles) is similarly repeated in status + reconcile.Fix. One iterator in
internal/render(orinternal/drift) — e.g.WalkPlanItems(plan, reg, state, sc, projectRoot, userHome)yielding{agent, op, ptr, class, srcHash/destHash, srcText/dstText}— with orphan items included behind a flag. Every export it needs (IsKeyMerge,CollectPointers,OrphanFiles, the hash helpers) already exists; only the composition is copy-pasted. status/diff/reconcile/explain then only render.Payoff. The next change to classification semantics (e.g. mode drift, which today only
statuslayers on atstatus.go:312) lands in one place instead of one-of-four.