ideapromote

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: 11 Imported by: 0

Documentation

Overview

Package ideapromote implements `specscore idea promote <slug>` — turning a sidekick seed (spec/ideas/seeds/<slug>.md) into a lint-clean Idea (spec/ideas/<slug>.md). See spec/features/cli/idea/promote/README.md.

This file houses the pre-mutation resolution primitives: resolving the seed path, detecting a destination collision, and computing the paths the verb will touch for the clean-tree pre-flight. Mutation logic (move/transform, back-link reconcile, cross-repo archive, verdict carry-forward) lives in sibling files.

Index

Constants

View Source
const DefaultVerdictMode = VerdictPointer

DefaultVerdictMode is the carry-forward mode used when neither the --verdict flag nor specscore.yaml promote.verdict_carry_forward is set.

Variables

This section is empty.

Functions

func CheckCollision

func CheckCollision(specRoot, slug string, force bool) error

CheckCollision returns an *exitcode.Error with code 1 (Conflict) when a destination Idea already exists at spec/ideas/<slug>.md and force is false. With force=true an existing destination is permitted.

func CrossRepoPromote

func CrossRepoPromote(specRoot, slug string, transformed []byte, seedBody string) (ideaAbs, archivedAbs string, err error)

CrossRepoPromote performs the cross-repo path: create the new Idea at spec/ideas/<slug>.md by copy+transform, then git-mv the seed to spec/ideas/archived/<slug>.md with its frontmatter `status` set to `promoted` and a `promoted_to: <slug>` key added. It NEVER sets `deprecated`. Returns the absolute Idea path and the absolute archived seed path.

The new Idea is written first (so a write failure leaves the seed untouched), then the seed is moved+marked.

func HasCrossRepo

func HasCrossRepo(links []BackLink) bool

HasCrossRepo reports whether any back-link is cross-repo. Cross-repo presence selects the cross-repo promotion path.

func Preflight

func Preflight(specRoot string, subjects []string) error

Preflight verifies that every repo-relative path in subjects is clean in the source working tree (no staged, unstaged, or untracked changes). It returns an *exitcode.Error with code 7 (DirtyTree) naming the dirty paths when any is dirty; a non-git repo is treated as vacuously clean, mirroring `idea relocate`'s pre-flight discipline.

subjects are deduplicated; a non-existent path that is untracked-clean (git reports nothing) does not trip the check.

func ResolveSeed

func ResolveSeed(specRoot, slug string) (string, error)

ResolveSeed verifies a seed exists at spec/ideas/seeds/<slug>.md within specRoot. When absent, it returns an *exitcode.Error with code 3 (NotFound) naming the missing path; no file is touched.

func SameRepoPromote

func SameRepoPromote(specRoot, slug string, transformed []byte) (string, error)

SameRepoPromote performs the same-repo path: git-mv the seed to the Idea path then overwrite it with the transformed Idea body. Returns the absolute destination Idea path.

In a git repo, the pure rename is committed BEFORE the transform is written, so the move is recorded as a 100%-similarity rename that `git log --follow spec/ideas/<slug>.md` reaches even though the subsequent transform rewrites the file substantially. The transform is then staged (committing it is left to the caller / lint-fix step). In a non-git workspace the rename is a plain os.Rename and history is moot.

func Transform

func Transform(seed SeedContent, opts TransformOptions) ([]byte, error)

Transform builds the lint-clean Idea body from a parsed seed. The seed's H1 becomes `# Idea: <title>`, frontmatter is dropped, the prose body folds into ## Context, every other canonical section gets its HTML-comment prompt (via idea.Scaffold), and the verdict is carried forward per opts.VerdictMode.

Types

type BackLink struct {
	// RepoRoot is the absolute root of the repo containing the entry.
	RepoRoot string
	// RelPath is the repo-relative path of the file containing the entry.
	RelPath string
	// Line is the 0-based index of the entry line within the file.
	Line int
	// Target is the raw markdown link target (the URL between the
	// parentheses).
	Target string
	// CrossRepo is true when the target is `<repo-slug>:`-qualified
	// (per sidekick-capture's cross-repo back-link format); false when
	// it is a bare relative path (same-repo).
	CrossRepo bool
}

BackLink is one `## Sidekick Seeds Generated` entry that references the promoted seed.

func DiscoverBackLinks(repos []RepoRef, slug string) ([]BackLink, error)

DiscoverBackLinks scans the `## Sidekick Seeds Generated` sections of every Markdown file under spec/ in each repo for list entries whose link target references spec/ideas/seeds/<slug>.md, returning each as a classified BackLink. repos is the set of repos to scan (source + siblings).

type Paths

type Paths struct {
	// SeedRel is the source seed path: spec/ideas/seeds/<slug>.md.
	SeedRel string
	// IdeaRel is the destination Idea path: spec/ideas/<slug>.md.
	IdeaRel string
	// ArchivedSeedRel is the cross-repo archive destination:
	// spec/ideas/archived/<slug>.md.
	ArchivedSeedRel string
}

Paths bundles the repo-relative on-disk locations the promote verb reasons about for a given slug.

func PathsFor

func PathsFor(slug string) Paths

PathsFor returns the canonical repo-relative paths for a slug.

