idearelocate

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 idearelocate implements cross-repo relocation of Idea and sidekick-seed artifacts per spec/features/cli/idea/relocate/README.md. This file houses the slug-and-target resolution primitives used by the verb's input-validation phase (Task 1 of the implementation plan). Mutation logic — file copy, in-file rewrite, cross-repo link cleanup, commits — lives in later tasks.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DirtyTreeError

func DirtyTreeError(dirty []PreflightSubject) error

DirtyTreeError formats a stderr-suitable error message naming every dirty subject, then returns an *exitcode.Error with code 7.

func ExecuteCommitPhase

func ExecuteCommitPhase(changes []RepoChange, mode CommitMode) ([]RepoChange, *CommitFailure, error)

ExecuteCommitPhase stages each change's paths and, in CommitAuto mode, commits them. On a commit failure mid-flight it returns a CommitFailure with the committed-so-far state, the failing change, and the unprocessed remainder.

Non-git repos are vacuously processed: no staging, no commit, no SHA — the verb tolerates non-git project roots in the same way preflight does, so a mid-experiment workspace not yet under VCS can still relocate artifacts on disk.

func ExecutePreCommitPhase

func ExecutePreCommitPhase(
	sourceRepoRoot string,
	source SourceArtifact,
	target TargetRepo,
	scanRepos []TargetRepo,
	slug string,
) (MutationResult, []LinkUpdateResult, error)

ExecutePreCommitPhase runs Task 3 (file copy + in-file rewrite + source delete) followed by Task 4 (cross-repo link cleanup), tracking rollback state. On any failure during these phases (other than destination collision), it restores on-disk state to its pre- invocation form and returns an *exitcode.Error with code 10 naming the failed step and the rollback actions performed.

Returns the MutationResult and per-repo link-update results when the whole pre-commit phase succeeds. Callers then proceed to Task 5's commit phase using those results.

func FindReferences

func FindReferences(repoRoot, slug string) ([]string, error)

FindReferences walks repoRoot/spec/**/*.md and returns the repo-root- relative paths of files that reference <slug> per the patterns from cli/idea/relocate#req:preflight-clean-tree-siblings-with-references:

  • Bold-prefixed metadata lines naming the slug in any of: **Source Ideas:**, **Related Ideas:**, **Supersedes:**, **Promotes To:**. The slug must appear as a comma-separated value, not as a substring of a different value.
  • Markdown links whose target ends in /<slug>.md (the slug is the last path component before .md, optionally preceded by /).

Files outside spec/ are ignored. The returned slice is sorted.

func FormatStdout

func FormatStdout(changes []RepoChange, mode CommitMode) string

FormatStdout produces the canonical per-affected-repo lines and the summary line per cli/idea/relocate#req:stdout-format. When mode=CommitNo, the [<sha7>] bracket is omitted.

func IsPathClean

func IsPathClean(repoRoot, relPath string) (bool, error)

IsPathClean reports whether <relPath> inside <repoRoot> has any uncommitted modifications (staged, unstaged, or untracked).

A non-git repoRoot (no .git directory anywhere up the tree) is treated as vacuously clean — there is no working tree to check. This matches the verb's "best-effort" pre-flight discipline: invocations against a non-git project don't fail pre-flight on the absence of git itself; the commit phase later will fail loudly if commits are requested in such a project.

func RewriteBody

func RewriteBody(content, targetRepo string) string

RewriteBody applies the two cross-repo-relocate substitution rules to content. targetRepo is the value of project.repo from the target's specscore.yaml — it replaces every word-bounded "this repo" occurrence in body prose.

Types

type ArtifactKind

type ArtifactKind string

ArtifactKind distinguishes an Idea document from a sidekick-seed.

const (
	// KindIdea marks artifacts at spec/ideas/<slug>.md.
	KindIdea ArtifactKind = "idea"
	// KindSeed marks artifacts at spec/ideas/seeds/<slug>.md.
	KindSeed ArtifactKind = "seed"
)

