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
- func CheckCollision(specRoot, slug string, force bool) error
- func CrossRepoPromote(specRoot, slug string, transformed []byte, seedBody string) (ideaAbs, archivedAbs string, err error)
- func HasCrossRepo(links []BackLink) bool
- func Preflight(specRoot string, subjects []string) error
- func ResolveSeed(specRoot, slug string) (string, error)
- func SameRepoPromote(specRoot, slug string, transformed []byte) (string, error)
- func Transform(seed SeedContent, opts TransformOptions) ([]byte, error)
- type BackLink
- type Paths
- type ReconcileResult
- type RepoRef
- type Result
- type SeedContent
- type SeedFate
- type TransformOptions
- type VerdictMode
Constants ¶
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 ¶
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 ¶
HasCrossRepo reports whether any back-link is cross-repo. Cross-repo presence selects the cross-repo promotion path.
func Preflight ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
type ReconcileResult ¶
ReconcileResult records a same-repo back-link rewrite for the stdout summary.
func ReconcileSameRepoBackLinks ¶
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 ¶
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.
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).