lesson

package
v0.31.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

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

View Source
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

View Source
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

func IsSingleFileLessonPath(lessonsDir, filePath string) bool

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

func Recur(path, note string) (int, error)

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

func ResolveLessonFile(lessonsDir, slug string) (string, error)

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

func ValidateSlug(slug string) error

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

type ChangeStatusResult struct {
	Slug string
	From lifecycle.Status
	To   lifecycle.Status
}

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:

  1. Resolve <slug> to an existing Lesson file. A missing file returns exit 3.
  2. lifecycle.Validate against the KindLesson matrix. Illegal transitions return exit 4.
  3. lifecycle.Rewrite the **Status:** line; capture original for rollback.
  4. Optionally write the `**Superseded By:**` successor reference and the `## Resolution` note, each rolled back together with the status line.
  5. 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

func Discover(lessonsDir string) ([]*Lesson, error)

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

func Parse(path string) (*Lesson, error)

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

func (l *Lesson) HasSection(title string) bool

HasSection reports whether title is present in the parsed body as an H2 heading.

func (*Lesson) MissingRequiredSections

func (l *Lesson) MissingRequiredSections() []string

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.

type ScaffoldOptions

type ScaffoldOptions struct {
	Slug  string
	Title string // defaults to a title-cased slug
	Owner string // defaults to "unknown"
	Date  string // ISO-8601 (YYYY-MM-DD); defaults to today's UTC date
}

ScaffoldOptions controls the flat Lesson file Scaffold emits.

Jump to

Keyboard shortcuts

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