designscaffold

package
v0.0.0-...-e368b13 Latest Latest
Warning

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

Go to latest
Published: Aug 26, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package designscaffold is the shared spec-scaffolding core, promoted out of cmd/verdi/design.go (CLAUDE.md: "anything used by two or more packages lives in a shared internal/ package ... never copy-paste across packages; ... keep cmd thin"). It has two consumers: `verdi design start` (cmd/verdi/design.go) and the workbench's stub-instantiate board action (internal/workbench, spec/scoping-canvas ac-6), which scaffolds a story (or spike) spec from a declared stub's own real AC/open-question ids rather than design start's single hardcoded placeholder edge.

Rendering is template-driven (spec/scaffold-templates ac-1, render.go): Render instantiates a text/template source against a ScaffoldData value; LoadTemplate resolves that source per class, a store's own .verdi/templates/<name>.md override winning over the embedded canonical default (templates/feature.md, templates/story.md) of the same name. Feature and Story below are this package's own convenience delegates to Render for the two classes' standard scaffold shapes — both call sites resolve their class's Class.Template via LoadTemplate themselves and pass the result in, rather than this package hardcoding a class-to- filename mapping.

Index

Constants

View Source
const (
	DefaultOwners  = "[unassigned]"
	DefaultProblem = "TODO: replace with the real problem statement before accept" // vocab:identity — shared pre-vocabulary TODO placeholder (see const-block doc comment above)
	DefaultOutcome = "TODO: replace with the real outcome statement before accept" // vocab:identity — shared pre-vocabulary TODO placeholder (see const-block doc comment above)
)

DefaultOwners, DefaultProblem, and DefaultOutcome are the scaffold's own fixed placeholder values — today's hardcoded content, moved verbatim from the retired string builders into ScaffoldData inputs rather than a second copy baked into the template text, so a store override template that wants real values instead of these placeholders has a real, data-driven field to reference ({{.Owners}}/{{.Problem}}/{{.Outcome}}). Exported (spec/creation-form ac-2/ac-4): every creation surface that fills UNANSWERED fields — the board's creation form, commit-to-design's producer call — falls back to these same disclosed placeholders, one table, never a re-typed copy per consumer.

DefaultProblem/DefaultOutcome's own "before accept" (spec/verb-surfaces ac-4, disclosed choice — see each const's own vocab:identity marker below): a package-level constant shared verbatim across every creation surface (board form, commit-to-design, cli-creation's --defer-statements, design start's flagless fallback), with no model in scope at its own declaration site. Routing it would require threading a *model.Model through every one of those call sites, a materially larger refactor than this story's own disclosed, minimal scope; each CONSUMING call site that already resolves a model routes its own copy of this same reminder instead (design.go's --defer-statements disclosure, this same story, does exactly that).

Variables

This section is empty.

Functions

func Canonical

func Canonical(filename string) ([]byte, error)

Canonical returns the embedded canonical template named filename — the shipped default a store with no override of its own resolves to.

func CheckClass

func CheckClass(spec *artifact.SpecFrontmatter, want artifact.SpecClass) error

CheckClass asserts that a rendered-and-decoded scaffold's own declared class (spec.Class) agrees with want — the class its resolved template was looked up UNDER: a model.yaml classes.<id> entry (model check's checkTemplates), a --kind request (design start), or a declared stub's own class (the workbench's stub-instantiate, always story).

A class's Template filename is DATA, not code (model.Class.Template): a misconfigured model.yaml can bind one class's Template to another class's template file, and a store's own .verdi/templates/ override can simply hardcode the wrong `class:` literal. Neither SplitFrontmatter nor DecodeSpec alone catches this — both accept any well-formed spec of ANY legal class, so a story class bound to feature.md still strict-decodes clean, just as a feature. Every scaffold consumer re-asserts this identity after decoding, before trusting the render belongs to the class it was resolved for (K1): model check's checkTemplates (a broken binding must fail closed at check time, exit 2, never surface first at a real design start or stub-instantiate); design start (never write a scaffold whose own class: line disagrees with the --kind it was asked for, even though stdout and the commit message echo the REQUESTED kind); and the workbench's stub-instantiate (same guard, story class only, before any git plumbing runs).

Returns nil when spec.Class == want. The error names both classes so a caller's own wrapping (template file, declared/requested class) gives an operator every fact needed in one message.

func Feature

func Feature(tmpl []byte, specRef, storyRef, title, problem, outcome string) (string, error)

