From 61af4713f4c091795bbd0f8991bb262903419982 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 20 Sep 2025 13:23:18 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`docs-re?= =?UTF-8?q?lease-pipeline`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @flyingrobots. * https://github.com/flyingrobots/hubless/pull/1#issuecomment-3314952311 The following files were modified: * `cmd/docs-components/main.go` * `cmd/release/main.go` * `internal/docscomponents/generator.go` * `internal/docscomponents/transclusion.go` * `internal/mock/data.go` * `internal/release/releaser.go` * `internal/ui/tui/mock/app.go` * `internal/ui/tui/mock/profile.go` * `internal/ui/tui/mock/styles.go` * `scripts/verify-docs.sh` --- cmd/docs-components/main.go | 14 ++++++ cmd/release/main.go | 11 +++++ internal/docscomponents/generator.go | 64 ++++++++++++++++++++++++- internal/docscomponents/transclusion.go | 9 +++- internal/mock/data.go | 18 +++++-- internal/release/releaser.go | 7 +++ internal/ui/tui/mock/app.go | 9 ++++ internal/ui/tui/mock/profile.go | 3 ++ internal/ui/tui/mock/styles.go | 5 ++ scripts/verify-docs.sh | 1 + 10 files changed, 136 insertions(+), 5 deletions(-) mode change 100755 => 100644 scripts/verify-docs.sh diff --git a/cmd/docs-components/main.go b/cmd/docs-components/main.go index 833f1be..085650a 100644 --- a/cmd/docs-components/main.go +++ b/cmd/docs-components/main.go @@ -21,6 +21,17 @@ func (s *stringSliceFlag) Set(value string) error { return nil } +// main is the CLI entrypoint for generating documentation components and optionally +// rendering documentation templates via the markdown-transclusion tool. +// +// It parses command-line flags to configure repository and output paths, generator +// options (graph direction, clusters, palette and palette file), and transclusion +// settings (binary, base path, and additional args). It constructs a docs +// generator, runs component generation, and—unless -skip-transclusion is set—resolves +// the transclusion binary and arguments (from flags or MARKDOWN_TRANSCLUSION_* env +// vars) and invokes markdown-transclusion to render a set of templates to their +// configured outputs. Any initialization, generation, or rendering error causes the +// program to log a fatal error and exit. func main() { log.SetFlags(0) log.SetPrefix("docs-components: ") @@ -138,6 +149,9 @@ func main() { } } +// parseArgs splits raw into whitespace-separated fields (using strings.Fields) +// and returns a newly allocated slice containing those fields. An empty or +// all-whitespace input yields an empty slice. func parseArgs(raw string) []string { fields := strings.Fields(raw) return append([]string(nil), fields...) diff --git a/cmd/release/main.go b/cmd/release/main.go index 840a4f3..ea9a329 100644 --- a/cmd/release/main.go +++ b/cmd/release/main.go @@ -11,6 +11,17 @@ import ( "github.com/flyingrobots/hubless/internal/release" ) +// main is the entry point for the hubless release CLI. +// +// It parses command-line flags to configure a release and invokes the releaser: +// - repo: repository root (default ".") +// - version: version to tag (required) +// - notes: path to release notes markdown (default "docs/reference/release-notes.md") +// - dry-run: show actions without creating a tag +// - skip-checks: skip fmt/lint/test/docs before tagging +// +// If --version is omitted the program prints a short message, shows usage and exits with code 2. +// Any other initialization or run error is logged and the program exits non‑zero. func main() { log.SetFlags(0) log.SetPrefix("hubless-release: ") diff --git a/internal/docscomponents/generator.go b/internal/docscomponents/generator.go index 7722ca5..f2cb1ab 100644 --- a/internal/docscomponents/generator.go +++ b/internal/docscomponents/generator.go @@ -30,7 +30,12 @@ type Generator struct { palettes map[string]map[string]paletteColor } -// NewGenerator builds a Generator using the provided repository and components directories. +// NewGenerator creates a Generator configured to write component snippets for the repository at +// repoRoot. It resolves and stores absolute paths for repoRoot and componentsDir (defaulting +// componentsDir to repoRoot/docs/components when empty), ensures the components directory exists, +// loads the built-in palettes and merges palettes from options.PaletteFile if provided, and +// normalizes the supplied GeneratorOptions. Returns an initialized Generator or an error if input +// validation, path resolution, directory creation, or palette loading fails. func NewGenerator(repoRoot, componentsDir string, options GeneratorOptions) (*Generator, error) { if repoRoot == "" { return nil, errors.New("repoRoot is required") @@ -770,6 +775,12 @@ func (g *Generator) readTaskRecords(dir string) ([]recordWithPath[taskRecord], e return results, nil } +// dependencyRowsFromArtifacts builds dependency rows for artifacts. +// It examines each artifact record, cleans its Dependencies with cleanValues, +// and for records that have one or more dependencies appends a dependencyRow +// containing the artifact ID, a link formed by joining linkPrefix with the +// basename of the record's sourcePath, and the cleaned dependency list. +// Records without dependencies are omitted from the returned slice. func dependencyRowsFromArtifacts(records []recordWithPath[artifactRecord], linkPrefix string) []dependencyRow { rows := make([]dependencyRow, 0, len(records)) for _, record := range records { @@ -787,6 +798,10 @@ func dependencyRowsFromArtifacts(records []recordWithPath[artifactRecord], linkP return rows } +// dependencyRowsFromTasks builds dependencyRow entries for each task record that has +// one or more dependencies. The returned rows contain the task ID, a link formed by +// joining linkPrefix with the task record's source file base name, and the cleaned +// list of dependency IDs. Records without dependencies are omitted. func dependencyRowsFromTasks(records []recordWithPath[taskRecord], linkPrefix string) []dependencyRow { rows := make([]dependencyRow, 0) for _, record := range records { @@ -804,6 +819,8 @@ func dependencyRowsFromTasks(records []recordWithPath[taskRecord], linkPrefix st return rows } +// countDoneArtifacts returns the number of artifact records whose Status is considered completed. +// It uses isDoneStatus to determine completion (e.g., DONE, COMPLETED, COMPLETE, SHIPPED, case-insensitive). func countDoneArtifacts(records []recordWithPath[artifactRecord]) int { count := 0 for _, record := range records { @@ -814,6 +831,7 @@ func countDoneArtifacts(records []recordWithPath[artifactRecord]) int { return count } +// (e.g., DONE, COMPLETED, COMPLETE, SHIPPED). func countDoneTasks(records []recordWithPath[taskRecord]) int { count := 0 for _, record := range records { @@ -830,6 +848,12 @@ type paletteColor struct { Text string } +// defaultPalettes returns the built-in color palettes used for graph node styling. +// +// The return value is a map from palette name to a map of class names +// ("milestone", "feature", "story", "task") to paletteColor. The function +// returns a deep copy of the internal base definitions so callers can modify +// the result without affecting the originals. func defaultPalettes() map[string]map[string]paletteColor { base := map[string]map[string]paletteColor{ "evergreen": { @@ -869,6 +893,16 @@ type rawPaletteColor struct { Text string `json:"text"` } +// mergePaletteFile reads a JSON palette file at path and merges its palette +// definitions into the provided palettes map. +// +// The file may contain multiple palette objects keyed by name; a top-level +// "$schema" key is ignored. Palette and class names are normalized to lower +// case and trimmed. Each palette class must be one of the known classes +// (milestone, feature, story, task) and must include non-empty `fill`, +// `stroke`, and `text` color values. Missing file is not an error (returns +// nil). On success the palettes map is mutated to include or update the +// palettes from the file; on failure a descriptive error is returned. func mergePaletteFile(path string, palettes map[string]map[string]paletteColor) error { data, err := os.ReadFile(path) if err != nil { @@ -919,6 +953,13 @@ func mergePaletteFile(path string, palettes map[string]map[string]paletteColor) return nil } +// renderProgressBar returns a fixed-width textual progress bar (10 units) and a +// percentage for the given done/total pair. +// +// If total is <= 0 the function returns a zeroed bar "[----------] 0%". The +// progress ratio is clamped to [0,1]; the number of filled units is computed by +// rounding ratio*10 and the percentage is ratio*100 rounded to the nearest +// integer. func renderProgressBar(done, total int) string { const width = 10 if total <= 0 { @@ -942,6 +983,8 @@ func renderProgressBar(done, total int) string { return fmt.Sprintf("[%s%s] %d%%", strings.Repeat("#", filled), strings.Repeat("-", width-filled), percent) } +// isDoneStatus reports whether a status string represents a completed state. +// It returns true for the case-insensitive values "DONE", "COMPLETED", "COMPLETE", or "SHIPPED"; otherwise false. func isDoneStatus(status string) bool { switch strings.ToUpper(strings.TrimSpace(status)) { case "DONE", "COMPLETED", "COMPLETE", "SHIPPED": @@ -951,6 +994,7 @@ func isDoneStatus(status string) bool { } } +// any other or empty status -> 5 func statusRank(status string) int { switch strings.ToUpper(strings.TrimSpace(status)) { case "DONE", "COMPLETED", "COMPLETE", "SHIPPED": @@ -968,6 +1012,9 @@ func statusRank(status string) int { } } +// cleanValues returns a new slice containing the non-empty strings from values +// after trimming surrounding whitespace. The original order is preserved. +// If values is nil or has length zero, cleanValues returns nil. func cleanValues(values []string) []string { if len(values) == 0 { return nil @@ -1053,6 +1100,8 @@ func (g *Generator) writeFile(destPath, contents string) error { return os.WriteFile(destPath, []byte(contents), 0o644) } +// formatList trims and filters empty strings from values and returns them joined by ", ". +// If the resulting list is empty it returns an em dash "—". func formatList(values []string) string { cleaned := cleanValues(values) if len(cleaned) == 0 { @@ -1062,6 +1111,9 @@ func formatList(values []string) string { return strings.Join(cleaned, ", ") } +// escapeMermaidLabel returns label with characters escaped for use in Mermaid diagrams. +// It escapes backslashes, double quotes, and square brackets so the label can be safely +// embedded in Mermaid node definitions. func escapeMermaidLabel(label string) string { replacer := strings.NewReplacer( "\\", "\\\\", @@ -1072,6 +1124,7 @@ func escapeMermaidLabel(label string) string { return replacer.Replace(label) } +// the record's id; records without a corresponding entry in nodeIDs are skipped. func collectNodesByType(typeLabel string, records []typedRecord, nodeIDs map[string]string) []string { nodes := make([]string, 0) for _, record := range records { @@ -1087,6 +1140,10 @@ func collectNodesByType(typeLabel string, records []typedRecord, nodeIDs map[str return nodes } +// normalizeOptions normalizes and validates graph-related fields in a GeneratorOptions value. +// It uppercases and validates GraphDirection (defaults to "LR" if invalid) and lowercases +// GraphPalette (defaults to "evergreen" or to "evergreen" if the named palette is not present +// in the provided palettes map), returning the adjusted options. func normalizeOptions(options GeneratorOptions, palettes map[string]map[string]paletteColor) GeneratorOptions { direction := strings.ToUpper(strings.TrimSpace(options.GraphDirection)) if !isValidDirection(direction) { @@ -1106,6 +1163,8 @@ func normalizeOptions(options GeneratorOptions, palettes map[string]map[string]p return options } +// isValidDirection reports whether the given graph direction is one of the +// supported Mermaid directions: "LR", "RL", "TB", or "BT". func isValidDirection(direction string) bool { switch direction { case "LR", "RL", "TB", "BT": @@ -1117,6 +1176,7 @@ func isValidDirection(direction string) bool { var orderedTypes = []string{"Milestone", "Feature", "Story", "Task"} +// string if the pointer is nil. func derefString(value *string) string { if value == nil { return "" @@ -1124,6 +1184,8 @@ func derefString(value *string) string { return strings.TrimSpace(*value) } +// isKnownPaletteClass reports whether the given class name is a recognized palette +// class. Valid names are "milestone", "feature", "story", and "task". func isKnownPaletteClass(class string) bool { switch class { case "milestone", "feature", "story", "task": diff --git a/internal/docscomponents/transclusion.go b/internal/docscomponents/transclusion.go index 8ce2f6f..3fb273c 100644 --- a/internal/docscomponents/transclusion.go +++ b/internal/docscomponents/transclusion.go @@ -19,7 +19,13 @@ type TransclusionOptions struct { OutputPath string } -// RunTransclusion executes the markdown-transclusion CLI to render a document template. +// RunTransclusion runs the markdown-transclusion CLI to render a document template +// using opts. It validates that opts.InputPath and opts.OutputPath are set, defaults +// opts.Bin to "markdown-transclusion" when empty, and uses opts.BasePath or the +// current working directory as the base. Paths are resolved to absolute values +// relative to the base, the output directory is created if necessary, and the CLI is +// executed with the provided context. On failure the returned error includes the +// CLI's combined output. func RunTransclusion(ctx context.Context, opts TransclusionOptions) error { if opts.InputPath == "" { return errors.New("input path is required") @@ -75,6 +81,7 @@ func RunTransclusion(ctx context.Context, opts TransclusionOptions) error { return nil } +// absolute path fails. func makeAbsoluteWithBase(pathValue, base string) (string, error) { if filepath.IsAbs(pathValue) { return pathValue, nil diff --git a/internal/mock/data.go b/internal/mock/data.go index 6832715..4782fd7 100644 --- a/internal/mock/data.go +++ b/internal/mock/data.go @@ -56,7 +56,9 @@ type StatusSection struct { Counter int } -// MockCatalog returns sample issues for list/detail wireframes. +// MockCatalog returns a slice of sample Issue values used by list/detail wireframes. +// The provided now time is used to compute LastUpdated, Comment.CreatedAt, and TimelineEvent.Timestamp +// so callers can control the generated timestamps. func MockCatalog(now time.Time) []Issue { return []Issue{ { @@ -110,7 +112,13 @@ func MockCatalog(now time.Time) []Issue { } } -// MockStatusSections returns the data driving the home buffer sections. +// MockStatusSections returns sample status sections used by the home buffer view. +// +// It produces four static sections — "Focus", "Inbox", "Boards", and "Saved Filters" — +// each with example items, a short hint for keyboard interaction, and a counter. +// +// The `now` parameter is accepted for API consistency with other mock factories but is +// not used to compute the returned data. func MockStatusSections(now time.Time) []StatusSection { return []StatusSection{ { @@ -140,7 +148,11 @@ func MockStatusSections(now time.Time) []StatusSection { } } -// MockBoard returns fake kanban columns for the kanban view. +// MockBoard returns a deterministic set of sample Kanban columns and cards used by mock UI flows. +// +// The returned slice contains three columns ("Open", "In Progress", "Review") populated with +// BoardCard entries (ID, Title, Assignee, Priority). Intended for development and testing of +// Kanban/board views—not for production data. func MockBoard() []BoardColumn { return []BoardColumn{ { diff --git a/internal/release/releaser.go b/internal/release/releaser.go index 5484f9b..1726035 100644 --- a/internal/release/releaser.go +++ b/internal/release/releaser.go @@ -25,6 +25,10 @@ type Releaser struct { repoRoot string } +// New returns a Releaser for the repository located at repoRoot. +// repoRoot must be a non-empty path; it may be relative and will be resolved +// to an absolute path. Returns an error if repoRoot is empty or the path +// cannot be resolved. func New(repoRoot string) (*Releaser, error) { if repoRoot == "" { return nil, errors.New("repo root is required") @@ -163,6 +167,9 @@ func (r *Releaser) capture(ctx context.Context, name string, args ...string) (st return string(output), err } +// normalizeVersion trims whitespace from v and ensures it begins with a "v". +// If the trimmed version is empty it is returned unchanged; otherwise, a +// leading "v" is added when missing (e.g. "1.2.3" -> "v1.2.3"). func normalizeVersion(version string) string { version = strings.TrimSpace(version) if version == "" { diff --git a/internal/ui/tui/mock/app.go b/internal/ui/tui/mock/app.go index ba6aaf4..94dcf2b 100644 --- a/internal/ui/tui/mock/app.go +++ b/internal/ui/tui/mock/app.go @@ -39,6 +39,15 @@ type AppModel struct { styles Styles } +// NewModel creates an AppModel initialized for a mocked TUI. +// +// The returned model is set to the Status screen, sized to the provided +// width and height, and populated with the given sections, issues, and board +// columns. The layout profile is selected based on width, default styles are +// applied, and selection indices (sectionIndex, issueIndex) start at 0. +// +// This constructor is intended for creating self-contained mock UI state used +// in layout and rendering tests. func NewModel(width, height int, sections []mockStatusSection, issues []mockIssue, board []mockBoardColumn) AppModel { prof := profileForWidth(width) return AppModel{ diff --git a/internal/ui/tui/mock/profile.go b/internal/ui/tui/mock/profile.go index 9b7a35e..3c1ca4c 100644 --- a/internal/ui/tui/mock/profile.go +++ b/internal/ui/tui/mock/profile.go @@ -14,6 +14,9 @@ func (p layoutProfile) Name() string { return p.name } +// profileForWidth returns the responsive layoutProfile for the given width. +// It maps widths < 100 to the "sm" (small) profile, widths >= 100 and < 140 to +// the "md" (medium) profile, and widths >= 140 to the "lg" (large) profile. func profileForWidth(width int) layoutProfile { switch { case width < 100: diff --git a/internal/ui/tui/mock/styles.go b/internal/ui/tui/mock/styles.go index 1c760bd..bf34f18 100644 --- a/internal/ui/tui/mock/styles.go +++ b/internal/ui/tui/mock/styles.go @@ -8,6 +8,11 @@ type Styles struct { Footer lipgloss.Style } +// newStyles constructs and returns a Styles value with the mock TUI's default styling. +// +// The returned Styles sets: +// - Statusline: foreground color #00B3A4 and bold text. +// - Footer: foreground color #6B9F7F. func newStyles() Styles { return Styles{ Statusline: lipgloss.NewStyle().Foreground(lipgloss.Color("#00B3A4")).Bold(true), diff --git a/scripts/verify-docs.sh b/scripts/verify-docs.sh old mode 100755 new mode 100644 index 0c8b850..21dbbb8 --- a/scripts/verify-docs.sh +++ b/scripts/verify-docs.sh @@ -12,6 +12,7 @@ TARGETS=( missing=() failures=() +# contains_placeholder returns 0 if the specified file contains an unresolved placeholder of the form `![[...]]`; otherwise returns 1. contains_placeholder() { local file="$1" if rg -n "!\\[\\[[^]]+\\]\\]" "$file" >/dev/null; then