cardgen

package
v1.2.2 Latest Latest
Warning

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

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package cardgen is nova-card's planner: it turns a structured source (a ratchet ledger, a findings TSV, a tool's rendered help) into a directory of briefs that `nova-sprint add --brief-dir` admits as they are. Everything here is a pure function over text (docs/SPEC-CARD-CONTRACT.md section 6, generated cards): the row parsers, the PATHS computation, the wave assignment and the rendering take bytes and return bytes, so the unit tests run on fixture ledgers with no repository, no clock and no store. cmd/nova-card is the transport: it reads the worktree, resolves the base sha, runs the lint over the output and writes the directory.

Index

Constants

View Source
const AlwaysInPathsRule = "" /* 291-byte string literal not displayed */

AlwaysInPathsRule is the one sentence the brief generator, the lander and the readers hold a card's scope to (docs/SPEC-SPRINT.md section 7, the lander's checks; docs/SPEC-CARD-CONTRACT.md): the files a change must touch to keep the tree green are inside every card's PATHS. The owner, 2026-10-06: "Can we stop this whole 'test outside of paths' thing. It's wasteful."

View Source
const AsARead = "AS A READ\nA By: trailer is judged only for being present and true: it names the worker who pushed the branch under read, whoever was preferred for the card. A trailer naming another friend than a WHO line or an earlier brief expected is no finding, and attribution alone never decides a verdict; read the change against the task, its test and its PATHS.\nThe scope of this change is its PATHS line. " + AlwaysInPathsRule + "\n"

AsARead is the brief's AS A READ section, the text a reader of the work is given (sprint.FriendReadBrief carries it through the next heading): a By: trailer is judged only for being present and true, and the scope of the change is its PATHS line as AlwaysInPathsRule widens it. The sentence stays out of THE TASK: add's paths-cover-named check would otherwise refuse every generated brief for naming tla/RUNS.tsv, tla/CASES.tsv and internal/docs/catalog.go.

View Source
const Attribution = "" /* 439-byte string literal not displayed */

Attribution is the line every brief carries about its commit's By: trailer. A brief never names its author: the deal may hand any card, a pinned one too, to any worker, a friend or a fleet machine, so the worker who does the attempt names itself, and the WHO line stays a preference, never an author (the coordinator's rule: a commit names the worker who did the work, never a model and never someone who did not). A model name is never a By:. Only a Claude worker writes a Co-Authored-By trailer, its true one; the line spells no fill-in template of it, which a worker of another model completes with its own model's name. internal/card refuses a brief that writes By: and a configured name (check author-name).

View Source
const HelpExampleTest = "TestHelpExampleLinesRunAsPrinted"

HelpExampleTest is the test a help card writes when its tool's package has none that runs the example lines; four of the twenty tools have it by this name.

View Source
const HelpLineLimit = 100

HelpLineLimit is the width a help line keeps under (docs/STANDARD.md).

View Source
const MaxPaths = 8

MaxPaths is the PATHS ceiling: entries past it are merged into their directory's glob.

Variables