Feature renders a draft feature spec's markdown content by instantiating tmpl — the class's resolved template, LoadTemplate's embedded canonical templates/feature.md or a store's own .verdi/templates/feature.md override — against the standard feature scaffold's inputs (spec/ scaffold-templates ac-1: designscaffold stops building strings and starts rendering templates; this function becomes Render's delegate rather than its own fmt.Sprintf body). storyRef is "" when the feature carries no tracker ref at all (optional for the feature class). problem and outcome are the statement sections' own content (spec/cli-creation ac-1, ledger L-N7): every existing caller before that story passed the DefaultProblem/DefaultOutcome placeholders explicitly, byte-preserving their scaffold; `design start`'s --problem/--outcome flags are the first caller to pass real statement text through this same rendering path, rather than a second, parallel render call that could drift from it.

func HumanizeName

func HumanizeName(name string) string

HumanizeName renders a kebab-case name as a Title Case placeholder title — moved verbatim from cmd/verdi/design.go's humanizeName. Used only where no real title source exists at all (05 §CLI's own I-10: "no magic, no tracker-derived naming" rules out inventing one from anything but the caller's own name).

func LoadOverride

func LoadOverride(root, filename string) ([]byte, bool, error)

LoadOverride reads the store's own template override for filename at .verdi/templates/<filename> under root: (bytes, true, nil) when one exists, (nil, false, nil) when the store carries none — absence is never an error (the absence-changes-nothing posture) — and a real read failure or an unsafe filename fails closed.

func LoadTemplate

func LoadTemplate(root, filename string) ([]byte, error)

LoadTemplate resolves filename's template bytes: a store override at .verdi/templates/<filename> under root when one exists, else the embedded canonical template of the same name (spec/scaffold-templates outcome: "a store with no templates/ directory at all changes nothing" — the same absence-changes-nothing posture store.Open's own model.yaml resolution already established, internal/store/open.go's loadModel). filename is a class's own Class.Template value (model.Class, kernel-required non-empty) — callers resolve it from the store's already-open model, never hardcode a class-to-filename mapping here.

LoadTemplate composes the two halves below (spec/creation-form ac-4 exposed them): LoadOverride for the store's own layer, Canonical for the embedded fallback of the same name. A consumer whose canonical default is NOT the same-named embedded class template — commit-to- design, whose no-override shape is the byte-pinned legacy scaffold — composes them differently; everyone else keeps calling this.

func Render

func Render(tmpl []byte, data ScaffoldData) (string, error)

Render instantiates tmpl (a text/template source: an embedded canonical template, or a store's own .verdi/templates/<name>.md override) against data, failing closed on any template syntax error or undefined-field access rather than producing a scaffold silently missing content. Every real caller self-validates the result afterward (SplitFrontmatter + DecodeSpec, exactly as design start and stub-instantiate already did before this story); `verdi model check` (ac-3) runs the identical instantiate-then-strict-decode round trip proactively, over every resolved template, so a broken template is caught at check time rather than at a scaffold consumer's first use.

The "safe" function (K4, verified latent at the round's final review) is registered here so it is available to the embedded canonical templates AND any store override alike — see safeScalar's own doc comment for what it guards and why it is a conditional guard rather than an unconditional %q.

Render is RenderValue's typed delegate over ScaffoldData — every real spec-scaffold caller's own shape — added by the same additive refactor that introduced RenderValue (AC-1/CX-16/R-10: internal/humanartifact's policy scaffolds are RenderValue's first non-ScaffoldData caller). No behavior change: this is the identical parse/execute body RenderValue now also runs for any data shape.

func RenderValue

func RenderValue(tmpl []byte, data any) (string, error)

RenderValue is the generalized render seam every human-artifact creation surface shares (AC-1: "one resolver and renderer" for every committed human-authored artifact kind, not just the spec-store's feature/story classes ScaffoldData was scoped to). It instantiates tmpl against ANY data value with the identical parse options and error wrapping Render already established: the "safe" func registered, and missingkey=error so an undefined field access fails closed rather than rendering a silent blank. internal/humanartifact's policy/overlay/ exemption scaffolds render through this entry directly, since their data shapes (PolicyScaffoldData and friends) are not — and must not become — ScaffoldData fields; Render itself is now this function's typed ScaffoldData delegate, so there is exactly one execution body either path runs.

func Story

func Story(tmpl []byte, specRef, storyRef, title string, spike bool, links []StoryLink, problem, outcome string) (string, error)

Story renders a draft story spec's markdown content (02 §Kind registry: story (NEW), including the spike variant) by instantiating tmpl — the class's resolved template, LoadTemplate's embedded canonical templates/story.md or a store's own .verdi/templates/story.md override — against the caller's inputs (spec/scaffold-templates ac-1: this function becomes Render's delegate rather than its own strings.Builder body). Generalized beyond design start's single hardcoded placeholder edge to any set of document-level links: design start (cmd/verdi/design.go) passes exactly one placeholder implements edge with no AC of its own to bind to; the workbench's stub-instantiate board action passes the stub's REAL implements/resolves edges (derived from the stub's own acceptance_criteria/resolves list). storyRef is the required `story:` tracker scalar (validateStory requires one unconditionally, even for the spike variant) — a caller with no real tracker ref of its own (stub-instantiate has none: ac-6 binds by slug, "with no new provenance record") passes an explicit placeholder value shaped like a real one (e.g. "todo:REPLACE-ME") rather than leaving the field empty and failing self-validation. spike selects the 02 spike variant: `spike: true`, no acceptance_criteria placeholder (spikes are evidence-model-exempt, 02 §Kind registry: "Spikes are exempt from the evidence model"), links become its resolves edges. Story does not itself enforce validateStory's edge-count/type grammar — an empty or wrongly-typed links list renders content that will fail DecodeSpec, by design (callers self-validate before writing, exactly like design start already does; the canonical story.md template's own "links:\n" followed by zero entries decodes as a nil Links slice, which validateStory then rejects for a non-spike story — TestStory_Negative_ NoLinks pins this). problem and outcome are the statement sections' own content (spec/ cli-creation ac-1, ledger L-N7): every caller before that story passed the DefaultProblem/DefaultOutcome placeholders explicitly, byte- preserving its scaffold — the workbench's stub-instantiate action (internal/stubinstantiate) still always does, since it never collects real statement text from an operator; design start's --problem/ --outcome flags and its TTY interview are the only callers that pass real statement text through this same rendering path.

