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 ¶
- Variables
- func FeatureSourceIdeas(specRoot string) (map[string][]string, error)
- func FindIdeaDirectories(specRoot string) ([]string, error)
- func IsLegalChangeStatusTarget(s lifecycle.Status) bool
- func LegalChangeStatusTargetNames() []string
- func LegalTransitionMatrix() string
- func Scaffold(opts ScaffoldOptions) ([]byte, error)
- func SortedStatuses() []string
- func ValidateSlug(slug string) error
- type ChangeStatusOptions
- type ChangeStatusResult
- type Discovered
- type HeaderField
- type Idea
- func (i *Idea) ArchiveReason() string
- func (i *Idea) EffectiveType() string
- func (i *Idea) IdeaType() string
- func (i *Idea) Phase() string
- func (i *Idea) PromotesTo() []string
- func (i *Idea) RelatedIdeas() []string
- func (i *Idea) Status() string
- func (i *Idea) Supersedes() []string
- func (i *Idea) Targets() string
- type PostMutationHook
- type ScaffoldOptions
- type Section
- type Table
Constants ¶
This section is empty.
Variables ¶
var RequiredHeaderFields = []string{
"Status",
"Date",
"Owner",
"Promotes To",
"Supersedes",
"Related Ideas",
}
RequiredHeaderFields lists required **X:** fields in order.
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.
var ValidIdeaTypes = map[string]bool{ "feature-request": true, "change-request": true, }
ValidIdeaTypes enumerates the allowed values for the **Type:** field.
var ValidRelationships = map[string]bool{ "depends_on": true, "alternative_to": true, "extends": true, "conflicts_with": true, }
Valid relationship types for Related Ideas entries.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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):
- Resolve <slug> to an active file at spec/ideas/<slug>.md. A missing active file (even if an archived copy exists) returns exit 3.
- lifecycle.Validate against the Idea matrix. Illegal transitions return exit 4.
- lifecycle.Rewrite the **Status:** line; capture original for rollback.
- If To == Archived: check collision, mkdir-p + os.Rename. Collision returns exit 1; mkdir/rename failure rolls back and returns exit 10.
- 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 ¶
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 ¶
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 ¶
ArchiveReason returns the Archive Reason value or "".
func (*Idea) EffectiveType ¶ added in v0.5.1
EffectiveType returns the Type field value, defaulting to "feature-request" when the field is absent.
func (*Idea) IdeaType ¶ added in v0.5.1
IdeaType returns the Type field value or "" if absent. An absent Type is treated as "feature-request" by convention.
func (*Idea) PromotesTo ¶
PromotesTo returns the parsed Promotes To slugs (comma-separated list). A value of "—" or "-" means empty.
func (*Idea) RelatedIdeas ¶
RelatedIdeas returns the raw entries (un-split) from the Related Ideas field.
func (*Idea) Supersedes ¶
Supersedes returns the parsed Supersedes slugs.
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 ¶
Table captures a markdown pipe-table within a section.
func ParseTable ¶
ParseTable extracts a markdown pipe-table from the given section body. Returns nil if no table is present. Only the first table is returned.