View Source
var Ledgers = map[string]Ledger{
	"serial-tests": {
		Name: "serial-tests", File: "internal/ci/testdata/serial-tests_allowlist.txt", Test: "internal/ci TestEveryTestOpensWithTParallel", Tier: "flash",
		Task: "The ledger {{ledger}} lists {{count}} test(s) of {{file}} that do not open with t.Parallel(): {{rows}}. Each row's reason names what makes the test unsafe beside another (t.Setenv, t.Chdir, os.Setenv, os.Chdir, a swapped package variable). Delete those rows from the ledger first, so {{test}} goes red naming each test; then make every one of those tests open with t.Parallel() as its first statement by giving it a per-test seam in place of the process-wide call: cmd.Env on the command it runs instead of t.Setenv, a directory argument or t.TempDir instead of t.Chdir, an injected value instead of a package-level swap. The test's assertions stay as they are; only the seam changes. A test that cannot be made safe this way is reported, with its row left in place, as not done." + ceilingNote + draftRule,
		Parse: func(line string) (Row, bool) {
			m := colonRowRE.FindStringSubmatch(line)
			if m == nil {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[2], Rest: m[3]}, true
		},
	},
	"slowwaits": {
		Name: "slowwaits", File: "internal/ci/testdata/slowwaits_allowlist.txt", Test: "internal/ci TestNoTestSleepsOverASecondOrWaitsOutADeadlineOverFive", Tier: "flash",
		Task: "The ledger {{ledger}} lists {{count}} test(s) of {{file}} that still write a sleep over one second or a deadline between five and thirty seconds: {{rows}}. Delete those rows from the ledger first, so {{test}} goes red naming each test; then give each test a bound under the rule's limit, or route its wait through the package's clock seam, keeping the assertion it makes. A test that waits a deadline out belongs behind the slow tag, and that move is reported, not made here." + draftRule,
		Parse: func(line string) (Row, bool) {
			m := colonRowRE.FindStringSubmatch(line)
			if m == nil {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[2], Rest: m[3]}, true
		},
	},
	"sleeps-skips": {
		Name: "sleeps-skips", File: "internal/ci/sleeps-skips_allowlist.txt", Test: "internal/ci TestNoUnitTestWaitsOnTheWallClock", Tier: "flash",
		Task: "The SLEEPS ledger {{ledger}} lists {{count}} unit test(s) of the package {{file}} that wait on the wall clock, or skip themselves with t.Skip(\"SLEEPS: ...\"): {{rows}}. Delete those rows from the ledger first, so {{test}} goes red naming each one; then make each test wait on no real time: an injected clock, testing/synctest, or a signal from the code under test in place of time.Sleep, time.After and a wall-clock deadline (docs/TESTING.md: unit tests use no real time). A test that needs real time is a functional test and moves behind the functional tag, which is reported." + draftRule,
		Parse: func(line string) (Row, bool) {
			f := strings.Split(line, "\t")
			if len(f) < 2 || f[0] == "" || f[1] == "" {
				return Row{}, false
			}
			return Row{File: f[0], Key: f[1], Rest: strings.Join(f[2:], " ")}, true
		},
	},
	"fixed-waits": {
		Name: "fixed-waits", File: "internal/ci/testdata/fixed-waits-allowlist.txt", Test: "internal/ci TestNoFixedWaitsOnTheCIPath", Tier: "flash",
		Task: "The ledger {{ledger}} lists {{count}} fixed wait(s) in {{file}}: {{rows}} (file:line, the kind of wait, the date, the reason). Delete those rows from the ledger first, so {{test}} goes red naming each site; then replace each wait: a fixed sleep becomes a poll on the condition it waited for, a short bound becomes one the code under test is handed, an elapsed-time assertion becomes an assertion on what happened. The allowlist only shrinks: a row that names no offender is refused by the test." + draftRule,
		Parse: func(line string) (Row, bool) {
			m := lineRowRE.FindStringSubmatch(line)
			if m == nil {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[2], Rest: m[3]}, true
		},
	},
	"dead-code": {
		Name: "dead-code", File: "internal/ci/testdata/dead_code_allowlist.txt", Test: "internal/ci TestDeadCode", Tier: "pro", Counted: true,
		Task: "The dead code ledger {{ledger}} carries the row `{{rows}}`: the package {{file}} has that many functions unreachable from any cmd/ root (the union over GOOS linux, darwin and windows). Run the class test {{test}} to read the names the deadcode tool reports for the package; delete each unreachable function with its tests and any helper only it used, then lower the package's count on its ledger row (to zero deletes the row) so {{test}} is green with fewer rows. A function that is reachable on one GOOS only is not dead and stays." + ceilingNote + draftRule,
		Parse: func(line string) (Row, bool) {
			m := countRowRE.FindStringSubmatch(line)
			if m == nil {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[2], Rest: m[2]}, true
		},
	},
	"namedpaths": {
		Name: "namedpaths", File: "internal/ci/testdata/namedpaths_allowlist.txt", Test: "internal/ci TestEveryNamedRepoPathExists", Tier: "pro",
		Task: "The ledger {{ledger}} carries the row `{{rows}}`: the name {{file}} looks like a path into this repository and is not one, and the row says why. Find what writes the name (grep the tree for it), then either make the name true (create the file the sentence promises, or point the sentence at the file that exists) or reword the sentence so it names no path; then delete the row, so {{test}} is green with one row fewer. The list is checked in both directions: a stale row is as red as a missing one." + draftRule,
		Parse: func(line string) (Row, bool) {
			m := firstWordRE.FindStringSubmatch(line)
			if m == nil {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[1], Rest: m[2]}, true
		},
	},
	"transcripts": {
		Name: "transcripts", File: "internal/ci/testdata/transcripts_allowlist.txt", Test: "internal/ci TestEveryTranscriptIsExecutedLineForLine", Tier: "pro",
		Task: "The ledger {{ledger}} carries the row `{{rows}}`: the `## {{file}}` section of docs/TESTS.md is executed by no test line for line through onboarding.CompareTranscript (docs/SPEC-TOOLWORK.md rule 3). Delete the row first, so {{test}} goes red naming the section; then write the test in cmd/{{file}} that runs the section's transcript line by line through onboarding.CompareTranscript, correcting the transcript where the tool's output has moved on, so the test is green. The list only shrinks." + draftRule,
		Parse: func(line string) (Row, bool) {
			m := transcriptRowRE.FindStringSubmatch(line)
			if m == nil || m[1] == "" {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[1], Rest: strings.TrimSpace(m[2])}, true
		},
	},
	"generality-fixtures": {
		Name: "generality-fixtures", File: "internal/ci/testdata/generality_text_fixtures_allowlist.txt", Test: "internal/ci TestGeneralityText", Tier: "pro", Generated: true,
		Task: "The ledger {{ledger}} carries the row `{{rows}}`: the whole file {{file}} is excepted from the generality scan because it carries fleet, person or host names as recorded data. Decide whether it still must: a synthetic fixture is rewritten with generic names and its row deleted; a true recording keeps its row and this card reports not-done with the reason. Run {{test}} with NOVA_CI_UPDATE=1 once after the change and commit what it writes; this ledger is a generated one land regenerates on a conflict." + draftRule,
		Parse: func(line string) (Row, bool) {
			m := firstWordRE.FindStringSubmatch(line)
			if m == nil {
				return Row{}, false
			}
			return Row{File: m[1], Key: m[1], Rest: m[2]}, true
		},
	},
}

