idea

package
v0.5.1 Latest Latest
Warning

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

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

Documentation

Overview

Package idea parses and represents SpecScore Idea artifacts.

An Idea is a single markdown file under `spec/ideas/` (or `spec/ideas/archived/`) that captures a pre-spec, lintable one-pager: problem framing, recommended direction, MVP scope, exclusions, and dealbreaker assumptions. See `spec/features/idea/README.md` for the full artifact schema.

This package provides parsing utilities — the lint rules that enforce the schema live in `pkg/lint`.

Package idea — lifecycle transition orchestration for the Idea kind.

This file hosts ChangeStatus, the kind-specific orchestrator invoked by `specscore idea change-status`. It composes pkg/lifecycle/ primitives (state-machine validation, status-line rewrite, rollback) with the Idea-specific archive file-relocation side effect.

LINT INVOCATION lives in the cobra adapter (internal/cli/idea.go), NOT here, to avoid an import cycle: pkg/lint already imports pkg/idea for the idea-* lint rules, so pkg/idea cannot depend back on pkg/lint. The adapter passes a Linter callback into ChangeStatus; this package only knows "run the post-mutation hook, and roll back if it fails".

Cross-references:

  • Verb spec: spec/features/cli/idea/change-status/README.md
  • Meta contract: spec/features/cli/lifecycle-transitions/README.md

All behavioral REQs cited in those documents are enforced here; the cobra adapter is a thin shim over this function.

Index

Constants

This section is empty.

Variables

View Source
var RequiredHeaderFields = []string{
	"Status",
	"Date",
	"Owner",
	"Promotes To",
	"Supersedes",
	"Related Ideas",
}

RequiredHeaderFields lists required **X:** fields in order.

View Source
var RequiredSections = []string{
	"Problem Statement",
	"Context",
	"Recommended Direction",
	"Alternatives Considered",
	"MVP Scope",
	"Not Doing (and Why)",
	"Key Assumptions to Validate",
	"SpecScore Integration",
	"Open Questions",
}

RequiredSections names the sections that every Idea MUST have, in order.

View Source
var ValidIdeaTypes = map[string]bool{
	"feature-request": true,
	"change-request":  true,
}

ValidIdeaTypes enumerates the allowed values for the **Type:** field.

View Source
var ValidRelationships = map[string]bool{
	"depends_on":     true,
	"alternative_to": true,
	"extends":        true,
	"conflicts_with": true,
}

Valid relationship types for Related Ideas entries.

View Source
var ValidStatuses = map[string]bool{
	"Draft":        true,
	"Under Review": true,
	"Approved":     true,
	"Specifying":   true,
	"Specified":    true,
	"Implementing": true,
	"Implemented":  true,
	"Archived":     true,
}

Valid statuses for an Idea.

Functions

func FeatureSourceIdeas

func FeatureSourceIdeas(specRoot string) (map[string][]string, error)

FeatureSourceIdeas scans every `spec/features/**/README.md` and returns a map of feature slug -> []idea-slug based on the **Source Ideas:** header. Features without the field are omitted. The slug is the feature's path relative to `spec/features/` (e.g. `cli`, `cli/spec/lint`, `cli/lifecycle-transitions`), so nested sub-features are first-class.

func FindIdeaDirectories

func FindIdeaDirectories(specRoot string) ([]string, error)

FindIdeaDirectories returns directories that exist at `spec/ideas/<slug>/` (violation per REQ: single-file). Ignores the reserved `archived/` and `seeds/` directories — `seeds/` holds sidekick-seed documents, which are a separate document type, not malformed Ideas.

func IsLegalChangeStatusTarget

func IsLegalChangeStatusTarget(s lifecycle.Status) bool

IsLegalChangeStatusTarget reports whether status is one of the user-facing --to values for `specscore idea change-status`. The CLI uses this for early flag validation (exit 2 InvalidArgs) BEFORE the state-machine check, so unrecognized values like --to=draft fail fast with a clear message rather than slipping through to a state-machine rejection at exit 4. The legal --to set is the union of every To column in the Idea matrix.

func LegalChangeStatusTargetNames

func LegalChangeStatusTargetNames() []string

LegalChangeStatusTargetNames returns the canonical-titled names of the legal --to values, for stderr rendering when a user supplies an unrecognized value.

func LegalTransitionMatrix

func LegalTransitionMatrix() string

