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
- Variables
- func AlwaysInPaths(p string) bool
- func ClassTestName(class string) string
- func Deadline(tier string) int
- func DuplicateID(cards []Card) string
- func ExampleTest(texts ...string) string
- func LedgerNames() []string
- func Manifest(p Plan) string
- func MergePaths(paths []string) []string
- func NewTestFile(c *Card, exists func(glob string) bool)
- func OKLine(dir string, p Plan) string
- func Render(h Header, c Card) string
- func Slug(file string) string
- type Card
- type ClassRed
- type Finding
- type Header
- type Ledger
- type LintFinding
- type Plan
- type Row
Constants ¶
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."
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.
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).
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.
const HelpLineLimit = 100
HelpLineLimit is the width a help line keeps under (docs/STANDARD.md).
const MaxPaths = 8
MaxPaths is the PATHS ceiling: entries past it are merged into their directory's glob.
Variables ¶
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 ¶
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 ¶
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 DuplicateID ¶
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 ¶
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 ¶
Manifest is the manifest.tsv of a directory: id, file, test, wave, deps, one card per line after the header.
func MergePaths ¶
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 ¶
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 Render ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
// 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 ¶
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 ¶
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.