Documentation
¶
Overview ¶
Package lesson parses and scaffolds single-file Lesson artifacts at spec/lessons/<slug>.md.
A Lesson records a gap in process — a missing check, gate, convention, or review step that let a defect ship unnoticed — and climbs a three-rung enforcement ladder (Recorded -> Stated -> Enforced) as the gap is closed by an increasingly binding mechanism. It is deliberately the flattest kind in the spec tree: no hierarchy, no source-Feature dependency, a single status line, and a free-form prose body whose only structural requirement is that four sections exist (lint checks presence, never content) — Incident, Process gap, Check, Enforcement — matching the append-only lessons-learned convention this artifact kind formalizes.
Package lesson — lifecycle transition orchestration for the Lesson kind.
This file hosts ChangeStatus, the kind-specific orchestrator invoked by `specscore lesson change-status`. It composes pkg/lifecycle/ primitives (state-machine validation, status-line rewrite, rollback) and adds the Lesson-specific structured `**Superseded By:**` successor reference — mirroring pkg/plan/transitions.go exactly, minus the execution band (a Lesson has none: every rung of the ladder is human-authored).
LINT INVOCATION lives in the cobra adapter (internal/cli/lesson.go), NOT here, to avoid an import cycle: pkg/lint imports pkg/lesson for the lesson-* lint rules, so pkg/lesson cannot depend back on pkg/lint. The adapter passes a PostMutationHook callback into ChangeStatus; this package only knows "run the post-mutation hook, and roll back if it fails".
Index ¶
- Constants
- Variables
- func IsSingleFileLessonPath(lessonsDir, filePath string) bool
- func LegalChangeStatusTargetNames() []string
- func LegalTransitionMatrix() string
- func Recur(path, note string) (int, error)
- func ResolveLessonFile(lessonsDir, slug string) (string, error)
- func Scaffold(opts ScaffoldOptions) ([]byte, error)
- func ValidateSlug(slug string) error
- type ChangeStatusOptions
- type ChangeStatusResult
- type Lesson
- type PostMutationHook
- type ScaffoldOptions
Constants ¶
const FormatURL = "https://specscore.md/lesson-specification"
FormatURL is the canonical spec URL for the Lesson document type. It is carried verbatim in both the frontmatter `format:` field and the adherence-footer line, per the artifact-frontmatter-convention.
Variables ¶
var RequiredSections = []string{"Incident", "Process gap", "Check", "Enforcement"}
RequiredSections is the closed, ordered set of H2 section headings every Lesson body MUST declare. Lint checks presence ONLY — never content, length, or wording — so a section holding nothing but a `<!-- TODO -->` prompt is lint-clean. Process gap and Enforcement are the load-bearing pair: a Lesson describing an Incident but naming no gap in the process is the useless entry the Process-gap requirement exists to refuse, and a Lesson proposing no enforcement path is the other half of that same refusal.
Functions ¶
func IsSingleFileLessonPath ¶
IsSingleFileLessonPath reports whether filePath looks like a single-file Lesson candidate location — directly under lessonsDir, with a `.md` extension, and not named README.md (the index file). It does NOT read the file; callers still must validate the title prefix via Parse().
func LegalChangeStatusTargetNames ¶
func LegalChangeStatusTargetNames() []string
LegalChangeStatusTargetNames returns the canonical-titled names of the legal --to values (every To column in the KindLesson matrix). Filtering lifecycle.LegalStatuses — itself already alphabetically sorted — preserves that order, so no separate sort step is needed.
func LegalTransitionMatrix ¶
func LegalTransitionMatrix() string
LegalTransitionMatrix returns a human-readable, ANSI-free rendering of the Lesson legal-transition matrix, suitable for cobra `Long` help text.
func Recur ¶
Recur records that a Lesson's gap manifested again: it increments the `**Recurred:** N` header count (inserting the field, defaulted to 0 then incremented to 1, when a pre-existing Lesson predates it) and appends a dated bullet — with the optional note — to a `## Recurrences` section (created immediately before the adherence footer when absent). It does NOT change `**Status:**`: a recurrence is a signal that a lesson needs to graduate, not a graduation itself — the decision to promote stays a deliberate `change-status` call. Returns the new recurrence count.
func ResolveLessonFile ¶
ResolveLessonFile resolves <slug> to an existing Lesson file under lessonsDir (spec/lessons/<slug>.md). A slug that does not resolve returns exit 3 (NotFound), naming the canonical path.
func Scaffold ¶
func Scaffold(opts ScaffoldOptions) ([]byte, error)
Scaffold returns a lint-clean flat Lesson file body: the artifact-frontmatter-convention frontmatter (`format:` + `status:` mirroring the body `**Status:** Recorded`), the `# Lesson:` title, the body-metadata header, the four required sections (Incident, Process gap, Check, Enforcement) with HTML-comment prompts, and the adherence footer whose URL agrees with `format:`.
A freshly scaffolded Lesson is immediately lint-clean: every required section exists (lint checks presence only, never content), so recording a lesson is a single command with no required flags beyond the slug — the friction-near-zero design this artifact kind depends on for agents to actually use it under time pressure.
func ValidateSlug ¶
ValidateSlug returns nil when slug is a lowercase, hyphen-separated, URL-safe identifier with no `/`.
Types ¶
type ChangeStatusOptions ¶
type ChangeStatusOptions struct {
// SpecRoot is the project root that contains the `spec/` subtree (NOT the
// `spec/` directory itself). The Lesson is resolved at
// SpecRoot/spec/lessons/<slug>.md.
SpecRoot string
// Slug is the Lesson slug. Caller is expected to have validated it via
// lesson.ValidateSlug.
Slug string
// To is the canonical (title-case) target status.
To lifecycle.Status
// Note is the optional free-form markdown transition note, written as a
// `## Resolution` section. REQUIRED (enforced by the cobra adapter) for
// the Withdrawn and Superseded dispositions.
Note string
// Successor is the slug of the lesson that supersedes this one. REQUIRED
// (enforced by the cobra adapter) for --to=Superseded, rejected
// otherwise. Written as a `**Superseded By:** <slug>` header line.
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 ¶
ChangeStatusResult is the success payload returned on exit 0. The cobra adapter formats it as the `<slug>: <from> → <to>` success line.
func ChangeStatus ¶
func ChangeStatus(opts ChangeStatusOptions) (ChangeStatusResult, error)
ChangeStatus performs a Lesson-kind lifecycle transition end-to-end.
Flow:
- Resolve <slug> to an existing Lesson file. A missing file returns exit 3.
- lifecycle.Validate against the KindLesson matrix. Illegal transitions return exit 4.
- lifecycle.Rewrite the **Status:** line; capture original for rollback.
- Optionally write the `**Superseded By:**` successor reference and the `## Resolution` note, each rolled back together with the status line.
- Invoke the PostMutation hook. Failure → full rollback + exit 10.
type Lesson ¶
type Lesson struct {
Path string // absolute path on disk
Slug string // filename without `.md`
HasLessonTitle bool // first H1 line was `# Lesson: <title>`
TitleLine int // 1-based line number of the title (0 when absent)
Title string // the `<title>` portion after `# Lesson: `
Status string // value of `**Status:**` (empty when missing)
StatusLine int // 1-based line of the field; 0 when absent
Date string // value of `**Date:**` (empty when missing)
DateLine int // 1-based line of the field; 0 when absent
Owner string // value of `**Owner:**` (empty when missing)
OwnerLine int // 1-based line of the field; 0 when absent
Recurred int // parsed `**Recurred:**` count; 0 when absent or unparsable
RecurredRaw string // raw value as written
RecurredLine int // 1-based line of the field; 0 when absent
RecurredValid bool // true when RecurredRaw parsed cleanly as a non-negative integer
SupersededBy string // value of `**Superseded By:**` (empty when missing)
SupersededByLine int // 1-based line of the field; 0 when absent
// SectionLines maps a present H2 section title to its 1-based heading
// line. Only sections found in the body appear here; callers check
// RequiredSections membership against this map's keys to find gaps.
SectionLines map[string]int
}
Lesson is a parsed single-file Lesson artifact.
func Discover ¶
Discover walks the direct children of lessonsDir and returns the parsed single-file Lessons found there, sorted alphabetically by Slug.
It selects candidates via IsSingleFileLessonPath (which excludes README.md and anything not directly under lessonsDir), Parses each, and keeps only files whose first H1 was `# Lesson: <title>` (HasLessonTitle == true).
An absent lessonsDir is not an error: Discover returns an empty slice and nil.
func Parse ¶
Parse reads a candidate Lesson file. It returns a populated Lesson even when the file is not actually a Lesson (HasLessonTitle == false in that case) so callers can distinguish "not a Lesson" from "malformed Lesson".
func (*Lesson) HasSection ¶
HasSection reports whether title is present in the parsed body as an H2 heading.
func (*Lesson) MissingRequiredSections ¶
MissingRequiredSections returns the subset of RequiredSections absent from the parsed body, in RequiredSections order. Returns nil when none are missing.
type PostMutationHook ¶
type PostMutationHook func() error
PostMutationHook is the callback the cobra adapter wires to `specscore spec lint --fix`. It MUST return nil on success; a non-nil return triggers full rollback of every on-disk mutation and the error is returned by ChangeStatus.