Types

type Field

type Field struct {
	Name string
	Kind FieldKind
}

Field is one ordered creation-surface input descriptor: the ScaffoldData field a template references, and how a surface sources its value. Produced by Fields in first-reference document order.

func Fields

func Fields(tmpl []byte) ([]Field, error)

Fields enumerates tmpl's placeholders as ordered field descriptors: first-reference document order, deduplicated, exactly the template positions rendered against the top-level ScaffoldData value. The walk changes context exactly where text/template's dot does: a range (or with, or field-invoked sub-template) body's relative fields belong to the passed value, never enumerated, while a $-rooted reference names the top-level value from ANY context and a sub-template invoked with the root dot ({{template "x" .}} at top level, {{template "x" $}} anywhere) enumerates its body against the root. Fails closed — naming the placeholder or construct, never a silently partial field list (judged-placeholder-enumeration-fail-closed) — on a syntactically broken template, a placeholder outside the ScaffoldData contract, and any construct the walker cannot prove enumerable (a local template variable's use, a whole-value {{.}}/{{$}} render against the root, an undefined sub-template).

type FieldKind

type FieldKind string

FieldKind classifies how a creation surface sources one enumerated field's value — presentation-agnostic roles, so the board form and the CLI interview read the same descriptor without a second table.

const (
	// FieldIdentity is derived from the new spec's own name (Ref):
	// surfaces collect a kebab-case name once and derive the ref, never
	// a free-text ref input.
	FieldIdentity FieldKind = "identity"
	// FieldInput is a single-line free input (Title, Owners, StoryRef).
	FieldInput FieldKind = "input"
	// FieldStatement is a multiline statement position (Problem,
	// Outcome) — the inputs guide 6.1 says a creation surface exists to
	// collect before the artifact exists.
	FieldStatement FieldKind = "statement"
	// FieldStructural is derived from creation context (Spike, Links,
	// ParentRef) — never asked of the author as a text field.
	FieldStructural FieldKind = "structural"
)

type ScaffoldData

type ScaffoldData struct {
	Ref       string
	Title     string
	Owners    string
	Problem   string
	Outcome   string
	StoryRef  string
	ParentRef string
	Links     []StoryLink
	Spike     bool
	// Pins and Dispositions are commit-to-design's content-carrying
	// fields (spec/creation-form ac-4 — the "content-carrying
	// template-contract extension" ledger L-M12's ratification
	// predicted): the board's pinned refs render as context: entries,
	// the sticky dispositions as the dispositions: block. Every other
	// consumer leaves them zero; a template failing to reference them
	// is never an error (the struct posture above).
	Pins         []artifact.Pin
	Dispositions []artifact.Disposition
}

ScaffoldData is Render's one input shape (fixed signature, docs/design/ plans/2026-07-17-extensibility-phase1-plan.md Task 8): every field a class's template might reference when scaffolding a fresh spec. Not every template uses every field — the canonical feature.md ignores Spike/Links/ParentRef entirely, and a plain (non-spike) story ignores nothing story.md itself defines — a template failing to reference a field is never an error; only referencing an UNDEFINED one is (Render's missingkey=error posture, mirroring struct field access semantics: an unknown field name is already a template execution error by construction, since ScaffoldData is a struct, not a map).

type StoryLink struct {
	Type artifact.LinkType
	Ref  string
}

StoryLink is one document-level edge a scaffolded story spec's `links:` block carries — an `implements` edge to a feature AC fragment, or (the spike variant) a `resolves` edge to an open-question fragment.

Jump to

Keyboard shortcuts

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