feature

package
v0.14.0 Latest Latest
Warning

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

Go to latest
Published: Jun 18, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

README

pkg/feature

Feature discovery, traversal, metadata, dependency resolution, and scaffolding. Pure library functions — no cobra, no fmt.Print, no os.Exit.

Outstanding Questions

None at this time.

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:

  1. Resolve a `<feature_id>` (possibly slash-bearing, e.g. "cli/idea/change-status") to its canonical README path.
  2. 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.
  3. 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

View Source
const (
	DefaultSpecDir = "spec"
	FeaturesSubDir = "features"
)

Default directory layout constants.

View Source
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

View Source
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.

View Source
var ValidStatuses = []string{"Draft", "In Review", "Approved", "Implementing", "Stable", "Amending", "Rejected", "Deprecated"}

ValidStatuses lists the allowed feature lifecycle statuses.

Functions

func BoolPtr

func BoolPtr(b bool) *bool

BoolPtr returns a pointer to a bool value.

func CountOpenQuestions

func CountOpenQuestions(readmePath string) (int, error)

CountOpenQuestions counts list items in the ## Open Questions section.

func DepsResolver

func DepsResolver(featuresDir, featureID string) ([]string, error)

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

func Exists(featuresDir, featureID string) bool

Exists checks if a feature ID corresponds to a valid feature directory with README.md.

func ExtractFeatureID

func ExtractFeatureID(item string) string

ExtractFeatureID extracts a feature ID from a dependency list item.

func ExtractOpenQuestions

func ExtractOpenQuestions(readmePath string) ([]string, error)

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

func FeatureIDFromRelativePath(relPath string) string

FeatureIDFromRelativePath converts a relative path like "../cli/README.md" to a feature ID like "cli".

func FeatureIDs

func FeatureIDs(features []Feature) []string

FeatureIDs is a convenience helper that extracts just the ID strings from a slice of Feature values.

func FilterFocusedFeatures

func FilterFocusedFeatures(allFeatures []string, targetID, direction string) []string

FilterFocusedFeatures returns features relevant to a focused tree view.

func FindFeatureRefs

func FindFeatureRefs(featuresDir, featureID string) ([]string, error)

FindFeatureRefs finds all features that reference the given featureID as a dependency.

func FindLinkedPlans

func FindLinkedPlans(repoRoot, featureID string) ([]string, error)

FindLinkedPlans scans spec/plans/*/README.md for plans that reference the given feature.

func FindSpecRepoRoot

func FindSpecRepoRoot(startDir string) (string, error)

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

func GenerateReadme(title, status, description string, deps []string) string

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

func GenerateSlug(title string) string

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

func IsValidStatus(status string) bool

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

func ParseContentsTable(readmePath string) (map[string]bool, error)

ParseContentsTable reads a README and extracts entries from the ## Contents section. Returns a map of directory names found in the table.

func ParseDependencies

func ParseDependencies(readmePath string) ([]string, error)

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

func ParseFeatureStatus(readmePath string) (string, error)

ParseFeatureStatus extracts the status from a feature README. Looks for patterns like "**Status:** Draft" or "Status: Stable".

func ParseFeatureTitle

func ParseFeatureTitle(readmePath string) (string, error)

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

func ParseFieldNames(fieldsStr string) ([]string, error)

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

func ReadmePath(featuresDir, featureID string) string

ReadmePath returns the absolute path to a feature's README.md.

func RefsResolver

func RefsResolver(featuresDir, featureID string) ([]string, error)

RefsResolver returns features that depend on the given feature.

func UpdateFeatureIndex

func UpdateFeatureIndex(indexPath, featureID, status, description string) (bool, error)

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

func UpdateParentContents(parentReadmePath, childSlug, description string) (bool, error)

UpdateParentContents adds or updates the ## Contents section in the parent's README. Returns true if the file was modified.

func ValidateFormat

func ValidateFormat(format string) error

ValidateFormat checks the format flag value is valid.

func ValidateSlug

func ValidateSlug(slug string) error

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:

  1. 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)).
  2. resolveFeatureID(featuresDir, featureID) — exit-3 if the `<feature_id>/README.md` path doesn't exist.
  3. lifecycle.Validate(...) — reads the artifact's current Status and exit-4s if (from, to) isn't in the Feature matrix.
  4. 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

func DiscoverChildFeatures(featuresDir, featureID, readmePath string) ([]ChildInfo, error)

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.

func Discover

func Discover(featuresDir string) ([]Feature, error)

Discover walks featuresDir and returns all features sorted alphabetically. A feature is any directory containing a README.md. Directories prefixed with _ are skipped (reserved for tooling).

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.

func GetInfo

func GetInfo(featuresDir, featureID string) (*Info, error)

GetInfo builds and returns the full Info for a 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.

func New

func New(featuresDir string, opts NewOptions) (*NewResult, error)

New scaffolds a new feature directory with a README template. It does NOT perform any git operations — those belong in the CLI layer.

type RelationResolver

type RelationResolver func(featuresDir, featureID string) ([]string, error)

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.

Jump to

Keyboard shortcuts

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