Documentation
¶
Overview ¶
Package decision — lifecycle transition orchestration for the Decision kind.
This file hosts ChangeStatus, the orchestrator invoked by `specscore decision change-status`. It composes pkg/lifecycle/ primitives (state-machine validation, status-line rewrite, rollback) with Decision- specific behavior that has no analogue in Idea/Plan/Task/Lesson:
- Archival is NOT an optional axis for a Decision the way it is for an Idea. pkg/lint's D-archived-location rule REQUIRES every Decision whose Status is Rejected, Superseded, or Deprecated to live under spec/decisions/archived/. ChangeStatus therefore relocates the file itself, atomically with the status rewrite, whenever `to` is one of those three dispositions — there is no separate `decision archive` verb to call afterward.
- Superseding is bidirectional. pkg/lint's D-supersedes-bidirectional rule requires the OLD decision's `**Superseded By:**` and the NEW (successor) decision's `**Supersedes:**` to point at each other. ChangeStatus writes BOTH sides in the same transition — the successor file is mutated as a side effect of transitioning the superseded one.
- The decisions index is NOT self-healing the way the ideas index is. pkg/lint's decisions-index fixer can ADD a missing row to the active table (used by `decision new`) but has no fixer that adds a missing archived-index bullet or removes an active-index row for a decision that just relocated. ChangeStatus performs both edits itself (removeActiveIndexRow / appendArchivedIndexEntry in index.go) before invoking the PostMutation hook, so the tree is lint-clean the moment the verb returns rather than depending on a lint-fixer capability that does not exist today.
LINT INVOCATION lives in the cobra adapter (internal/cli/decision.go), NOT here, mirroring every other kind package: the adapter passes a PostMutationHook callback into ChangeStatus; this package only knows "run the post-mutation hook, and roll back everything if it fails."
Cross-references:
- Decision status vocabulary + archival/supersession rules: pkg/lint/decision_rules.go (decisionValidStatuses, D-archived-location, D-superseded-requires-successor, D-supersedes-bidirectional, D-supersedes-target-exists).
- Sibling shapes: pkg/plan/transitions.go (prep band + disposition band, **Superseded By:** on --to=superseded), pkg/lesson/transitions.go (--note required for disposition targets, --successor required only for Superseded), pkg/idea/archive.go (relocate + collision + full rollback).
Index ¶
- Constants
- func AllNumbers(specRoot string) []int
- func EnsureArchivedIndexStub(specRoot string) (created bool, err error)
- func LegalChangeStatusTargetNames() []string
- func LegalTransitionMatrix() string
- func NextNumber(specRoot string) (int, error)
- func Scaffold(opts ScaffoldOptions) ([]byte, error)
- func ValidateFullSlug(slug string) error
- func ValidateSlug(slug string) error
- type ChangeStatusOptions
- type ChangeStatusResult
- type PostMutationHook
- type ScaffoldOptions
Constants ¶
const FormatURL = "https://specscore.md/decision-specification"
FormatURL is the canonical spec URL for the Decision document type, emitted in both the frontmatter `format:` field and the adherence-footer line.
Variables ¶
This section is empty.
Functions ¶
func AllNumbers ¶
AllNumbers returns all decision numbers found in spec/decisions/ and archived/, sorted ascending.
func EnsureArchivedIndexStub ¶ added in v0.32.0
EnsureArchivedIndexStub guarantees that spec/decisions/archived/ exists and contains a lint-clean README.md index. It creates the directory if absent and writes archivedDecisionsIndexStub to archived/README.md ONLY when that file does not already exist. It returns created=true iff it wrote the stub, so callers can undo exactly what they materialized on rollback.
Mirrors idea.EnsureArchivedIndexStub. specRoot is the project root that contains the spec/ subtree (NOT the spec/ directory itself).
func LegalChangeStatusTargetNames ¶ added in v0.32.0
func LegalChangeStatusTargetNames() []string
LegalChangeStatusTargetNames returns the canonical-titled names of the legal --to values, for stderr rendering and help text.
func LegalTransitionMatrix ¶ added in v0.32.0
func LegalTransitionMatrix() string
LegalTransitionMatrix returns a human-readable, ANSI-free rendering of the Decision legal-transition matrix, suitable for cobra `Long` help text.
func NextNumber ¶
NextNumber scans spec/decisions/ and spec/decisions/archived/ for the highest NNNN prefix and returns max + 1. Returns 1 if no decisions exist.
func Scaffold ¶
func Scaffold(opts ScaffoldOptions) ([]byte, error)
Scaffold returns a lint-clean Decision file body.
func ValidateFullSlug ¶ added in v0.32.0
ValidateFullSlug validates the full on-disk Decision identifier (NNNN-slug) used by `decision change-status`'s positional argument and its --successor flag. A bare slug (no NNNN- prefix), as accepted by `decision new`, is rejected here — change-status resolves directly to a file, and that file is always named NNNN-<slug>.md.
func ValidateSlug ¶
Types ¶
type ChangeStatusOptions ¶ added in v0.32.0
type ChangeStatusOptions struct {
// SpecRoot is the project root that contains the `spec/` subtree (NOT the
// `spec/` directory itself). The Decision is resolved at
// SpecRoot/spec/decisions/<Slug>.md.
SpecRoot string
// Slug is the full on-disk Decision identifier, e.g.
// "0009-go-wasm-single-engine". Caller is expected to have validated it
// via ValidateFullSlug.
Slug string
// To is the canonical (title-case) target status. The cobra adapter
// parses the raw --to value via lifecycle.ParseStatus before reaching
// this function.
To lifecycle.Status
// Note is the optional free-form markdown transition note, written as a
// `## Resolution` section atomically with the status rewrite. The cobra
// adapter REQUIRES a non-empty Note for every disposition target
// (Rejected, Superseded, Deprecated) — a Decision moving into archival
// always carries a reason, both in the artifact body and (flattened to a
// single line) as the archived-index entry's reason column.
Note string
// Successor is the full on-disk identifier of the Decision that
// supersedes this one. REQUIRED (enforced by the cobra adapter) for
// --to=Superseded, rejected otherwise. ChangeStatus writes
// `**Supersedes:** <Slug>` onto the successor file and
// `**Superseded By:** <Successor>` onto this one — the bidirectional link
// D-supersedes-bidirectional requires.
Successor string
// PostMutation is the post-rewrite hook (typically a spec-lint pass).
// Required; ChangeStatus returns exit 10 if nil.
PostMutation PostMutationHook
}
ChangeStatusOptions packages the inputs to ChangeStatus.
type ChangeStatusResult ¶ added in v0.32.0
type ChangeStatusResult struct {
Slug string
From lifecycle.Status
To lifecycle.Status
// ArchivedPath is non-empty when the transition relocated the file (every
// disposition target: Rejected, Superseded, Deprecated).
ArchivedPath string
}
ChangeStatusResult is the success payload returned on exit 0. The cobra adapter formats it as the `<slug>: <from> → <to>` success line.
func ChangeStatus ¶ added in v0.32.0
func ChangeStatus(opts ChangeStatusOptions) (ChangeStatusResult, error)
ChangeStatus performs a Decision-kind lifecycle transition end-to-end.
Flow:
- Resolve <slug> to spec/decisions/<slug>.md. A missing file returns exit 3.
- lifecycle.Validate against the KindDecision matrix. Illegal transitions return exit 4.
- When `to` is a disposition (Rejected/Superseded/Deprecated): pre-check the archived-path collision (exit 1) and, for Superseded, resolve the successor file (exit 2 if it does not exist) — BEFORE any mutation.
- Rewrite the **Status:** line; capture original for rollback.
- For Superseded: write **Supersedes:** on the successor and **Superseded By:** on this decision.
- Write the optional `## Resolution` note.
- For a disposition: relocate the file to spec/decisions/archived/ and sync both index files (remove the active row, add the archived entry).
- Invoke the PostMutation hook. Failure → full rollback + exit 10.
ChangeStatus performs all rollback internally — by the time it returns an error, the on-disk state is byte-identical to its pre-invocation shape.
type PostMutationHook ¶ added in v0.32.0
type PostMutationHook func() error
PostMutationHook is the callback the cobra adapter wires to `specscore spec lint --fix` (plus a verify pass). It MUST return nil on success; a non-nil return triggers full rollback of every on-disk mutation ChangeStatus performed, and the error is returned by ChangeStatus.