LegalTransitionMatrix returns a human-readable, ANSI-free rendering of the Idea legal-transition matrix, suitable for inclusion in cobra `Long` help text. Built from lifecycle.LegalTargets so the help stays current as the matrix grows.

Rows are emitted in the order returned by lifecycle.LegalStatuses (alphabetical) but only for statuses that have ≥1 outgoing legal target. The output is intentionally simple two-column ASCII — no box characters or color codes.

func Scaffold

func Scaffold(opts ScaffoldOptions) ([]byte, error)

Scaffold returns a lint-clean Idea file body for the given options. Every required section is emitted either with an HTML-comment prompt (the default) or with the supplied content.

func SortedStatuses

func SortedStatuses() []string

Sort returns a slice of statuses sorted alphabetically (helper for tests).

func ValidateSlug

func ValidateSlug(slug string) error

ValidateSlug returns nil if slug matches `[a-z0-9]+(-[a-z0-9]+)*`.

Types

type ChangeStatusOptions

type ChangeStatusOptions struct {
	// SpecRoot is the project root that contains the `spec/` subtree
	// (NOT the `spec/` directory itself). The Idea file is resolved at
	// SpecRoot/spec/ideas/<Slug>.md.
	SpecRoot string

	// Slug is the Idea slug, e.g. "payment-fraud". Caller is expected
	// to have validated it via idea.ValidateSlug.
	Slug string

	// To is the canonical (title-case) target status, e.g. "Approved"
	// or "Archived". The cobra adapter parses the raw --to value via
	// lifecycle.ParseStatus before reaching this function.
	To lifecycle.Status

	// PostMutation is the post-rewrite hook (typically a spec-lint
	// pass). Required; ChangeStatus returns exit 10 if nil.
	PostMutation PostMutationHook
}

ChangeStatusOptions packages the inputs to ChangeStatus.

type ChangeStatusResult

type ChangeStatusResult struct {
	Slug string
	From lifecycle.Status
	To   lifecycle.Status
}

ChangeStatusResult is the success payload returned by ChangeStatus on exit 0. The cobra adapter formats it as the success-output line.

func ChangeStatus

func ChangeStatus(opts ChangeStatusOptions) (ChangeStatusResult, error)

ChangeStatus performs an Idea-kind lifecycle transition end-to-end.

Flow (matches the verb spec step-list):

  1. Resolve <slug> to an active file at spec/ideas/<slug>.md. A missing active file (even if an archived copy exists) returns exit 3.
  2. lifecycle.Validate against the Idea matrix. Illegal transitions return exit 4.
  3. lifecycle.Rewrite the **Status:** line; capture original for rollback.
  4. If To == Archived: check collision, mkdir-p + os.Rename. Collision returns exit 1; mkdir/rename failure rolls back and returns exit 10.
  5. Invoke the PostMutation hook. Failure → full rollback + exit 10.

ChangeStatus performs all rollback internally — by the time it returns an error, the on-disk state is byte-identical to its pre-invocation shape per lifecycle-transitions#REQ:rollback-on-lint-failure and cli/idea/change-status#REQ:rollback-includes-relocation.

type Discovered

type Discovered struct {
	Slug       string
	Path       string // absolute or relative path to the .md file
	Archived   bool   // true if located under archived/
	IsProposal bool   // true if located under spec/features/*/proposals/
	FeatureDir string // non-empty for proposals: the feature directory slug
}

Discovered is a summary of an Idea file found during discovery.

func Discover

func Discover(specRoot string) ([]Discovered, error)

Discover walks `<specRoot>/ideas` and returns every idea file found. Returns ([], nil) if the directory does not exist.

type HeaderField

type HeaderField struct {
	Name  string
	Value string
	Line  int
}

HeaderField captures a single **Name:** value line.

type Idea

type Idea struct {
	Path           string
	Slug           string
	Title          string // full title line (without leading "# ")
	TitleName      string // name portion after "Idea: " or "Proposal: "
	TitleOK        bool   // true if title matches "# Idea: <Name>" or "# Proposal: <Name>"
	TitlePrefix    string // "Idea" or "Proposal" (set when TitleOK is true)
	TitleLine      int
	HasTitle       bool
	Fields         []HeaderField // in-order header fields encountered
	FieldByName    map[string]HeaderField
	Sections       []Section
	SectionByTitle map[string]*Section
	RawLines       []string
}