Ledgers is every ledger nova-card generates from, by name. The names are the words a coordinator uses for them; the files are where internal/ci keeps them.

Functions

func AlwaysInPaths

func AlwaysInPaths(p string) bool

AlwaysInPaths says p is a file AlwaysInPathsRule puts inside every card's PATHS: a Go test file, a file under a testdata directory at any depth, one of the TLA+ ledgers, the docs catalog, or an AGENTS.md map. p is a path relative to the repository root.

func ClassTestName

func ClassTestName(class string) string

ClassTestName is the internal/ci test a class with no test of its own is pinned by: TestTheTreePasses then the class in upper camel case (gofmt: TestTheTreePassesGofmt).

func Deadline

func Deadline(tier string) int

Deadline is the minutes a tier gets when the header names none.

func DuplicateID

func DuplicateID(cards []Card) string

DuplicateID is the first id two cards of a plan share, "" when every id is its own: a card is written as <id>.md, so a second card of one id would overwrite the first while the manifest listed both. The ledger and findings planners make ids unique (uniqueID); a tool named twice to --from help does not.

func ExampleTest

func ExampleTest(texts ...string) string

ExampleTest is the test that holds a tool's help examples to the binary, read off the texts of its package's _test.go files: HelpExampleTest when a file declares it, else the first test function whose body calls onboarding.ExampleLines, else "".

func LedgerNames

func LedgerNames() []string

LedgerNames is every ledger name, sorted, for the help and a refusal.

func Manifest

func Manifest(p Plan) string