type CommitFailure

type CommitFailure struct {
	Failed       RepoChange
	FailedStderr string
	Committed    []RepoChange
	Unprocessed  []RepoChange
}

CommitFailure carries the post-mortem details for a stop-on-first- failure event: which change failed, the captured stderr from git-commit, the changes already committed (with SHAs), and the changes that were never attempted.

func (*CommitFailure) AsExitError

func (f *CommitFailure) AsExitError() *exitcode.Error

AsExitError formats the CommitFailure into a stderr-suitable message and wraps it in an *exitcode.Error with code 10 per cli/idea/relocate#req:stop-on-first-commit-failure.

type CommitMode

type CommitMode int

CommitMode toggles the post-mutation commit behavior.

const (
	// CommitAuto stages and commits every affected repo.
	CommitAuto CommitMode = iota
	// CommitNo stages only — no commits.
	CommitNo
)

type LinkUpdateResult

type LinkUpdateResult struct {
	RepoPath string
	Updated  []string
}

LinkUpdateResult records which files were updated in a given repository.

func UpdateCrossRepoLinks(allRepos []TargetRepo, targetRepo TargetRepo, slug string, targetRelPath string) ([]LinkUpdateResult, error)

UpdateCrossRepoLinks updates all markdown link references to the relocated slug across all SpecScore repositories.

In-repo links are rewritten to relative paths. Cross-repo links are rewritten to full GitHub URL form. Bold-prefixed metadata lines are preserved.

type MutationResult

type MutationResult struct {
	// DestinationPath is the absolute path the artifact was written to
	// inside the target repo.
	DestinationPath string
}

MutationResult records the on-disk effects of ApplyMutation. Returned to the CLI verb for use in the per-affected-repo stdout summary.

func ApplyMutation

func ApplyMutation(sourceRepoRoot string, source SourceArtifact, target TargetRepo) (MutationResult, error)

ApplyMutation performs Task 3 of cli/idea/relocate:

  • Computes the destination path by mirroring source's repo-relative path inside target.Path.
  • Rejects a pre-existing destination file with exit 1 (Conflict). No mutations are performed in this case.
  • Reads the source artifact, applies RewriteBody (org rename + "this repo" → target.RepoName), writes the result to the destination (creating parent dirs as needed).
  • Deletes the source artifact.

Task 6's pre-commit rollback wrapper is responsible for restoring state on any mid-sequence I/O failure (file copy or in-file rewrite failure). ApplyMutation itself does NOT roll back — it surfaces the underlying error so the wrapper can decide. The exception is the destination-collision case, which is treated as a pre-check and guarantees zero mutations by design.

type PreflightSubject

type PreflightSubject struct {
	RepoRoot string // absolute path to the repo
	RelPath  string // path relative to RepoRoot
}

PreflightSubject identifies one (repo, file) pair that pre-flight must verify is clean.

func CheckPreflight

func CheckPreflight(subjects []PreflightSubject) ([]PreflightSubject, error)

CheckPreflight runs IsPathClean against every subject and returns the list of dirty (repo, path) subjects, in input order. The error result is non-nil only if a git invocation itself fails; a dirty path is signaled via the returned slice, not via the error.

func PreflightSubjectsForRelocate

func PreflightSubjectsForRelocate(
	sourceRepoRoot string,
	sourceRelPath string,
	targetRepoRoot string,
	targetRelPath string,
	siblings []TargetRepo,
	slug string,
) ([]PreflightSubject, error)

PreflightSubjectsForRelocate composes the full list of preflight subjects for one relocate invocation:

  • Source repo: the artifact file + spec/ideas/README.md.
  • Target repo: the destination path + spec/ideas/README.md.
  • Each sibling SpecScore-managed repo (other than source and target) that contains a reference to the slug: every matched file path.

Caller supplies the resolved source and target. siblings is the list of additional sibling repos discovered via DiscoverSiblings; the caller is responsible for excluding source and target from siblings before passing in.