Idea is a parsed Idea artifact.

func Parse

func Parse(path string) (*Idea, error)

Parse reads an Idea file and returns its parsed representation. Parse is resilient — it returns a partial Idea even if the file is malformed (missing title, missing sections). Callers (lint rules) decide what is an error.

func (*Idea) ArchiveReason

func (i *Idea) ArchiveReason() string

ArchiveReason returns the Archive Reason value or "".

func (*Idea) EffectiveType added in v0.5.1

func (i *Idea) EffectiveType() string

EffectiveType returns the Type field value, defaulting to "feature-request" when the field is absent.

func (*Idea) IdeaType added in v0.5.1

func (i *Idea) IdeaType() string

IdeaType returns the Type field value or "" if absent. An absent Type is treated as "feature-request" by convention.

func (*Idea) Phase added in v0.5.1

func (i *Idea) Phase() string

Phase returns the Phase field value or "" if absent.

func (*Idea) PromotesTo

func (i *Idea) PromotesTo() []string

PromotesTo returns the parsed Promotes To slugs (comma-separated list). A value of "—" or "-" means empty.

func (*Idea) RelatedIdeas

func (i *Idea) RelatedIdeas() []string

RelatedIdeas returns the raw entries (un-split) from the Related Ideas field.

func (*Idea) Status

func (i *Idea) Status() string

Status returns the Status field value or "" if missing.

func (*Idea) Supersedes

func (i *Idea) Supersedes() []string

Supersedes returns the parsed Supersedes slugs.

func (*Idea) Targets added in v0.5.1

func (i *Idea) Targets() string

Targets returns the Targets field value or "" if absent.

type PostMutationHook

type PostMutationHook func() error

PostMutationHook is the callback ChangeStatus invokes after a successful status rewrite (and, for archive transitions, file move). It is the integration point for `specscore spec lint --fix` plus the verify pass.

The hook MUST return nil on success. A non-nil return triggers full rollback (status line restored AND, for archive transitions, file moved back) and the error is wrapped and returned by ChangeStatus.

The cobra adapter is responsible for wiring this to pkg/lint; tests can supply a fake to exercise the rollback paths without invoking lint.

type ScaffoldOptions

type ScaffoldOptions struct {
	Slug string
	// Title is the human-readable name after `# Idea: ` or `# Proposal: `.
	// Defaults to a title-cased version of the slug.
	Title string
	Owner string
	// Date in ISO-8601 (YYYY-MM-DD). Defaults to today's UTC date.
	Date string
	// Status defaults to "Draft".
	Status string

	// Type is the idea type: "feature-request" (default) or "change-request".
	// When "change-request", the title prefix becomes "# Proposal: " and the
	// Type/Targets fields are emitted in the header.
	Type string
	// Targets is the feature slug this change-request targets. Only meaningful
	// when Type is "change-request".
	Targets string
	// Phase is an optional lifecycle phase value pre-populated in the header.
	Phase string

	// Section content. Empty strings leave the default prompt in place.
	HMW                  string // Problem Statement
	Context              string
	RecommendedDirection string
	Alternatives         []string // Each element is a bullet for Alternatives Considered.
	MVP                  string
	// NotDoing is a list of exclusions. When empty, a lint-clean default
	// list is emitted so the Not Doing section passes idea-not-doing-non-empty.
	NotDoing []string
	// Assumptions is an optional list of assumption-table rows.
	// Each row is {Tier, Assumption, HowToValidate}. When empty, a
	// lint-clean default table with one Must-be-true row is emitted.
	Assumptions [][3]string
	// SpecScore Integration overrides.
	NewFeatures  string
	Existing     string
	Dependencies string
	// OpenQuestions bullets (optional).
	OpenQuestions []string
}

ScaffoldOptions controls the content emitted by Scaffold. Any field left empty keeps the section's default HTML-comment prompt.

type Section

type Section struct {
	Title     string
	StartLine int
	EndLine   int
	Body      string
	Items     []string // lines starting with "- "
}

Section captures a single ## heading and its body.

type Table

type Table struct {
	Headers []string
	Rows    [][]string
}

Table captures a markdown pipe-table within a section.

func ParseTable

func ParseTable(body string) *Table

ParseTable extracts a markdown pipe-table from the given section body. Returns nil if no table is present. Only the first table is returned.

Jump to

Keyboard shortcuts

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