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 ¶
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 ¶
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 ¶
PlanJSONPath returns the path to the project's .sprout/plan.json for the given project root.
func PlanMarkdownPath ¶
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 (*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.