Manifest is the manifest.tsv of a directory: id, file, test, wave, deps, one card per line after the header.

func MergePaths

func MergePaths(paths []string) []string

MergePaths drops duplicates and an entry a sibling glob already covers, then, past MaxPaths, folds files into their directory's glob until the list fits.

func NewTestFile

func NewTestFile(c *Card, exists func(glob string) bool)

NewTestFile marks the test file a card creates: when its package's *_test.go glob names nothing at the base (exists reports it), the package has no test yet and the card writes <dir>/<file>_test.go, named on its NEW: line. The glob stays in PATHS: the land holds the diff to PATHS (internal/diffcheck), and the new file matches it.

func OKLine

func OKLine(dir string, p Plan) string

OKLine is the one line a generation prints.

func Render

func Render(h Header, c Card) string

Render writes one brief: the header lines nova-sprint add reads, the paragraph every card of the night carried, the rules verbatim from the card template, the ATTRIBUTION line, the task, the steps, and the AS A READ section a reader is given (AsARead, which carries AlwaysInPathsRule). It is the card template's shape with the <...> filled, so it passes the add's lint and nova-swarm lint --card --child-rules by construction.

func Slug

func Slug(file string) string

Slug is a file's name as an id segment: lower case, runs of other characters as one hyphen, a trailing _test.go or .go dropped.

Types

type Card

type Card struct {
	ID    string
	File  string
	Rows  []Row
	Paths []string
	Test  string
	Tier  string
	Wave  int
	Deps  []string
	Task  string
	// Kind is the KIND: line: swarm.LedgerKind for a card cut from a ledger, fix-red for
	// every other generated card.
	Kind string
	// Ledger, Count and Counted are a ledger card's: the ledger file, what its entry for
	// File reads at the base (the number of rows for File, or, Counted, the count a
	// `pkg N` row carries), so the STOP line says the entry shrinks from Count to 0 with
	// the class test green.
	Ledger  string
	Count   int
	Counted bool
	// New is the NEW: line, the files the card creates (none for most): a test file
	// in a package that has none yet (NewTestFile).
	New []string
}

Card is one planned brief before it is rendered: its id, the rows it covers, the paths it may touch, its test, tier, wave and dependencies (card ids).

func PlanClassRed

func PlanClassRed(r ClassRed, base, tier string) Card