type ReconcileResult

type ReconcileResult struct {
	RepoRoot string
	RelPath  string
}

ReconcileResult records a same-repo back-link rewrite for the stdout summary.

func ReconcileSameRepoBackLinks(links []BackLink, slug string) ([]ReconcileResult, error)

ReconcileSameRepoBackLinks rewrites every same-repo back-link entry that pointed at spec/ideas/seeds/<slug>.md so it points at the new spec/ideas/<slug>.md, preserving each entry's relative-path depth. Cross-repo entries are left untouched. Returns the list of files updated.

type RepoRef

type RepoRef struct {
	Path string
}

RepoRef is a minimal repo handle for back-link scanning.

type Result

type Result struct {
	// Slug is the promoted slug.
	Slug string `json:"slug" yaml:"slug"`
	// IdeaPath is the absolute path of the created Idea.
	IdeaPath string `json:"idea_path" yaml:"idea_path"`
	// SeedFate is "moved" (same-repo) or "archived" (cross-repo).
	SeedFate SeedFate `json:"seed_fate" yaml:"seed_fate"`
	// SeedPath is the absolute path the seed ended up at: the Idea path
	// (moved) or the archived path (archived).
	SeedPath string `json:"seed_path" yaml:"seed_path"`
	// ReconciledBackLinks lists the repo-relative paths of same-repo
	// back-link files rewritten from the seeds path to the Idea path.
	// Empty in the cross-repo path. Always non-nil for stable JSON output.
	ReconciledBackLinks []string `json:"reconciled_back_links" yaml:"reconciled_back_links"`
}

Result is the deterministic, structured summary of a successful promotion. It drives both the text summary and the --format json|yaml output, mirroring `idea relocate`'s stdout-format shape.

func (Result) FormatText

func (r Result) FormatText() string

FormatText renders the deterministic human-readable summary: the created Idea path, the seed's fate, and each reconciled back-link.

type SeedContent

type SeedContent struct {
	// Frontmatter holds the YAML frontmatter key→raw-value lines (in
	// encounter order) when the seed opens with a `---` fence. Empty when
	// the seed has no frontmatter.
	Frontmatter map[string]string
	// FrontmatterOrder preserves the key order of Frontmatter.
	FrontmatterOrder []string
	// Title is the text after the first `# ` H1 line (the seed's title).
	// Empty when the seed has no H1.
	Title string
	// Body is the prose between the title and the first `## ` H2 section
	// (or end of file), trimmed of surrounding blank lines. This is what
	// folds into the Idea's ## Context.
	Body string
	// Verdict is the full `## Consilium Verdict` section (heading line
	// included) when present, else empty.
	Verdict string
}

SeedContent is the parsed shape of a sidekick seed relevant to the promote transform.

func ParseSeed

func ParseSeed(content string) SeedContent

ParseSeed parses a sidekick-seed file body into its promote-relevant parts: optional YAML frontmatter, the H1 title, the prose body up to the first H2, and a ## Consilium Verdict section if present.

type SeedFate

type SeedFate string

SeedFate is the disposition of the source seed after promotion.

const (
	// FateMoved is the same-repo outcome: the seed was git-moved to the
	// Idea path.
	FateMoved SeedFate = "moved"
	// FateArchived is the cross-repo outcome: the seed was archived to
	// spec/ideas/archived/<slug>.md.
	FateArchived SeedFate = "archived"
)

type TransformOptions

type TransformOptions struct {
	Slug string
	// Owner defaults to the seed's captured_by / unknown when empty.
	Owner string
	// Date defaults to today (UTC) when empty.
	Date string
	// VerdictMode selects how the seed verdict is carried forward.
	VerdictMode VerdictMode
	// SeedRefForPointer is the provenance reference written by the
	// pointer mode (e.g. the seed's repo-relative path). When empty the
	// pointer falls back to the seed filename.
	SeedRefForPointer string
}

TransformOptions controls the seed→Idea transform.

type VerdictMode

type VerdictMode string

VerdictMode selects how the seed's ## Consilium Verdict is carried into the created Idea.

const (
	// VerdictPointer writes a single-line provenance pointer to the verdict.
	VerdictPointer VerdictMode = "pointer"
	// VerdictFull copies the entire ## Consilium Verdict section.
	VerdictFull VerdictMode = "full"
	// VerdictDrop omits the verdict entirely.
	VerdictDrop VerdictMode = "drop"
)

func ResolveVerdictMode

func ResolveVerdictMode(specRoot string, flag VerdictMode) VerdictMode

ResolveVerdictMode picks the effective carry-forward mode: the --verdict flag wins when set (non-empty), otherwise the project default from specscore.yaml promote.verdict_carry_forward, otherwise the package default (pointer). An unrecognized config value is ignored in favor of the default — flag validation (exit 2) already guards the user-facing path; a malformed config should not crash the verb.

func ValidateVerdict

func ValidateVerdict(raw string) (VerdictMode, error)

ValidateVerdict parses a --verdict flag value into a VerdictMode. An empty string is allowed (it signals "unset" — the caller falls back to config or the default). Any other unrecognized value returns an *exitcode.Error with code 2 (InvalidArgs).

Jump to

Keyboard shortcuts

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