Documentation
¶
Overview ¶
Package validate implements the custom-artifact structural checks spec-lifecycle.md §3.3 wires into each gate's pre-check ("lifecycle validate --stage <s>") and implementation-plan.md §2.3 assigns to M2. On top of internal/spec's delta-grammar parser (which this package delegates to for every specs/**/spec.md delta file — one grammar code path, never duplicated: this package never re-implements a rule ParseDelta already enforces), it checks the three artifacts the OpenSpec format itself has no opinion on:
- proposal.md: a "---"-delimited YAML frontmatter block is present, and its `issue` field is a non-empty string (spec-lifecycle.md §4's proposal row, §10's sourceTracking join key).
- design.md: an explicit NFR-discharge section ("## NFR Discharge", case/hyphen-insensitive) is present (spec-lifecycle.md §4's design row, §4.1's NFR routing rule, §7's ADR-proposal seam).
- tasks.md: every "## Milestone <n>: <name>" block carries the four fixed labels **Goal** / **Deliverables** / **Validation contract** / **Steps**, and Validation contract has at least one non-blank line under it (spec-lifecycle.md §4.2, verbatim).
Stage -> artifact mapping ¶
Derived from spec-lifecycle.md §4's artifact table (its Stage column) and §3.3's gate mechanics: each stage's `lifecycle validate --stage <s>` checks only the artifact(s) *produced during* that stage. An earlier stage's artifacts were already gated approved by the time a later stage runs — the schema's requires: DAG (internal/schema) already establishes their existence is a precondition, not something a later stage's validate call needs to re-check.
refine -> proposal.md + every changes/<change>/specs/**/spec.md delta design -> design.md plan -> tasks.md
(The bug flow's compressed profile — spec-lifecycle.md §8 — and its repro/fix stage names are out of scope for this package; M2 covers only the three feature-flow stages. A future bug-flow validate call is a straightforward extension of the same shape, deferred to whichever milestone wires the bug flow's gate records.)
A stage whose artifact (or, for refine, whose specs/ delta directory) does not exist at all is reported as a single "missing_artifact" Finding, not a read error — `lifecycle validate` can legitimately run before an agent has produced anything yet, as a pre-check sanity probe (spec-lifecycle.md §3.3 step 2).
Warning-level polish ¶
One additional, WARNING-only (never error-, never exit-code-affecting) heuristic runs over each specs/**/spec.md delta: a "### Requirement:" heading that sits under an H2 section OpenSpec's own delta grammar does not recognize (i.e. not one of ADDED/MODIFIED/REMOVED/RENAMED Requirements) is silently ignored by both the real oracle and this package's own internal/spec.ParseDelta (see internal/spec/doc.go's divergence table) — a genuine "requirement-shaped drop with no diagnostic" that the M1 verifier flagged. This package's warning scan is a plain, non-fence-mask-aware line scan (deliberately simpler than internal/spec's own header detection): it may rarely false-positive on a "### Requirement:"-shaped line inside a fenced code sample, which is acceptable for an advisory that never affects validity or the exit code, and it does not change internal/spec's parsing behavior or the conformance corpus's results in any way.
Index ¶
Constants ¶
const ( ChangeTypeFeature = "feature" ChangeTypeBug = "bug" )
Change type vocabulary (spec-lifecycle.md §8, §0's Stage glossary). Recorded at intake in proposal.md's frontmatter (this milestone's own decision — see ProposalMeta's doc comment) and consumed by internal/approve/internal/status to key the bug-vs-feature stage set (implementation-plan.md §2.11).
Variables ¶
var Stages = []Stage{StageRefine, StageDesign, StagePlan}
Stages lists every recognized Stage value, in gate order — the set `lifecycle validate --stage` accepts.
Functions ¶
func ArtifactsForStage ¶
ArtifactsForStage returns the artifact path(s) — relative to a change folder — that `stage` gates (the package doc's Stage -> artifact table). Useful for a caller (e.g. the CLI) that wants to name what it's about to check without duplicating the mapping.
func HasArtifact ¶
HasArtifact reports whether the named file (e.g. "tasks.md") exists directly under dir.
func HasSpecsDeltas ¶
HasSpecsDeltas reports whether dir/specs contains at least one spec.md delta file — used by internal/approve to decide whether a promoted bug's "repro" gate should also validate a specs/ delta (spec-lifecycle.md §8: "If the repro reveals mis-specced behavior... gate it like a feature refine").
Types ¶
type Finding ¶
type Finding struct {
File string `json:"file"`
Line int `json:"line,omitempty"`
Kind string `json:"kind"`
Message string `json:"message"`
Severity Severity `json:"severity"`
}
Finding is one precise, position-anchored validation result: which file, which line (0 if not tied to a single line), a stable machine Kind a caller can switch on without matching prose, and a human-readable Message.
func Change ¶
Change validates the artifact(s) a stage gates inside a single change folder (dir — e.g. "openspec/changes/042-user-auth", absolute or relative; every Finding's File is derived from dir exactly as given). Findings are returned in a stable order (by File, then Line); a non-nil error means validation could not even be attempted (e.g. a change directory that exists but isn't readable) — distinct from findings, which are validation *results*, not failures to run.
func Plan ¶
Plan exports validatePlan — see Proposal's doc comment. Used directly by internal/approve's bug-flow "fix" gate when tasks.md is present (spec-lifecycle.md §8: "tasks.md optional").
func Proposal ¶
Proposal exports validateProposal for callers outside this package that need proposal.md's own structural check without going through Change's three-stage dispatch (internal/approve's bug-flow "repro" gate, which checks proposal.md but — unlike StageRefine — does not always require a specs/ delta; spec-lifecycle.md §8).
func SpecsDeltas ¶
SpecsDeltas exports validateSpecsDeltas — see Proposal's doc comment.
type ProposalMeta ¶
ProposalMeta is the intake-time metadata proposal.md's frontmatter carries beyond the pass/fail structural check validateProposal (Proposal) already performs: the sourceTracking join key, a proposed design-skip, and the change type.
Type is spec-lifecycle.md §8/§2.11's "change type recorded at intake" — the spec names the concept ("status/guard/archive gate-checks key off the change type recorded at intake") but does not pin exactly where that's recorded. This package decides: proposal.md's frontmatter, a `type: feature|bug` field alongside the already-load-bearing `issue` and `designSkip` fields (schema.yaml's proposal artifact documents it for the same reason). Absent (or non-string) is ChangeTypeFeature — a proposal.md written before this decision landed still parses.
func ReadProposalMeta ¶
func ReadProposalMeta(dir string) (ProposalMeta, error)
ReadProposalMeta reads dir/proposal.md's frontmatter fields. It does NOT itself validate structure — call Proposal(dir) (or Change(dir, StageRefine)) first if that matters to the caller. A missing file or unparsable/absent frontmatter yields a zero ProposalMeta (Type defaults to ChangeTypeFeature) and a nil error: this is metadata extraction for an already-validated (or not-yet-existing) artifact, not a second validation pass.