planstore

package
v0.29.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: Apache-2.0 Imports: 8 Imported by: 0

Documentation

Overview

Package planstore is the file-system-facing half of the structured plan: it reads and writes the machine-readable plan document stored at .sprout/plan.json and the rendered .sprout/plan.md view.

It builds on pkg/plancontract, which owns the plan schema and its validator (the "pure data + validation contract", with no I/O of its own). This package owns the I/O:

  • Load reads and validates .sprout/plan.json; a missing file yields the distinguishable ErrNoPlan sentinel, not a failure.
  • Save validates the plan before touching disk, so an invalid plan is never written. Every save bumps the plan's revision, stamps `updated`, and regenerates the .sprout/plan.md view from the JSON — the markdown is a derived artifact, regenerated on every write, never the source of truth.

The on-disk location is project-relative (.sprout/ under the project root), so a plan travels with the code and lands in git history.

Index

Constants

View Source
const (
	// PlanJSONName is the authoritative machine-readable plan document.
	PlanJSONName = "plan.json"
	// PlanMarkdownName is the human-readable view rendered from the JSON on
	// every write. It is derived, never hand-edited: a hand-edit is drift and
	// the next Save overwrites it.
	PlanMarkdownName = "plan.md"
)

File names, under the project's .sprout/ directory.

Variables

View Source
var ErrNoPlan = errors.New("no structured plan found")

ErrNoPlan is returned by Load when the project has no .sprout/plan.json yet. It is an expected, distinguishable not-found (not a failure): callers detect it with errors.Is(err, ErrNoPlan) and treat it as "no plan", while any other error from Load means the file exists but is unreadable or invalid.

Functions

func PlanJSONPath

func PlanJSONPath(root string) string

PlanJSONPath returns the path to the project's .sprout/plan.json for the given project root.

func PlanMarkdownPath

func PlanMarkdownPath(root string) string

PlanMarkdownPath returns the path to the project's .sprout/plan.md for the given project root.

func RenderMarkdown

func RenderMarkdown(plan *plancontract.Plan) string

RenderMarkdown renders plan as a deterministic, human-readable markdown view: the goal as the title, a small metadata block (schema version, revision, timestamps, and the design/starter references when set), then the scope, steps, acceptance and out-of-scope sections. Sections with no entries are omitted, so a minimal plan renders minimally.

The rendering is pure (no I/O) and stable: it walks the plan's slices in document order and emits no nondeterministic values, so a given plan renders to identical markdown on every run and platform. It is the function Save uses to regenerate .sprout/plan.md from the JSON, and it is exported so other tooling (e.g. `sprout plan --structured`) can render the same view.

Types

type Store

type Store struct {
	// Clock supplies the current time used to stamp `updated` on every Save.
	// When nil, time.Now is used. Tests pin it to a fixed instant for
	// deterministic `updated` values.
	Clock func() time.Time
}

Store reads and writes the structured plan for a project. It is safe for concurrent use by a single writer; it holds no mutable state beyond the (optional) clock. With multiple concurrent writers the revision floor is read before the write rather than atomically with it, so monotonicity is guaranteed for a single writer only.

func New

func New() *Store

New returns a Store that stamps `updated` with the wall clock.

func (*Store) Load

func (s *Store) Load(root string) (*plancontract.Plan, error)

Load reads and validates the structured plan from the .sprout/plan.json under root.

  • When the file does not exist, Load returns (nil, ErrNoPlan).
  • When the file is corrupt JSON, Load returns a wrapped decode error.
  • When the file is valid JSON but fails plancontract.Validate, Load returns the *plancontract.ValidationError (recovered with errors.As). In either of those cases the returned plan is nil, so a caller can never observe an invalid plan through Load.

On success Load returns a freshly decoded, valid plan.

func (*Store) Save

func (s *Store) Save(root string, plan *plancontract.Plan) (*plancontract.Plan, error)

Save validates plan and writes it to .sprout/plan.json under root, then regenerates the .sprout/plan.md view from it. It returns the stored plan — a copy with the bumped revision and stamped `updated` — on success.

Every save bumps the plan's revision by one, from the higher of the revision the plan carries and the revision currently stored on disk, and stamps `updated` with s.now(), so each write is observable as a new revision and the revision never decreases. The floor matters because a caller can legitimately arrive with a stale plan — one loaded before another process wrote, or assembled by hand — and bumping only from the caller's value would move the revision backwards. A stored file that cannot be read or parsed contributes no floor: the save proceeds from the caller's revision alone. Overwriting a mangled stored file with a valid plan is the better recovery here — refusing to save would break the normal flow for an unrelated defect the write is about to repair anyway — and it means a failed floor read never masks the outcome of the write itself. `created` stays as set on the caller's plan.

The caller's plan is left unmodified: Save works on a copy.

An invalid plan is rejected before any file is touched: neither plan.json nor plan.md is written and the error wraps the *plancontract.ValidationError naming every problem.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL