Documentation
¶
Overview ¶
Package feature provides feature discovery, traversal, metadata, dependency resolution, and scaffolding. Pure library functions — no cobra, no fmt.Print, no os.Exit.
Package feature — lifecycle-transition primitives.
This file hosts the kind-specific glue between the generic pkg/lifecycle state machine and the Feature artifact layout. It keeps three responsibilities:
- Resolve a `<feature_id>` (possibly slash-bearing, e.g. "cli/idea/change-status") to its canonical README path.
- Validate the requested transition against the Feature kind's legal-transition matrix, falling through to an exit-2 error when the raw --to value isn't a recognized status name.
- Rewrite the artifact's **Status:** line in place and return a Restore closure the CLI layer can call to roll back on lint failure.
pkg/feature/transitions.go DELIBERATELY does NOT import pkg/lint — the lint dance (run --fix, inspect violations, roll back on error) belongs to the CLI handler in internal/cli/feature.go. Keeping the import direction strict (lint → feature, never the reverse) avoids the cycle that the parallel pkg/idea/transitions.go work has hit.
Index ¶
- Constants
- Variables
- func BoolPtr(b bool) *bool
- func CountOpenQuestions(readmePath string) (int, error)
- func DepsResolver(featuresDir, featureID string) ([]string, error)
- func EnrichTransitiveNodes(featuresDir string, nodes []*EnrichedFeature, fields []string)
- func Exists(featuresDir, featureID string) bool
- func ExtractFeatureID(item string) string
- func ExtractOpenQuestions(readmePath string) ([]string, error)
- func FeatureIDFromRelativePath(relPath string) string
- func FeatureIDs(features []Feature) []string
- func FilterFocusedFeatures(allFeatures []string, targetID, direction string) []string
- func FindFeatureRefs(featuresDir, featureID string) ([]string, error)
- func FindLinkedPlans(repoRoot, featureID string) ([]string, error)
- func FindSpecRepoRoot(startDir string) (string, error)
- func GenerateReadme(title, status, description string, deps []string) string
- func GenerateSlug(title string) string
- func IsValidStatus(status string) bool
- func MarkFocus(nodes []*FeatureNode, targetID string)
- func ParseContentsTable(readmePath string) (map[string]bool, error)
- func ParseDependencies(readmePath string) ([]string, error)
- func ParseFeatureStatus(readmePath string) (string, error)
- func ParseFeatureTitle(readmePath string) (string, error)
- func ParseFieldNames(fieldsStr string) ([]string, error)
- func PrintTransitiveText(sb *strings.Builder, nodes []*EnrichedFeature, depth int)
- func PrintTree(w *strings.Builder, nodes []*FeatureNode, depth int)
- func ReadmePath(featuresDir, featureID string) string
- func RefsResolver(featuresDir, featureID string) ([]string, error)
- func UpdateFeatureIndex(indexPath, featureID, status, description string) (bool, error)
- func UpdateParentContents(parentReadmePath, childSlug, description string) (bool, error)
- func ValidateFormat(format string) error
- func ValidateSlug(slug string) error
- type ChangeStatusResult
- type ChildInfo
- type EnrichedFeature
- func BuildEnrichedTree(featuresDir string, featureIDs []string, fields []string, focusID string) []*EnrichedFeature
- func ResolveFields(featuresDir, featureID string, fields []string) (*EnrichedFeature, error)
- func TransitiveDeps(featuresDir, startID string) []*EnrichedFeature
- func TransitiveRefs(featuresDir, startID string) []*EnrichedFeature
- type Feature
- type FeatureNode
- type Info
- type NewOptions
- type NewResult
- type RelationResolver
- type SectionInfo
Constants ¶
const ( DefaultSpecDir = "spec" FeaturesSubDir = "features" )
Default directory layout constants.
const FormatURL = "https://specscore.md/feature-specification"
FormatURL is the canonical spec URL for the Feature document type, emitted in both the frontmatter `format:` field (artifact-frontmatter-convention) and the adherence-footer line, which carry the same URL.
Variables ¶
var ValidFields = map[string]bool{ "status": true, "oq": true, "questions": true, "title": true, "deps": true, "refs": true, "children": true, "plans": true, "proposals": true, }
ValidFields lists all recognized metadata field names.
var ValidStatuses = []string{"Draft", "In Review", "Approved", "Implementing", "Stable", "Amending", "Rejected", "Deprecated"}
ValidStatuses lists the allowed feature lifecycle statuses.
Functions ¶
func CountOpenQuestions ¶
CountOpenQuestions counts list items in the ## Open Questions section.
func DepsResolver ¶
DepsResolver returns the direct dependencies for a feature.
func EnrichTransitiveNodes ¶
func EnrichTransitiveNodes(featuresDir string, nodes []*EnrichedFeature, fields []string)
EnrichTransitiveNodes adds field metadata to a transitive tree.
func Exists ¶
Exists checks if a feature ID corresponds to a valid feature directory with README.md.
func ExtractFeatureID ¶
ExtractFeatureID extracts a feature ID from a dependency list item.
func ExtractOpenQuestions ¶
ExtractOpenQuestions returns the text of each top-level list item under the ## Open Questions section, with the leading "- " stripped. Returns an empty slice when the section is absent or contains no items. One question per "- " line; multi-line items capture only the first line, matching CountOpenQuestions's notion of an item.
func FeatureIDFromRelativePath ¶
FeatureIDFromRelativePath converts a relative path like "../cli/README.md" to a feature ID like "cli".
func FeatureIDs ¶
FeatureIDs is a convenience helper that extracts just the ID strings from a slice of Feature values.
func FilterFocusedFeatures ¶
FilterFocusedFeatures returns features relevant to a focused tree view.
func FindFeatureRefs ¶
FindFeatureRefs finds all features that reference the given featureID as a dependency.
func FindLinkedPlans ¶
FindLinkedPlans scans spec/plans/*/README.md for plans that reference the given feature.
func FindSpecRepoRoot ¶
FindSpecRepoRoot walks up from startDir looking for specscore.yaml. As a fallback it checks for a spec/features/ directory. Returns the directory that serves as the spec repo root.
func GenerateReadme ¶
GenerateReadme produces the full README.md content for a new feature. If description is empty, a TODO placeholder is used. The Dependencies section is only included when deps is non-empty.
func GenerateSlug ¶
GenerateSlug converts a human-readable title to a URL-safe directory name.
It lowercases the title, replaces spaces and underscores with hyphens, removes non-alphanumeric/non-hyphen characters, collapses consecutive hyphens, and trims leading/trailing hyphens.
func IsValidStatus ¶
IsValidStatus reports whether status matches one of the ValidStatuses (case-insensitive).
func MarkFocus ¶
func MarkFocus(nodes []*FeatureNode, targetID string)
MarkFocus sets the focus flag on the target node in the tree.
func ParseContentsTable ¶
ParseContentsTable reads a README and extracts entries from the ## Contents section. Returns a map of directory names found in the table.
func ParseDependencies ¶
ParseDependencies reads a feature's README.md and extracts the ## Dependencies section. Returns a sorted list of feature IDs listed as bullet items.
Supports two formats:
- bare ID: "- claim-and-push"
- markdown link: "- [Name](../path/README.md) -- optional description"
func ParseFeatureStatus ¶
ParseFeatureStatus extracts the status from a feature README. Looks for patterns like "**Status:** Draft" or "Status: Stable".
func ParseFeatureTitle ¶
ParseFeatureTitle returns the first H1 from a feature README, stripping the conventional "Feature: " prefix produced by GenerateReadme. If no H1 is present, an empty string is returned with no error.
func ParseFieldNames ¶
ParseFieldNames validates and returns field names from a comma-separated string.
func PrintTransitiveText ¶
func PrintTransitiveText(sb *strings.Builder, nodes []*EnrichedFeature, depth int)
PrintTransitiveText writes transitive results as indented text.
func PrintTree ¶
func PrintTree(w *strings.Builder, nodes []*FeatureNode, depth int)
PrintTree writes the tree to a strings.Builder with tab indentation. Nodes with Focus set are prefixed with "* ".
func ReadmePath ¶
ReadmePath returns the absolute path to a feature's README.md.
func RefsResolver ¶
RefsResolver returns features that depend on the given feature.
func UpdateFeatureIndex ¶
UpdateFeatureIndex adds a new row to the feature index at spec/features/README.md. Returns true if the file was modified.
The emitted row's column count matches the existing table header so repos with different schemas (4-column `Feature|Status|Kind|Description`, 7-column `Feature|Status|Kind|URL|Consumer Path|Index|Description`, etc.) all get well-formed rows. Per-column values are inferred from known header names (see indexRowCellFor); unknown columns get a `—` placeholder for human curation. When no header can be parsed, the function falls back to a 4-cell `Feature | Status | Kind | Description` row — the historical default emitted by `specscore feature new`.
func UpdateParentContents ¶
UpdateParentContents adds or updates the ## Contents section in the parent's README. Returns true if the file was modified.
func ValidateFormat ¶
ValidateFormat checks the format flag value is valid.
func ValidateSlug ¶
ValidateSlug checks that slug is a valid feature slug.
A valid slug is non-empty, lowercase, contains only alphanumeric chars, hyphens, and forward slashes (for nested paths). Consecutive hyphens are not allowed. Each segment (split by "/") must not have leading or trailing hyphens.
Types ¶
type ChangeStatusResult ¶
type ChangeStatusResult struct {
FeatureID string
ReadmePath string
From lifecycle.Status
To lifecycle.Status
Restore func() error
}
ChangeStatusResult is the outcome of a successful Status rewrite. The Restore closure restores the artifact's original `**Status:**` line byte-for-byte (forwarding to lifecycle.Rollback under the hood). The CLI layer holds the closure across its post-rewrite `spec lint --fix` pass and invokes it iff a lint violation forces rollback (per the lifecycle-transitions Meta REQ: rollback-on-lint-failure).
func ChangeStatus ¶
func ChangeStatus(featuresDir, featureID, toRaw string) (*ChangeStatusResult, error)
ChangeStatus validates and applies a Feature Status transition.
Flow:
- ParseStatus(toRaw) — exit-2 if the raw flag value is not a recognized Feature status (covers `--to=banana`, `--to=draft` (no arc INTO Draft), `--to=archived` (Idea-only)).
- resolveFeatureID(featuresDir, featureID) — exit-3 if the `<feature_id>/README.md` path doesn't exist.
- lifecycle.Validate(...) — reads the artifact's current Status and exit-4s if (from, to) isn't in the Feature matrix.
- lifecycle.Rewrite(...) — line-targeted rewrite; returns the original Status line for rollback.
On any pre-rewrite failure, ChangeStatus returns an *exitcode.Error carrying the correct exit code and an empty result. On success it returns the result with a Restore closure the caller MUST invoke if post-rewrite work fails.
type ChildInfo ¶
type ChildInfo struct {
Path string `yaml:"path" json:"path"`
InReadme bool `yaml:"in_readme" json:"in_readme"`
}
ChildInfo represents a child sub-feature.
func DiscoverChildFeatures ¶
DiscoverChildFeatures finds immediate child sub-feature directories and checks whether each is listed in the parent's ## Contents table.
type EnrichedFeature ¶
type EnrichedFeature struct {
Path string `yaml:"path" json:"path"`
Focus *bool `yaml:"focus,omitempty" json:"focus,omitempty"`
Cycle *bool `yaml:"cycle,omitempty" json:"cycle,omitempty"`
Title string `yaml:"title,omitempty" json:"title,omitempty"`
Status string `yaml:"status,omitempty" json:"status,omitempty"`
OQ *int `yaml:"oq,omitempty" json:"oq,omitempty"`
Questions []string `yaml:"questions,omitempty" json:"questions,omitempty"`
Deps []string `yaml:"deps,omitempty" json:"deps,omitempty"`
Refs []string `yaml:"refs,omitempty" json:"refs,omitempty"`
Plans []string `yaml:"plans,omitempty" json:"plans,omitempty"`
Proposals []string `yaml:"proposals,omitempty" json:"proposals,omitempty"`
ChildPaths []string `yaml:"children,omitempty" json:"children,omitempty"`
ChildNodes []*EnrichedFeature `yaml:"child_nodes,omitempty" json:"child_nodes,omitempty"`
}
EnrichedFeature holds a feature ID with optional metadata fields. ChildPaths holds child IDs for flat output; ChildNodes holds enriched children for tree representations.
func BuildEnrichedTree ¶
func BuildEnrichedTree(featuresDir string, featureIDs []string, fields []string, focusID string) []*EnrichedFeature
BuildEnrichedTree builds a tree of EnrichedFeature nodes with resolved fields.
func ResolveFields ¶
func ResolveFields(featuresDir, featureID string, fields []string) (*EnrichedFeature, error)
ResolveFields computes the requested metadata fields for a feature. Returns partial results alongside any errors encountered.
func TransitiveDeps ¶
func TransitiveDeps(featuresDir, startID string) []*EnrichedFeature
TransitiveDeps follows dependency chains recursively with cycle detection.
func TransitiveRefs ¶
func TransitiveRefs(featuresDir, startID string) []*EnrichedFeature
TransitiveRefs follows reference chains recursively with cycle detection.
type Feature ¶
type Feature struct {
// ID is the slash-separated path relative to the features directory
// (e.g. "cli/task/claim").
ID string
}
Feature holds a discovered feature's identity.
type FeatureNode ¶
type FeatureNode struct {
Name string
ID string // full feature ID
Focus bool // marked as target in focused tree
Children []*FeatureNode
}
FeatureNode represents a feature in a tree structure.
func BuildTree ¶
func BuildTree(featureIDs []string) []*FeatureNode
BuildTree builds a tree from a sorted list of feature IDs.
type Info ¶
type Info struct {
Path string `yaml:"path" json:"path"`
Status string `yaml:"status" json:"status"`
Deps []string `yaml:"deps" json:"deps"`
Refs []string `yaml:"refs" json:"refs"`
Children []ChildInfo `yaml:"children,omitempty" json:"children,omitempty"`
Plans []string `yaml:"plans,omitempty" json:"plans,omitempty"`
Sections []SectionInfo `yaml:"sections" json:"sections"`
}
Info is the top-level metadata structure for a single feature.
type NewOptions ¶
type NewOptions struct {
Title string // Human-readable feature title (required).
Slug string // Feature slug (directory name); auto-generated from Title if empty.
Parent string // Parent feature ID for creating a sub-feature.
Status string // Initial feature status (default "Draft").
Description string // Short description for the Summary section.
DependsOn []string // Feature IDs this feature depends on.
// Body, when non-empty, is used verbatim as the README content instead of
// the generated template (e.g. a template fetched from the gallery by the
// CLI's runtime-fetch). The caller is responsible for its validity.
Body string
}
NewOptions holds the parameters for creating a new feature.
type NewResult ¶
type NewResult struct {
FeatureID string // The created feature's full ID.
FeatureDir string // Absolute path to the created feature directory.
ReadmePath string // Absolute path to the created README.md.
ChangedFiles []string // All files that were created or modified.
Info Info // Metadata for the newly created feature.
}
NewResult describes the outcome of creating a new feature.
type RelationResolver ¶
RelationResolver is a function that returns related feature IDs for a given feature.
type SectionInfo ¶
type SectionInfo struct {
Title string `yaml:"title" json:"title"`
Lines string `yaml:"lines" json:"lines"`
Items int `yaml:"items,omitempty" json:"items,omitempty"`
Children []SectionInfo `yaml:"children,omitempty" json:"children,omitempty"`
}
SectionInfo represents a heading section in the README.
func ParseSections ¶
func ParseSections(readmePath string) ([]SectionInfo, error)
ParseSections reads a README and builds a section TOC from markdown headings. Supports h2 and h3 nesting. Counts list items (lines starting with "- ") within each section.