PlanClassRed is the fix-red card the generator stamps for a base red on one class: its id is fix-red-<class>-<base> (one card per class and base, so the lander's judgment and a hand generation name the same card), its PATHS the files the finding names with their package's tests, and its TEST the class test. A class that is a run with no test (gofmt, vet) gets the class test the card writes in internal/ci, ClassTestName: fix-red is a gated kind, and its red and green is a test.

func PlanHelp

func PlanHelp(tool, help, test, prefix, tier string) Card

PlanHelp is one card for a tool from its rendered help: the lines over HelpLineLimit are listed; the terms and the examples are the model's reading, so the card is pro. help is the banner `tool help` printed; a tool with no long line and nothing else to say still gets a card, because the examples and the terms are the judgment the card asks for. test is the test of cmd/<tool> that runs the help's example lines (ExampleTest over the package's test files), and "" when the package has none or no checkout was read: then the card writes HelpExampleTest first and its gate is the package.

type ClassRed

type ClassRed struct {
	Class   string
	Run     string
	Test    string
	Files   []string
	Finding string
}

ClassRed is a base red on one class of the tree's class suite, as the lander's base gate found it (internal/sprint land_class.go; docs/SPEC-SPRINT.md section 7, the base's class gate): the class, the run that went red, the test that pins it ("pkg TestX", "" when the class is a run with no test, gofmt and vet), the files the run's output names, and the finding on one line.

type Finding

type Finding struct {
	File   string
	Line   string
	What   string
	Remedy string
	Test   string
	Row    int
}

Finding is one row of a findings TSV: file:line, what was found, the remedy, and the test (`pkg TestName`, or empty for none) that pins the fix.

func ParseFindings

func ParseFindings(text string) (findings []Finding, skipped []string)

ParseFindings reads a findings TSV: four tab-separated columns, file:line, finding, remedy, test; a header row whose first cell is `file` is skipped, as are comments.

type Header struct {
	Repo    string // owner/name
	Base    string // the branch
	Sha     string // the base sha, 40 hex
	Minutes int    // the deadline; 0 takes the tier's default
}

Header is what every brief of one generation shares.

type Ledger

type Ledger struct {
	Name string // the --ledger name
	File string // repository-relative path of the ledger file
	Test string // the TEST: line's package and test name
	Tier string // flash or pro
	// Generated says the ledger is one land regenerates (docs/SPEC-SPRINT.md section
	// 7, the generality family): cards on it need no dependency on one another.
	Generated bool
	// Counted says a row carries a count (`pkg N`, dead_code) rather than naming one
	// site: the card's STOP reads the count, not the number of rows.
	Counted bool
	// Task is the card's task paragraph. {{file}}, {{rows}}, {{ledger}}, {{test}} and
	// {{count}} are substituted; everything else is written as it stands.
	Task string
	// Parse reads one non-comment row. ok is false for a row the ledger does not
	// recognise, which the planner reports and skips.
	Parse func(line string) (r Row, ok bool)
}

Ledger describes one ratchet ledger of internal/ci: where it lives, the class test that holds it, how a row reads, the tier its cards default to, and the task text a card gets with its rows substituted.

type LintFinding

type LintFinding struct {
	ID      string
	Check   string
	Line    int
	Excerpt string
}

LintFinding is one line the generator refuses a brief on.

func Lint

func Lint(id, brief string) []LintFinding

Lint holds one rendered brief to what nova-sprint add holds it to (cmd/nova-sprint verbs.go lintBriefReads): the model lines of line 1, the child rules, the typed header and a tree card's steps; and to the template's own placeholders, which the add does not read but nova-swarm lint --card does. Empty means admitted.

func (LintFinding) String

func (f LintFinding) String() string

type Plan

type Plan struct {
	Cards   []Card
	Waves   int
	Tier    string
	Skipped []string
	// Shared is true when two cards of the plan name one file in their PATHS and
	// neither needs the other: the add needs --allow-shared-paths.
	Shared bool
}

Plan is a directory's worth of cards with what the planner had to leave out.

func PlanFindings

func PlanFindings(findings []Finding, prefix, tier string, max int) Plan

PlanFindings is one card per file of a findings TSV, in first-seen order, every card wave 1 with no dependency (each touches its own file). The TEST is the first finding's test that names one; a card with none says `TEST: none ...` and is a read card is not: a finding is a fix, so the kind stays fix-red and a missing test is the card's first job, said in the task. max keeps the first max cards, 0 is all.

func PlanLedger

func PlanLedger(l Ledger, rows []Row, prefix, tier string, max int) Plan

PlanLedger groups a ledger's rows by file (one card per file, in ledger order), computes each card's PATHS and TEST, and plans one wave with no dependency for every ledger: the lander resolves a ledger conflict as the union of removals, so adjacent deletions of one file no longer conflict (docs/SPEC-CARD-CONTRACT.md section 6, generated cards). Every card shares the ledger's path and none needs another, so Shared tells the add it wants --allow-shared-paths; a generated ledger (docs/SPEC-SPRINT.md section 7) plans the same. prefix opens every id; tier "" takes the ledger's own; max keeps the first max cards (0 is all), cut before the plan is rendered so no kept card needs a cut one.

type Row

type Row struct {
	File string
	Key  string
	Rest string
	Line int
}

Row is one entry of a source: the file (or package directory) the work lives in, the key that names the entry inside that file, and the rest of the row as its source wrote it. Line is the row's line in the source file, 1-based.

func ParseLedger

func ParseLedger(l Ledger, text string) (rows []Row, skipped []string)

ParseLedger reads a ledger file's rows in file order, skipping comments and blank lines. A row the ledger does not recognise is returned in skipped with its line.

Jump to

Keyboard shortcuts

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