type RepoAction

type RepoAction string

RepoAction labels a repo's role in the relocate. Used both in stdout per-repo lines and to derive commit-subject formatting.

const (
	// ActionMoved is the source repo's role: the artifact was deleted
	// from it.
	ActionMoved RepoAction = "moved"
	// ActionReceived is the target repo's role: the artifact landed in
	// it.
	ActionReceived RepoAction = "received"
	// ActionUpdatedLinks is a sibling repo's role: only link rewrites.
	ActionUpdatedLinks RepoAction = "updated-links"
)

type RepoChange

type RepoChange struct {
	Repo    TargetRepo // includes Path, RepoName, Org
	Action  RepoAction
	Kind    ArtifactKind // idea or seed
	Slug    string
	Paths   []string // repo-relative paths to `git add`
	Subject string   // commit subject (computed)
	SHA     string   // populated after a successful commit; empty in CommitNo
}

RepoChange is one affected repo's slice of work — paths to stage and, in auto-commit mode, a subject line for `git commit`.

func AssembleRepoChanges

func AssembleRepoChanges(
	source TargetRepo, sourceKind ArtifactKind, sourceRelPath string,
	target TargetRepo, targetRelPath string,
	siblings []TargetRepo,
	linkUpdates map[string][]string,
	slug string,
) []RepoChange

AssembleRepoChanges builds the ordered slice of affected-repo work. Order:

  1. Source (action=moved)
  2. Target (action=received)
  3. Siblings (action=updated-links), alphabetically by RepoName, ONLY when their Updated[] is non-empty.

linkUpdates maps each affected repo's path to its updated repo-rel paths. The source and target repos additionally carry the artifact- move path: sourceRelPath in source (a `git add` of a deleted file stages the deletion), and targetRelPath in target.

type SourceArtifact

type SourceArtifact struct {
	Path string       // absolute path to the artifact file
	Kind ArtifactKind // idea or seed
}

SourceArtifact is the resolved on-disk location of a relocate-target artifact within the source project.

func ResolveSourceArtifact

func ResolveSourceArtifact(specRoot, slug string) (SourceArtifact, error)

ResolveSourceArtifact resolves <slug> per cli/idea/relocate#req:slug-resolves-idea-or-seed: Idea path is checked first at spec/ideas/<slug>.md; on miss, the seed path spec/ideas/seeds/<slug>.md is checked. Exactly one match returns that path + kind. Both existing returns an *exitcode.Error with code 5 (AmbiguousSlug). Neither existing returns code 3 (NotFound).

type TargetRepo

type TargetRepo struct {
	Path     string // absolute path to the repo root (the dir containing specscore.yaml)
	RepoName string // value of project.repo from the target's specscore.yaml (may be empty)
	Org      string // value of project.org from the target's specscore.yaml (may be empty)
}

TargetRepo is the resolved destination repository.

func DiscoverSiblings

func DiscoverSiblings(specRoot string) ([]TargetRepo, error)

DiscoverSiblings returns the same data as discoverSiblings (the unexported helper used by ResolveTargetRepo). Exported here so the CLI verb can fetch the list once and pass it into both resolution and pre-flight, without scanning the workspace twice.

func ResolveTargetRepo

func ResolveTargetRepo(specRoot, toRepo string) (TargetRepo, error)

ResolveTargetRepo resolves the --to-repo flag value per cli/idea/relocate#req:target-repo-resolution.

  • Value containing no "/" is a repo slug: scan sibling directories of specRoot's parent for specscore.yaml files and match project.repo. Single match returns it. Zero matches → exit 3 (NotFound). Multiple matches → exit 2 (InvalidArgs).
  • Value containing "/" is a path: relative to specRoot, or absolute if starting with "/". The path MUST be a directory containing a specscore.yaml; missing yaml → exit 6 (TargetNotSpecScore); missing directory → exit 3 (NotFound).

Jump to

Keyboard shortcuts

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