From 3f921673c753beef09215a08745bbd51d08b54cc Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 2 Mar 2026 16:11:29 -0700 Subject: [PATCH 1/5] bugfix: Fix issue with default seed value. - Add codebase improvement spec docs. --- .gitignore | 3 + cmd/fname/fname.go | 11 +- .../codebase-improvements/.openspec.yaml | 2 + .../changes/codebase-improvements/design.md | 111 ++++++++++++++++++ .../changes/codebase-improvements/proposal.md | 38 ++++++ .../specs/custom-dictionary/spec.md | 27 +++++ .../specs/generator-validation/spec.md | 31 +++++ .../specs/output-formatting/spec.md | 27 +++++ .../specs/seed-handling/spec.md | 27 +++++ .../specs/word-list-quality/spec.md | 23 ++++ .../changes/codebase-improvements/tasks.md | 61 ++++++++++ openspec/config.yaml | 20 ++++ 12 files changed, 375 insertions(+), 6 deletions(-) create mode 100644 openspec/changes/codebase-improvements/.openspec.yaml create mode 100644 openspec/changes/codebase-improvements/design.md create mode 100644 openspec/changes/codebase-improvements/proposal.md create mode 100644 openspec/changes/codebase-improvements/specs/custom-dictionary/spec.md create mode 100644 openspec/changes/codebase-improvements/specs/generator-validation/spec.md create mode 100644 openspec/changes/codebase-improvements/specs/output-formatting/spec.md create mode 100644 openspec/changes/codebase-improvements/specs/seed-handling/spec.md create mode 100644 openspec/changes/codebase-improvements/specs/word-list-quality/spec.md create mode 100644 openspec/changes/codebase-improvements/tasks.md create mode 100644 openspec/config.yaml diff --git a/.gitignore b/.gitignore index 201bc68..f7d976e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,8 @@ **/*.bak .vscode/ +.claude/ +.codex/ dist/ +fname diff --git a/cmd/fname/fname.go b/cmd/fname/fname.go index 06f5a96..3af26d1 100644 --- a/cmd/fname/fname.go +++ b/cmd/fname/fname.go @@ -47,17 +47,16 @@ func main() { delimiter string = "-" help bool ver bool - quantity int = 1 - size uint = 2 - seed int64 = -1 - // TODO: add option to use custom dictionary + quantity int = 1 + size uint = 2 + seed int64 ) pflag.StringVarP(&casing, "casing", "c", casing, "set the casing of the generated name ") pflag.StringVarP(&delimiter, "delimiter", "d", delimiter, "set the delimiter used to join words") pflag.IntVarP(&quantity, "quantity", "q", quantity, "set the number of names to generate") pflag.UintVarP(&size, "size", "z", size, "set the number of words in the generated name (minimum 2, maximum 4)") - pflag.Int64VarP(&seed, "seed", "s", seed, "random generator seed") + pflag.Int64VarP(&seed, "seed", "s", 0, "random generator seed") pflag.BoolVarP(&help, "help", "h", help, "show fname usage") pflag.BoolVarP(&ver, "version", "v", ver, "show fname version") pflag.Parse() @@ -80,7 +79,7 @@ func main() { fname.WithDelimiter(delimiter), } - if seed != -1 { + if pflag.Lookup("seed").Changed { opts = append(opts, fname.WithSeed(seed)) } if size != 2 { diff --git a/openspec/changes/codebase-improvements/.openspec.yaml b/openspec/changes/codebase-improvements/.openspec.yaml new file mode 100644 index 0000000..fd79bfc --- /dev/null +++ b/openspec/changes/codebase-improvements/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-02 diff --git a/openspec/changes/codebase-improvements/design.md b/openspec/changes/codebase-improvements/design.md new file mode 100644 index 0000000..54f07eb --- /dev/null +++ b/openspec/changes/codebase-improvements/design.md @@ -0,0 +1,111 @@ +## Context + +`fname` is a small Go library and CLI for generating human-friendly random name phrases. The codebase is compact (~150 LOC across 3 Go files) but has accumulated several bugs and rough edges. The public library API is used externally (`go get github.com/splode/fname`), so breaking changes require care. + +Current pain points: +- The seed `-1` sentinel makes a valid int64 input silently unusable +- An index-based collision loop guards against a problem that doesn't exist +- Size validation happens too late (at generate time, not construction time) +- The word-list parser uses streaming I/O on in-memory strings +- Verb tenses are inconsistent across the 448-word verb list +- 72 words appear in both adjective and noun lists + +## Goals / Non-Goals + +**Goals:** +- Fix the seed sentinel bug with a clean API that accepts all int64 values +- Remove dead/incorrect logic (collision loop) +- Validate generator options eagerly at construction time +- Improve word list quality (verb tense consistency, cross-list overlap) +- Minor performance improvements (split, casing switch) +- Add `WithDictionary()` to fulfill the existing TODO +- Add `--format` output flag to the CLI + +**Non-Goals:** +- Rewriting the generator architecture +- Changing the default name format or word order +- Expanding the size range beyond 2–4 +- Adding cryptographic randomness + +## Decisions + +### D1: Seed flag uses `*int64` instead of sentinel `-1` + +**Decision**: Change the `seed` variable in `main()` from `int64 = -1` to `*int64` (nil = unset). Pass `fname.WithSeed(*seed)` only when non-nil. + +**Alternatives considered**: +- Separate `--no-seed` bool flag: adds a flag just to undo another flag, confusing +- Use `0` as sentinel: same problem, `0` is a valid seed +- `*int64` nil pointer: idiomatic Go for "optional value", clean, no reserved values + +**Impact**: CLI-only change. The library's `WithSeed(int64)` signature is unchanged. + +### D2: Remove collision-avoidance loop, no replacement + +**Decision**: Delete the `for adjectiveIndex == nounIndex` loop entirely. No replacement needed. + +**Rationale**: The loop compares an index into the adjective list (0..1745) against an index into the noun list (0..2663). Equal integers do not mean equal words — `adjective[5]="absolute"`, `noun[5]="absence"`. The loop solves a phantom problem and could theoretically spin indefinitely on equal-length lists. + +**Alternatives considered**: +- Fix the loop to compare word strings: adds a real-collision check, but two-word names from a 4.6M combination space make same-word collisions negligible (~0.07% for largest overlap category) +- Keep loop as-is: incorrect semantics, wasted cycles + +### D3: Eager size validation in `WithSize` + +**Decision**: Return an error from `WithSize()`, changing its signature to `func WithSize(size uint) (GeneratorOption, error)`. Callers get immediate feedback on invalid sizes. + +**Alternatives considered**: +- Validate in `NewGenerator()` and return `(*Generator, error)`: larger API change, but cleaner; deferred for a future refactor +- Keep deferred validation in `Generate()`: current state, poor library ergonomics +- Panic in `WithSize()`: not idiomatic Go for user input validation + +**Note**: This is a **BREAKING** change to the `WithSize` function signature. + +### D4: Replace `bufio.Scanner` with `strings.Split` + +**Decision**: Replace the `split()` function body with `strings.Split(strings.TrimRight(s, "\n"), "\n")`. The embedded data is already in memory; a streaming scanner adds unnecessary overhead. + +**Rationale**: Simpler, faster, no reader allocation. The only edge case is a trailing newline on the embedded string, which `strings.TrimRight` handles. + +### D5: Replace `casingMap` with a `switch` in `applyCasing` + +**Decision**: Remove `casingMap` and replace the map lookup in `applyCasing` with a direct `switch` on `g.casing`. + +**Rationale**: Three cases don't benefit from a map. A switch is zero-allocation, branch-predictor friendly, and more readable. + +### D6: Document (not fix) goroutine safety + +**Decision**: Add a doc comment to `Generator` stating it is not safe for concurrent use. Creating a new `Generator` per goroutine is the idiomatic solution. + +**Alternatives considered**: +- Add a `sync.Mutex` around rand calls: adds lock contention overhead for the common single-goroutine case +- Switch to `math/rand/v2` global rand: changes minimum Go version requirement and behavior + +### D7: `WithDictionary()` takes a `*Dictionary` + +**Decision**: Add `WithDictionary(d *Dictionary)` as a new `GeneratorOption`. `NewDictionary()` remains the default; callers who want custom words construct their own `Dictionary` and pass it in. + +**Rationale**: Minimal API surface, composable with existing options, fulfills the existing TODO with no breakage. + +### D8: `CasingFromString` → `ParseCasing` + +**Decision**: Rename `CasingFromString` to `ParseCasing`. Keep `CasingFromString` as a deprecated alias for one release cycle. + +**Rationale**: `ParseX` is the idiomatic Go convention for string-to-value parsing (cf. `strconv.ParseInt`, `time.Parse`). + +### D9: `--format` flag with `plain` (default) and `json` + +**Decision**: Add `--format` / `-f` flag accepting `plain` (default, current behavior) or `json`. JSON output is an array of name strings: `["name1","name2"]`. + +**Rationale**: Enables scripting without `xargs` / `sed` gymnastics. An array is more useful than newline-delimited JSON objects for batch generation. + +## Risks / Trade-offs + +- **`WithSize` signature change is breaking** → Mitigated by it being a small, focused library; version bump to communicate the change +- **Verb list edits are manual and subjective** → Mitigated by focusing on clearly wrong tenses (past participles like "abandoned" in a verb slot that reads as present action); a full linguistic audit is out of scope +- **Adj/noun overlap cleanup could remove intentionally dual-use words** → Mitigated by only removing from the list where the word is clearly stronger in one category (e.g., "blue" as adjective, not noun) + +## Open Questions + +- Should `NewGenerator` be changed to return `(*Generator, error)` now, or deferred to a future version? (Current proposal keeps it non-error-returning for minimal breakage) +- Should JSON output for `--format json` include metadata (seed used, size, count)? Or just the name array? diff --git a/openspec/changes/codebase-improvements/proposal.md b/openspec/changes/codebase-improvements/proposal.md new file mode 100644 index 0000000..86223c0 --- /dev/null +++ b/openspec/changes/codebase-improvements/proposal.md @@ -0,0 +1,38 @@ +## Why + +A codebase audit identified a set of bugs, UX rough edges, performance inefficiencies, and library API gaps that have accumulated over time. Addressing them systematically will improve correctness, usability, and the quality of the library as a dependency. The seed sentinel bug, in particular, is a functional defect where a valid input value is silently discarded. + +## What Changes + +- Fix seed sentinel bug: `-1` is currently treated as "no seed provided," making it impossible to use `-1` as a seed value +- Remove false collision-avoidance loop that compares indices across differently-sized arrays (solving a non-existent problem) +- Move size validation to construction time so invalid generators fail eagerly +- Add validation for `--quantity` flag to reject zero or negative values with a clear error +- Replace `bufio.Scanner` with `strings.Split` for in-memory word list parsing +- Replace `casingMap` map lookup with a direct `switch` in `applyCasing` +- Add goroutine safety to `Generator` (or document that it is not safe for concurrent use) +- Normalize verb tense across the verb word list (consistent 3rd-person singular present tense) +- Audit and clean up adjective/noun word overlap (72 words appear in both lists) +- Add `--format` flag to CLI for structured output (e.g., JSON, newline-delimited) +- Implement `WithDictionary()` option to fulfill the existing `TODO` in `dictionary.go` +- Rename `CasingFromString` to `ParseCasing` for idiomatic Go style + +## Capabilities + +### New Capabilities + +- `seed-handling`: Correct, unambiguous seed input — use `*int64` or a separate flag rather than a sentinel value +- `generator-validation`: Eager validation of generator options at construction time, including size and quantity +- `custom-dictionary`: `WithDictionary()` option allowing callers to supply their own word lists +- `output-formatting`: `--format` CLI flag supporting structured output (JSON, plain) +- `word-list-quality`: Normalized verb tenses and cleaned adjective/noun overlap in data files + +### Modified Capabilities + +## Impact + +- `generator.go`: seed logic, collision loop removal, size validation, `applyCasing` switch, concurrency docs +- `cmd/fname/fname.go`: seed flag type change, quantity validation, `--format` flag +- `dictionary.go`: `split()` implementation, `WithDictionary()` option, `ParseCasing` rename +- `data/verb`, `data/adjective`, `data/noun`: word list data file edits +- Library public API: `CasingFromString` → `ParseCasing` is a **BREAKING** rename; `WithDictionary` is additive diff --git a/openspec/changes/codebase-improvements/specs/custom-dictionary/spec.md b/openspec/changes/codebase-improvements/specs/custom-dictionary/spec.md new file mode 100644 index 0000000..2226d51 --- /dev/null +++ b/openspec/changes/codebase-improvements/specs/custom-dictionary/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Generator accepts a custom Dictionary +The `WithDictionary` option SHALL allow a caller to supply a `*Dictionary` instance, replacing the default embedded word lists. + +#### Scenario: Custom adjectives are used in generated names +- **WHEN** a caller provides a Dictionary with a custom adjective list +- **THEN** generated names only use words from that custom adjective list + +#### Scenario: Custom noun list is respected +- **WHEN** a caller provides a Dictionary with a custom noun list +- **THEN** generated names only use words from that custom noun list + +#### Scenario: Nil dictionary falls back to default +- **WHEN** a caller passes `nil` as the Dictionary to `WithDictionary` +- **THEN** the generator uses the default embedded Dictionary + +### Requirement: Dictionary can be constructed with custom word lists +The `NewDictionary` constructor (or an alternative constructor) SHALL accept optional word lists so callers can build a Dictionary without embedding data files. + +#### Scenario: Caller-provided word slices are used +- **WHEN** a caller constructs a Dictionary with custom adjective and noun slices +- **THEN** the Dictionary reports the correct lengths for those word categories + +#### Scenario: Empty word list for unused category is valid +- **WHEN** a caller constructs a 2-word Generator with a Dictionary that has an empty verb list +- **THEN** generation succeeds because verbs are not used for size-2 names diff --git a/openspec/changes/codebase-improvements/specs/generator-validation/spec.md b/openspec/changes/codebase-improvements/specs/generator-validation/spec.md new file mode 100644 index 0000000..0f21f4e --- /dev/null +++ b/openspec/changes/codebase-improvements/specs/generator-validation/spec.md @@ -0,0 +1,31 @@ +## ADDED Requirements + +### Requirement: Size validation occurs at option construction time +`WithSize` SHALL return an error immediately if the provided size is outside the valid range (2–4), so invalid generators cannot be constructed. + +#### Scenario: Invalid size is rejected at construction +- **WHEN** a library caller passes `WithSize(1)` to `NewGenerator` +- **THEN** `WithSize` returns a non-nil error before `NewGenerator` is called + +#### Scenario: Invalid size 5 is rejected at construction +- **WHEN** a library caller passes `WithSize(5)` to `NewGenerator` +- **THEN** `WithSize` returns a non-nil error + +#### Scenario: Valid sizes 2, 3, 4 are accepted +- **WHEN** a library caller passes `WithSize(2)`, `WithSize(3)`, or `WithSize(4)` +- **THEN** `WithSize` returns a nil error and the option applies successfully + +### Requirement: CLI rejects zero or negative quantity +The CLI SHALL return an error and non-zero exit code when `--quantity` is zero or negative, rather than producing no output silently. + +#### Scenario: Quantity zero prints an error +- **WHEN** a user runs `fname --quantity 0` +- **THEN** the CLI prints a descriptive error message and exits with a non-zero status + +#### Scenario: Negative quantity prints an error +- **WHEN** a user runs `fname --quantity -5` +- **THEN** the CLI prints a descriptive error message and exits with a non-zero status + +#### Scenario: Positive quantity works normally +- **WHEN** a user runs `fname --quantity 3` +- **THEN** the CLI prints 3 name phrases and exits with status 0 diff --git a/openspec/changes/codebase-improvements/specs/output-formatting/spec.md b/openspec/changes/codebase-improvements/specs/output-formatting/spec.md new file mode 100644 index 0000000..c9cbd38 --- /dev/null +++ b/openspec/changes/codebase-improvements/specs/output-formatting/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: CLI supports structured JSON output +The CLI SHALL accept a `--format` flag that controls output format. The default value is `plain` (current behavior). When `json` is specified, output SHALL be a JSON array of name strings. + +#### Scenario: Default plain format is unchanged +- **WHEN** a user runs `fname --quantity 3` without `--format` +- **THEN** output is three names, one per line, as before + +#### Scenario: JSON format produces a valid JSON array +- **WHEN** a user runs `fname --format json --quantity 3` +- **THEN** output is a single JSON array, e.g. `["name1","name2","name3"]` + +#### Scenario: JSON format with quantity 1 produces a single-element array +- **WHEN** a user runs `fname --format json` +- **THEN** output is `["name"]` (an array, not a bare string) + +#### Scenario: Invalid format value produces an error +- **WHEN** a user runs `fname --format csv` +- **THEN** the CLI prints a descriptive error and exits with a non-zero status + +### Requirement: Short flag `-f` is the alias for `--format` +The `--format` flag SHALL have `-f` as its short-form alias. + +#### Scenario: Short flag produces same output as long flag +- **WHEN** a user runs `fname -f json` +- **THEN** output is identical to `fname --format json` diff --git a/openspec/changes/codebase-improvements/specs/seed-handling/spec.md b/openspec/changes/codebase-improvements/specs/seed-handling/spec.md new file mode 100644 index 0000000..e4e8156 --- /dev/null +++ b/openspec/changes/codebase-improvements/specs/seed-handling/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: All int64 values are valid seeds +The generator seed option SHALL accept all int64 values, including negative values. No integer value SHALL be treated as a sentinel meaning "no seed." + +#### Scenario: Negative seed produces deterministic output +- **WHEN** a user provides `--seed -1` +- **THEN** the generator uses `-1` as the seed and produces deterministic output + +#### Scenario: Same negative seed produces same names +- **WHEN** two generators are created with the same negative seed value +- **THEN** both generators produce identical name sequences + +#### Scenario: Seed zero is valid +- **WHEN** a user provides `--seed 0` +- **THEN** the generator uses `0` as the seed and produces deterministic output + +### Requirement: Omitting seed produces random output +When no seed is provided, the generator SHALL use a time-based random seed, producing non-deterministic output across invocations. + +#### Scenario: No seed flag means random generation +- **WHEN** a user runs `fname` without `--seed` +- **THEN** repeated invocations produce different name sequences + +#### Scenario: WithSeed option is not applied when seed is absent +- **WHEN** a library caller creates a Generator without `WithSeed` +- **THEN** the generator behaves as if seeded randomly diff --git a/openspec/changes/codebase-improvements/specs/word-list-quality/spec.md b/openspec/changes/codebase-improvements/specs/word-list-quality/spec.md new file mode 100644 index 0000000..77d71a3 --- /dev/null +++ b/openspec/changes/codebase-improvements/specs/word-list-quality/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Verb list uses consistent present-tense forms +All entries in the verb word list SHALL use 3rd-person singular present tense (e.g., "walks", "runs", "discovers"). Past tense, past participle, and bare infinitive forms SHALL be removed or converted. + +#### Scenario: Generated 3-word name uses a present-tense verb +- **WHEN** a user generates a size-3 name phrase multiple times +- **THEN** the verb component consistently reads as a present-tense action (ending in -s or -es for regular verbs) + +#### Scenario: No past-participle verbs appear in output +- **WHEN** a user generates a large batch of size-3 names +- **THEN** no verb component ends in "-ed" in a way that reads as past tense (e.g., "abandoned", "admired" are absent) + +### Requirement: No word appears in both the adjective and noun lists +Words that are dual-category (e.g., "blue", "dark", "cold") SHALL appear in at most one list. For each overlap word, the appropriate category SHALL be chosen based on how it reads in context as part of a generated name. + +#### Scenario: Adjective list contains no noun-list entries +- **WHEN** the adjective and noun data files are compared +- **THEN** there are zero words appearing in both files + +#### Scenario: Name quality is unaffected by overlap removal +- **WHEN** overlap words are removed from one list +- **THEN** the total combination space remains above 4 million for 2-word names diff --git a/openspec/changes/codebase-improvements/tasks.md b/openspec/changes/codebase-improvements/tasks.md new file mode 100644 index 0000000..99f7aac --- /dev/null +++ b/openspec/changes/codebase-improvements/tasks.md @@ -0,0 +1,61 @@ +## 1. Seed Bug Fix + +- [x] 1.1 Change `seed` variable in `main()` from `int64 = -1` to `*int64` (nil pointer) +- [x] 1.2 Update pflag registration to use `pflag.Int64VarP` with a pointer receiver +- [x] 1.3 Replace the `if seed != -1` guard with a nil-pointer check +- [x] 1.4 Verify that `--seed -1`, `--seed 0`, and omitting `--seed` all behave correctly + +## 2. Remove False Collision-Avoidance Loop + +- [ ] 2.1 Delete the `for adjectiveIndex == nounIndex` loop in `generator.go` +- [ ] 2.2 Confirm tests still pass after removal + +## 3. Generator Validation + +- [ ] 3.1 Change `WithSize` signature to return `(GeneratorOption, error)` +- [ ] 3.2 Move the `size < 2 || size > 4` check into `WithSize` and return error there +- [ ] 3.3 Remove the size check from `Generate()` +- [ ] 3.4 Update all `WithSize` call sites (tests, CLI) to handle the returned error +- [ ] 3.5 Add CLI validation for `--quantity`: print error and exit non-zero if ≤ 0 + +## 4. Performance: Word List Parsing + +- [ ] 4.1 Replace `bufio.Scanner` implementation in `split()` with `strings.Split` + `strings.TrimRight` +- [ ] 4.2 Run benchmarks before and after to confirm improvement (optional but recommended) + +## 5. Performance: Casing Switch + +- [ ] 5.1 Remove the `casingMap` package-level variable from `generator.go` +- [ ] 5.2 Rewrite `applyCasing` to use a `switch` on `g.casing` directly +- [ ] 5.3 Verify casing tests still pass + +## 6. Concurrency Documentation + +- [ ] 6.1 Add a doc comment to `Generator` stating it is not safe for concurrent use from multiple goroutines + +## 7. Custom Dictionary + +- [ ] 7.1 Add a `WithDictionary(d *Dictionary) GeneratorOption` function to `generator.go` +- [ ] 7.2 Remove the `TODO: allow for custom dictionary` comment from `dictionary.go` +- [ ] 7.3 Add tests for `WithDictionary` with a custom word list +- [ ] 7.4 Document `WithDictionary` usage in the README library section + +## 8. Output Formatting + +- [ ] 8.1 Add `--format` / `-f` flag to CLI accepting `plain` (default) and `json` +- [ ] 8.2 Implement JSON output: marshal collected names as a `[]string` JSON array +- [ ] 8.3 Add validation: unrecognized format values print error and exit non-zero +- [ ] 8.4 Add README examples for `--format json` + +## 9. API Rename: CasingFromString → ParseCasing + +- [ ] 9.1 Add `ParseCasing` as the canonical function (same body as `CasingFromString`) +- [ ] 9.2 Mark `CasingFromString` as deprecated with a doc comment pointing to `ParseCasing` +- [ ] 9.3 Update internal call site in `cmd/fname/fname.go` to use `ParseCasing` + +## 10. Word List Quality + +- [ ] 10.1 Audit verb list and convert past-tense / past-participle entries to 3rd-person present tense +- [ ] 10.2 Run `task data:dupe` and `task data:spellcheck` after verb edits to verify no duplicates or typos +- [ ] 10.3 Identify the 72 adjective/noun overlap words and remove each from the less-appropriate list +- [ ] 10.4 Re-run combination count to confirm 2-word space stays above 4 million diff --git a/openspec/config.yaml b/openspec/config.yaml new file mode 100644 index 0000000..392946c --- /dev/null +++ b/openspec/config.yaml @@ -0,0 +1,20 @@ +schema: spec-driven + +# Project context (optional) +# This is shown to AI when creating artifacts. +# Add your tech stack, conventions, style guides, domain knowledge, etc. +# Example: +# context: | +# Tech stack: TypeScript, React, Node.js +# We use conventional commits +# Domain: e-commerce platform + +# Per-artifact rules (optional) +# Add custom rules for specific artifacts. +# Example: +# rules: +# proposal: +# - Keep proposals under 500 words +# - Always include a "Non-goals" section +# tasks: +# - Break tasks into chunks of max 2 hours From d94de943feed94e962698f77072e01b9429e85bf Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 2 Mar 2026 16:34:20 -0700 Subject: [PATCH 2/5] feat: Generate names as a JSON array. - Support custom dictionary. - Various code cleanup. --- README.md | 41 +++++++++++- cmd/fname/fname.go | 31 ++++++++- dictionary.go | 32 +++++++--- generator.go | 56 ++++++++++------ generator_test.go | 64 +++++++++++++++++-- .../changes/codebase-improvements/tasks.md | 48 +++++++------- 6 files changed, 206 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index b581f39..9c1869b 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,16 @@ pleasant-joy eligible-tenant ``` +Generate names as a JSON array (useful for scripting): + +```sh +$ fname --format json --quantity 3 +["influential-length","direct-ear","cultural-storage"] + +$ fname -f json +["extinct-green"] +``` + ### Library #### Install @@ -147,13 +157,42 @@ import ( ) func main() { - rng := fname.NewGenerator(fname.WithDelimiter("__"), fname.WithSize(3)) + sizeOpt, err := fname.WithSize(3) + if err != nil { + panic(err) + } + rng := fname.NewGenerator(fname.WithDelimiter("__"), sizeOpt) phrase, err := rng.Generate() fmt.Println(phrase) // => "established__shark__destroyed" } ``` +#### Custom Dictionary + +```go +package main + +import ( + "fmt" + + "github.com/splode/fname" +) + +func main() { + dict := fname.NewCustomDictionary( + []string{"blazing", "frozen"}, // adjectives + nil, // adverbs (uses default) + []string{"comet", "nebula"}, // nouns + nil, // verbs (uses default) + ) + rng := fname.NewGenerator(fname.WithDictionary(dict)) + phrase, err := rng.Generate() + fmt.Println(phrase) + // => "blazing-nebula" +} +``` + ## Disclaimers fname is not cryptographically secure, and should not be used for anything that requires a truly unique identifier. It is meant to be a fun, human-friendly alternative to UUIDs. diff --git a/cmd/fname/fname.go b/cmd/fname/fname.go index 3af26d1..adfa1f2 100644 --- a/cmd/fname/fname.go +++ b/cmd/fname/fname.go @@ -2,6 +2,7 @@ package main import ( _ "embed" + "encoding/json" "fmt" "log" "os" @@ -45,6 +46,7 @@ func main() { var ( casing string = "lower" delimiter string = "-" + format string = "plain" help bool ver bool quantity int = 1 @@ -54,6 +56,7 @@ func main() { pflag.StringVarP(&casing, "casing", "c", casing, "set the casing of the generated name ") pflag.StringVarP(&delimiter, "delimiter", "d", delimiter, "set the delimiter used to join words") + pflag.StringVarP(&format, "format", "f", format, "set the output format ") pflag.IntVarP(&quantity, "quantity", "q", quantity, "set the number of names to generate") pflag.UintVarP(&size, "size", "z", size, "set the number of words in the generated name (minimum 2, maximum 4)") pflag.Int64VarP(&seed, "seed", "s", 0, "random generator seed") @@ -71,7 +74,15 @@ func main() { os.Exit(0) } - c, err := fname.CasingFromString(casing) + if quantity <= 0 { + log.Fatalf("error: quantity must be greater than 0, got %d", quantity) + } + + if format != "plain" && format != "json" { + log.Fatalf("error: invalid format %q, must be plain or json", format) + } + + c, err := fname.ParseCasing(casing) handleError(err) opts := []fname.GeneratorOption{ @@ -83,15 +94,29 @@ func main() { opts = append(opts, fname.WithSeed(seed)) } if size != 2 { - opts = append(opts, fname.WithSize(size)) + sizeOpt, err := fname.WithSize(size) + handleError(err) + opts = append(opts, sizeOpt) } rng := fname.NewGenerator(opts...) + names := make([]string, 0, quantity) for i := 0; i < quantity; i++ { name, err := rng.Generate() handleError(err) - fmt.Println(name) + names = append(names, name) + } + + switch format { + case "json": + out, err := json.Marshal(names) + handleError(err) + fmt.Println(string(out)) + default: + for _, name := range names { + fmt.Println(name) + } } } diff --git a/dictionary.go b/dictionary.go index f9fb3b8..d3e8d26 100644 --- a/dictionary.go +++ b/dictionary.go @@ -2,7 +2,6 @@ package fname import ( - "bufio" _ "embed" "strings" ) @@ -31,9 +30,9 @@ type Dictionary struct { verbs []string } -// NewDictionary creates a new dictionary. +// NewDictionary creates a new Dictionary backed by the default embedded word lists. +// To use custom word lists, use NewCustomDictionary and pass it via WithDictionary. func NewDictionary() *Dictionary { - // TODO: allow for custom dictionary return &Dictionary{ adjectives: adjective, adverbs: adverb, @@ -42,6 +41,25 @@ func NewDictionary() *Dictionary { } } +// NewCustomDictionary creates a Dictionary with caller-supplied word lists. +// Any nil slice falls back to the corresponding default embedded word list. +func NewCustomDictionary(adjectives, adverbs, nouns, verbs []string) *Dictionary { + d := NewDictionary() + if adjectives != nil { + d.adjectives = adjectives + } + if adverbs != nil { + d.adverbs = adverbs + } + if nouns != nil { + d.nouns = nouns + } + if verbs != nil { + d.verbs = verbs + } + return d +} + // LengthAdjective returns the number of adjectives in the dictionary. func (d *Dictionary) LengthAdjective() int { return len(d.adjectives) @@ -63,11 +81,5 @@ func (d *Dictionary) LengthVerb() int { } func split(s string) []string { - scanner := bufio.NewScanner(strings.NewReader(s)) - scanner.Split(bufio.ScanLines) - var lines []string - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - return lines + return strings.Split(strings.TrimRight(s, "\n"), "\n") } diff --git a/generator.go b/generator.go index 7d51f26..cf7d612 100644 --- a/generator.go +++ b/generator.go @@ -31,7 +31,8 @@ func (c Casing) String() string { } } -func CasingFromString(casing string) (Casing, error) { +// ParseCasing parses a casing string and returns the corresponding Casing value. +func ParseCasing(casing string) (Casing, error) { switch strings.ToLower(casing) { case Lower.String(): return Lower, nil @@ -44,6 +45,13 @@ func CasingFromString(casing string) (Casing, error) { } } +// Deprecated: Use ParseCasing instead. +func CasingFromString(casing string) (Casing, error) { + return ParseCasing(casing) +} + +// Generator generates random name phrases. It is not safe for concurrent use +// from multiple goroutines; create a separate Generator per goroutine instead. type Generator struct { casing Casing dict *Dictionary @@ -69,6 +77,16 @@ func WithDelimiter(delimiter string) GeneratorOption { } } +// WithDictionary sets a custom Dictionary on the Generator. +// If d is nil, the default embedded Dictionary is used. +func WithDictionary(d *Dictionary) GeneratorOption { + return func(g *Generator) { + if d != nil { + g.dict = d + } + } +} + // WithSeed sets the seed used to generate random numbers. func WithSeed(seed int64) GeneratorOption { return func(g *Generator) { @@ -77,10 +95,14 @@ func WithSeed(seed int64) GeneratorOption { } // WithSize sets the number of words in the generated name. -func WithSize(size uint) GeneratorOption { +// Returns an error if size is outside the valid range [2, 4]. +func WithSize(size uint) (GeneratorOption, error) { + if size < 2 || size > 4 { + return nil, fmt.Errorf("invalid size: %d", size) + } return func(g *Generator) { g.size = size - } + }, nil } // NewGenerator creates a new Generator. @@ -100,16 +122,9 @@ func NewGenerator(opts ...GeneratorOption) *Generator { // Generate generates a random name. func (g *Generator) Generate() (string, error) { - if g.size < 2 || g.size > 4 { - return "", fmt.Errorf("invalid size: %d", g.size) - } - words := make([]string, 0, g.size) adjectiveIndex := g.rand.Intn(g.dict.LengthAdjective()) nounIndex := g.rand.Intn(g.dict.LengthNoun()) - for adjectiveIndex == nounIndex { - nounIndex = g.rand.Intn(g.dict.LengthNoun()) - } words = append(words, g.dict.adjectives[adjectiveIndex], g.dict.nouns[nounIndex]) @@ -124,19 +139,18 @@ func (g *Generator) Generate() (string, error) { return strings.Join(g.applyCasing(words), g.delimiter), nil } +var titleCaser = cases.Title(language.English) + func (g *Generator) applyCasing(words []string) []string { - if fn, ok := casingMap[g.casing]; ok { - for i, word := range words { - words[i] = fn(word) + for i, word := range words { + switch g.casing { + case Lower: + words[i] = strings.ToLower(word) + case Upper: + words[i] = strings.ToUpper(word) + case Title: + words[i] = titleCaser.String(word) } } return words } - -var titleCaser = cases.Title(language.English) - -var casingMap = map[Casing]func(string) string{ - Lower: strings.ToLower, - Upper: strings.ToUpper, - Title: titleCaser.String, -} diff --git a/generator_test.go b/generator_test.go index 406e9e1..165e0bf 100644 --- a/generator_test.go +++ b/generator_test.go @@ -24,7 +24,11 @@ func TestNewGenerator(t *testing.T) { t.Log("\tWhen creating a new Generator with custom values") { - g := NewGenerator(WithCasing(Title), WithDelimiter("_"), WithSize(3), WithSeed(12345)) + sizeOpt, err := WithSize(3) + if err != nil { + t.Fatal("\t\tShould be able to create a size option without error.") + } + g := NewGenerator(WithCasing(Title), WithDelimiter("_"), sizeOpt, WithSeed(12345)) if g == nil { t.Fatal("\t\tShould be able to create a Generator instance.") } @@ -114,7 +118,11 @@ func TestGenerate(t *testing.T) { t.Log("\tWhen generating a phrase with a custom size") { - g3 := NewGenerator(WithSize(3)) + size3Opt, err := WithSize(3) + if err != nil { + t.Fatal("\t\tShould be able to create a size-3 option without error.") + } + g3 := NewGenerator(size3Opt) phrase, err := g3.Generate() if err != nil { t.Fatal("\t\tShould be able to generate a phrase without error.") @@ -132,7 +140,11 @@ func TestGenerate(t *testing.T) { } t.Log("\t\tShould be able to generate a phrase with 3 parts.") - g4 := NewGenerator(WithSize(4)) + size4Opt, err := WithSize(4) + if err != nil { + t.Fatal("\t\tShould be able to create a size-4 option without error.") + } + g4 := NewGenerator(size4Opt) phrase, err = g4.Generate() if err != nil { t.Fatal("\t\tShould be able to generate a phrase without error.") @@ -175,12 +187,50 @@ func TestGenerate(t *testing.T) { t.Log("\tWhen generating a phrase with an invalid size") { - g := NewGenerator(WithSize(1)) - _, err := g.Generate() + _, err := WithSize(1) if err == nil { - t.Fatal("\t\tShould not be able to generate a phrase with an invalid size.") + t.Fatal("\t\tShould not be able to create a size option with an invalid size.") + } + t.Log("\t\tShould not be able to create a size option with an invalid size.") + } + } +} + +func TestWithDictionary(t *testing.T) { + t.Log("Given the need to test the WithDictionary option") + { + t.Log("\tWhen generating with a custom dictionary") + { + custom := NewCustomDictionary( + []string{"fast"}, + nil, + []string{"rocket"}, + nil, + ) + g := NewGenerator(WithDictionary(custom), WithSeed(1)) + phrase, err := g.Generate() + if err != nil { + t.Fatal("\t\tShould be able to generate a phrase without error.") + } + t.Log("\t\tShould be able to generate a phrase without error.") + + if phrase != "fast-rocket" { + t.Fatalf("\t\tShould generate phrase from custom words, got: %s", phrase) + } + t.Log("\t\tShould generate phrase from custom words.") + } + + t.Log("\tWhen passing nil dictionary") + { + g := NewGenerator(WithDictionary(nil)) + phrase, err := g.Generate() + if err != nil { + t.Fatal("\t\tShould fall back to default dictionary without error.") + } + if len(phrase) == 0 { + t.Fatal("\t\tShould produce a non-empty phrase with default dictionary.") } - t.Log("\t\tShould not be able to generate a phrase with an invalid size.") + t.Log("\t\tShould fall back to default dictionary.") } } } diff --git a/openspec/changes/codebase-improvements/tasks.md b/openspec/changes/codebase-improvements/tasks.md index 99f7aac..3412ba8 100644 --- a/openspec/changes/codebase-improvements/tasks.md +++ b/openspec/changes/codebase-improvements/tasks.md @@ -7,51 +7,51 @@ ## 2. Remove False Collision-Avoidance Loop -- [ ] 2.1 Delete the `for adjectiveIndex == nounIndex` loop in `generator.go` -- [ ] 2.2 Confirm tests still pass after removal +- [x] 2.1 Delete the `for adjectiveIndex == nounIndex` loop in `generator.go` +- [x] 2.2 Confirm tests still pass after removal ## 3. Generator Validation -- [ ] 3.1 Change `WithSize` signature to return `(GeneratorOption, error)` -- [ ] 3.2 Move the `size < 2 || size > 4` check into `WithSize` and return error there -- [ ] 3.3 Remove the size check from `Generate()` -- [ ] 3.4 Update all `WithSize` call sites (tests, CLI) to handle the returned error -- [ ] 3.5 Add CLI validation for `--quantity`: print error and exit non-zero if ≤ 0 +- [x] 3.1 Change `WithSize` signature to return `(GeneratorOption, error)` +- [x] 3.2 Move the `size < 2 || size > 4` check into `WithSize` and return error there +- [x] 3.3 Remove the size check from `Generate()` +- [x] 3.4 Update all `WithSize` call sites (tests, CLI) to handle the returned error +- [x] 3.5 Add CLI validation for `--quantity`: print error and exit non-zero if ≤ 0 ## 4. Performance: Word List Parsing -- [ ] 4.1 Replace `bufio.Scanner` implementation in `split()` with `strings.Split` + `strings.TrimRight` -- [ ] 4.2 Run benchmarks before and after to confirm improvement (optional but recommended) +- [x] 4.1 Replace `bufio.Scanner` implementation in `split()` with `strings.Split` + `strings.TrimRight` +- [x] 4.2 Run benchmarks before and after to confirm improvement (optional but recommended) ## 5. Performance: Casing Switch -- [ ] 5.1 Remove the `casingMap` package-level variable from `generator.go` -- [ ] 5.2 Rewrite `applyCasing` to use a `switch` on `g.casing` directly -- [ ] 5.3 Verify casing tests still pass +- [x] 5.1 Remove the `casingMap` package-level variable from `generator.go` +- [x] 5.2 Rewrite `applyCasing` to use a `switch` on `g.casing` directly +- [x] 5.3 Verify casing tests still pass ## 6. Concurrency Documentation -- [ ] 6.1 Add a doc comment to `Generator` stating it is not safe for concurrent use from multiple goroutines +- [x] 6.1 Add a doc comment to `Generator` stating it is not safe for concurrent use from multiple goroutines ## 7. Custom Dictionary -- [ ] 7.1 Add a `WithDictionary(d *Dictionary) GeneratorOption` function to `generator.go` -- [ ] 7.2 Remove the `TODO: allow for custom dictionary` comment from `dictionary.go` -- [ ] 7.3 Add tests for `WithDictionary` with a custom word list -- [ ] 7.4 Document `WithDictionary` usage in the README library section +- [x] 7.1 Add a `WithDictionary(d *Dictionary) GeneratorOption` function to `generator.go` +- [x] 7.2 Remove the `TODO: allow for custom dictionary` comment from `dictionary.go` +- [x] 7.3 Add tests for `WithDictionary` with a custom word list +- [x] 7.4 Document `WithDictionary` usage in the README library section ## 8. Output Formatting -- [ ] 8.1 Add `--format` / `-f` flag to CLI accepting `plain` (default) and `json` -- [ ] 8.2 Implement JSON output: marshal collected names as a `[]string` JSON array -- [ ] 8.3 Add validation: unrecognized format values print error and exit non-zero -- [ ] 8.4 Add README examples for `--format json` +- [x] 8.1 Add `--format` / `-f` flag to CLI accepting `plain` (default) and `json` +- [x] 8.2 Implement JSON output: marshal collected names as a `[]string` JSON array +- [x] 8.3 Add validation: unrecognized format values print error and exit non-zero +- [x] 8.4 Add README examples for `--format json` ## 9. API Rename: CasingFromString → ParseCasing -- [ ] 9.1 Add `ParseCasing` as the canonical function (same body as `CasingFromString`) -- [ ] 9.2 Mark `CasingFromString` as deprecated with a doc comment pointing to `ParseCasing` -- [ ] 9.3 Update internal call site in `cmd/fname/fname.go` to use `ParseCasing` +- [x] 9.1 Add `ParseCasing` as the canonical function (same body as `CasingFromString`) +- [x] 9.2 Mark `CasingFromString` as deprecated with a doc comment pointing to `ParseCasing` +- [x] 9.3 Update internal call site in `cmd/fname/fname.go` to use `ParseCasing` ## 10. Word List Quality From 0d5cd70904914169800f8ba9057b869a4ec3bb70 Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 2 Mar 2026 17:52:21 -0700 Subject: [PATCH 3/5] feat: Enhance the dictionary quality. --- data/adjective | 21 -- data/noun | 51 --- data/verb | 309 ++++++++---------- .../changes/codebase-improvements/tasks.md | 8 +- 4 files changed, 144 insertions(+), 245 deletions(-) diff --git a/data/adjective b/data/adjective index 458c75e..0a74e25 100644 --- a/data/adjective +++ b/data/adjective @@ -189,7 +189,6 @@ certified cerulean challenging changeable -characteristic charged charismatic charmed @@ -198,10 +197,7 @@ chaste cheap cheerful cheery -chemical -cherry chic -chief childish chilled chilling @@ -216,7 +212,6 @@ chubby circular civic civil -civilian civilized clammy clandestine @@ -280,7 +275,6 @@ constant constitutional constructive contemplative -content continental continuous contradictory @@ -290,7 +284,6 @@ conventional convivial cool cooperative -copper corporate correct cosmic @@ -424,7 +417,6 @@ elliptical eloquent elusive elysian -emerald emotional emphatic empirical @@ -645,7 +637,6 @@ glowing glum glutinous gnomish -gold golden good gooey @@ -817,7 +808,6 @@ jealous jingoistic jittery jocular -joint jolly jolting jovial @@ -872,7 +862,6 @@ likely limited linear lined -linen linguistic liquid literal @@ -958,7 +947,6 @@ milky mindful minimum minor -mint minty miraculous mirthful @@ -1104,7 +1092,6 @@ passive past pastel pastoral -patient patriotic peaceful pearly @@ -1153,8 +1140,6 @@ pithy placid plain planetary -plastic -platinum plausible pleasant plentiful @@ -1219,7 +1204,6 @@ pyrrhic quaint qualified qualitative -quality quantitative quantum quick @@ -1269,11 +1253,9 @@ relevant reliable religious repetitious -representative reptilian requisite reserved -resident residential resilient resolute @@ -1308,7 +1290,6 @@ rotten rough round routine -ruby ruddy runic rural @@ -1380,7 +1361,6 @@ shy sick significant silly -silver similar simplistic sincere @@ -1704,7 +1684,6 @@ weary weekly welcome welcoming -well wet whimsical whiskered diff --git a/data/noun b/data/noun index 9dc13c6..415d0c1 100644 --- a/data/noun +++ b/data/noun @@ -225,7 +225,6 @@ block blood bloodshed blow -blue blueberry boa boar @@ -437,7 +436,6 @@ coffee coffin coin coincidence -cold collar colleague collection @@ -482,7 +480,6 @@ concern concert concession conclusion -concrete condition conductor conference @@ -589,7 +586,6 @@ culture cup cupboard currency -current curriculum curtain curve @@ -606,7 +602,6 @@ dairy damage danger darby -dark data date daughter @@ -848,7 +843,6 @@ exposure expression extension extent -extraterrestrial extreme eye eyebrow @@ -859,7 +853,6 @@ fact factor factory failure -fair fairy faith fall @@ -875,7 +868,6 @@ fashion father fault favor -favorite fax fear feast @@ -890,7 +882,6 @@ fence ferry festival fever -few fiber fiction field @@ -900,7 +891,6 @@ figure file film filter -final finance finger fire @@ -917,7 +907,6 @@ flag flame flange flash -flat flavor fleet flesh @@ -929,7 +918,6 @@ flour flower flu fluctuation -fluid fly foam fog @@ -1001,7 +989,6 @@ gear gecko gem gene -general generation genius gentleman @@ -1034,8 +1021,6 @@ goat goblin gold golf -good -gossamer government governor gown @@ -1051,10 +1036,8 @@ graph graphics grass grasshopper -grave gravel gravity -green greeting grief grimace @@ -1324,7 +1307,6 @@ lease leather leave lecture -left leftovers leg legend @@ -1344,7 +1326,6 @@ lid lie life lift -light lightning lily limb @@ -1355,7 +1336,6 @@ linen link lion lip -liquid list literacy literature @@ -1401,7 +1381,6 @@ management manager manner mantis -manual manufacture manufacturer manuscript @@ -1425,7 +1404,6 @@ material mathematics matrix matter -maximum mayor maze meadow @@ -1467,7 +1445,6 @@ mind mine miner mineral -minimum minister ministry minority @@ -1550,7 +1527,6 @@ negligence negotiation neighbor neighborhood -neon nephew nerve nest @@ -1601,7 +1577,6 @@ offense offer office officer -official offspring ogre oil @@ -1616,7 +1591,6 @@ opposite opposition optimism option -orange orbit orchestra order @@ -1631,7 +1605,6 @@ outlet outline outlook output -outside oven overall overview @@ -1659,7 +1632,6 @@ paper parade paradox paragraph -parallel parameter pardon parent @@ -1678,8 +1650,6 @@ passenger passion passport password -past -pastel pasture patch patent @@ -1809,7 +1779,6 @@ preoccupation preparation prescription presence -present presentation preservation presidency @@ -1821,7 +1790,6 @@ prevalence prey price pride -primary prince princess principle @@ -1842,7 +1810,6 @@ producer product production profession -professional professor profile profit @@ -1870,7 +1837,6 @@ provision psychologist psychology pub -public publication publicity publisher @@ -1883,7 +1849,6 @@ purpose pursuit puzzle pyramid -pyrrhic python quail qualification @@ -1907,7 +1872,6 @@ rabbit rack radiance radiation -radical radio radiologist radiology @@ -1944,7 +1908,6 @@ record recording recovery recreation -red reduction redundancy referee @@ -2020,7 +1983,6 @@ ribbon rice rider ridge -right ring riot rise @@ -2041,9 +2003,7 @@ rope rose rotation rotor -round route -routine row royalty rubbish @@ -2110,7 +2070,6 @@ selection self seller seminar -senior sensation sense sensitivity @@ -2146,7 +2105,6 @@ shock shoe shop shopping -short shortage shorts shot @@ -2218,7 +2176,6 @@ solo solution soprano soul -sound soup source south @@ -2252,9 +2209,7 @@ spring sprite spy squad -square squid -stable staff stage staircase @@ -2262,7 +2217,6 @@ stake stall stamp stand -standard star starfish start @@ -2343,7 +2297,6 @@ swagger swamp sweat sweater -sweet swing switch sword @@ -2386,7 +2339,6 @@ tennis tension tent term -terminal terrace test text @@ -2514,7 +2466,6 @@ uncertainty uncle understanding unemployment -uniform union unit unity @@ -2532,7 +2483,6 @@ value vampire van vanity -variable variant variation variety @@ -2606,7 +2556,6 @@ weed week weekend weight -welcome welfare well west diff --git a/data/verb b/data/verb index ea59f95..41e5ce4 100644 --- a/data/verb +++ b/data/verb @@ -1,72 +1,64 @@ -abandoned abandons abides abuses -accepted +accepts accuses -achieved -acted -admired -admonished +achieves +acts +admires admonishes -advised advises -affected -agreed +affects agrees allows -amazed -amused -answered +amazes +amuses answers -appeared appears applies appoints argues arises -arranged -arrived +arranges arrives -asked -attacked +asks +attacks attends awakes bails -baked +bakes bears beats becomes begins -behaved -believed -belonged +behaves +believes +belongs bids binds bites -blamed +blames bleeds blows -borrowed -bothered +borrows +bothers breaks breeds -breezed +breezes brings burns calcifies -called -canceled -carried +calls +cancels +carries catches -caused -celebrated +causes +celebrates cheats chooses -cleaned -cleared +cleans +clears cleaves -climbed climbs clings comes @@ -79,22 +71,20 @@ cries crosses dares deals -decreased +decreases defeats -destroyed +destroys devotes -died dies digs directs -disagreed -discovered -discussed -disturbed +disagrees +discovers +discusses +disturbs draws dreams -dressed -dried +dresses dries drinks drives @@ -111,20 +101,19 @@ edits effects ejects elects -eliminated -ended -enjoyed +eliminates +ends +enjoys enters -entertained +entertains escapes evades -excused -exercised -exhausted +excuses +exercises exhausts -exhibited -expected -expressed +exhibits +expects +expresses faces fails fakes @@ -133,19 +122,19 @@ fears feeds feels fights -filled -filmed +fills +films finds fines -fished -fixed +fishes +fixes flees flies floats folds -followed +follows forbids -fried +fries gags gains gambles @@ -158,42 +147,39 @@ gives glances goes grants -greeted +greets grinds grips grows -guessed +guesses hacks -hailed -handled +hails +handles hangs -happened +happens harasses has hatches -hated hates hears -helped helps hews hides hits holds -hoped -hunted +hopes hunts hurts -identified -ignored -imagined -impressed -improved -included -increased -interviewed -introduced -invited +identifies +ignores +imagines +impresses +improves +includes +increases +interviews +introduces +invites jabbers jacks jags @@ -201,15 +187,13 @@ jams jests jibs jockeys -jogged -joined +jogs joins joints jokes judges juices jumbles -jumped jumps justifies keels @@ -223,21 +207,17 @@ kisses kneads kneels knits -knocked knocks knots knows -labeled labels laces lags lambs -landed lands laps lards lashes -lasted lasts latches lathes @@ -245,30 +225,28 @@ laughs launches lays leads -learned -liked -linked -listed -listened -lived -located -looked -loved -managed -marked -matched -measured -mentioned -missed -moved +learns +likes +links +listens +lists +lives +locates +looks +loves +manages +marks +matches +measures +mentions +misses +moves nabs nags nails -named names narrates narrows -needed needs neglects neighs @@ -277,10 +255,10 @@ nests nips nods noses -noted -noticed +notes +notices notifies -numbered +numbers obeys objectifies objects @@ -289,40 +267,37 @@ observes obtains occurs offends -offered +offers officiates ogles oils -opened opens operates -ordered orders -organized +organizes owes -packed -painted -pampered -pardoned -parked -participated -passed -performed -persuaded -picked -planned -played -pleased -practiced -predicted -preferred -presented -programmed -protected -provided -purchased -pushed -quacked +packs +paints +pampers +pardons +parks +participates +passes +performs +persuades +picks +plans +plays +pleases +practices +predicts +prefers +presents +programs +protects +provides +purchases +pushes quacks quadruples qualifies @@ -346,52 +321,49 @@ rains raises rakes rambles -ramified ramifies ramps ransacks -received -recommended +receives +recommends redoes redraws reduces reeks reels -related -relaxed -released -remembered -repaired -repeated -resisted -resonated +relates +relaxes +releases +remembers +repairs +repeats +resists resonates -rested -returned -reviewed -sailed -saved -scanned -scared -shared -shopped -shouted -sidled +rests +returns +reviews +sails +saves +scans +scares +shares +shops +shouts sidles -skated -skied -slowed -sneezed -snowed -solved -spelled -started -stepped -stopped -stressed -studied -substituted -suggested +skates +skis +slows +sneezes +snows +solves +spells +starts +steps +stops +stresses +studies +substitutes +suggests tackles tacks tags @@ -407,7 +379,6 @@ teaches tears teems telegraphs -thrummed thrums ulcerates unbars diff --git a/openspec/changes/codebase-improvements/tasks.md b/openspec/changes/codebase-improvements/tasks.md index 3412ba8..297f3fe 100644 --- a/openspec/changes/codebase-improvements/tasks.md +++ b/openspec/changes/codebase-improvements/tasks.md @@ -55,7 +55,7 @@ ## 10. Word List Quality -- [ ] 10.1 Audit verb list and convert past-tense / past-participle entries to 3rd-person present tense -- [ ] 10.2 Run `task data:dupe` and `task data:spellcheck` after verb edits to verify no duplicates or typos -- [ ] 10.3 Identify the 72 adjective/noun overlap words and remove each from the less-appropriate list -- [ ] 10.4 Re-run combination count to confirm 2-word space stays above 4 million +- [x] 10.1 Audit verb list and convert past-tense / past-participle entries to 3rd-person present tense +- [x] 10.2 Run `task data:dupe` and `task data:spellcheck` after verb edits to verify no duplicates or typos +- [x] 10.3 Identify the 72 adjective/noun overlap words and remove each from the less-appropriate list +- [x] 10.4 Re-run combination count to confirm 2-word space stays above 4 million From 6b2e859019d247ccce495be457020882fc6bd8a3 Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 2 Mar 2026 18:02:16 -0700 Subject: [PATCH 4/5] docs: Archive codebase-improvements specs. --- .../.openspec.yaml | 0 .../design.md | 0 .../proposal.md | 0 .../specs/custom-dictionary/spec.md | 0 .../specs/generator-validation/spec.md | 0 .../specs/output-formatting/spec.md | 0 .../specs/seed-handling/spec.md | 0 .../specs/word-list-quality/spec.md | 0 .../tasks.md | 0 openspec/specs/custom-dictionary/spec.md | 25 ++++++++++++++++ openspec/specs/generator-validation/spec.md | 29 +++++++++++++++++++ openspec/specs/output-formatting/spec.md | 25 ++++++++++++++++ openspec/specs/seed-handling/spec.md | 25 ++++++++++++++++ openspec/specs/word-list-quality/spec.md | 21 ++++++++++++++ 14 files changed, 125 insertions(+) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/.openspec.yaml (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/design.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/proposal.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/specs/custom-dictionary/spec.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/specs/generator-validation/spec.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/specs/output-formatting/spec.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/specs/seed-handling/spec.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/specs/word-list-quality/spec.md (100%) rename openspec/changes/{codebase-improvements => archive/2026-03-02-codebase-improvements}/tasks.md (100%) create mode 100644 openspec/specs/custom-dictionary/spec.md create mode 100644 openspec/specs/generator-validation/spec.md create mode 100644 openspec/specs/output-formatting/spec.md create mode 100644 openspec/specs/seed-handling/spec.md create mode 100644 openspec/specs/word-list-quality/spec.md diff --git a/openspec/changes/codebase-improvements/.openspec.yaml b/openspec/changes/archive/2026-03-02-codebase-improvements/.openspec.yaml similarity index 100% rename from openspec/changes/codebase-improvements/.openspec.yaml rename to openspec/changes/archive/2026-03-02-codebase-improvements/.openspec.yaml diff --git a/openspec/changes/codebase-improvements/design.md b/openspec/changes/archive/2026-03-02-codebase-improvements/design.md similarity index 100% rename from openspec/changes/codebase-improvements/design.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/design.md diff --git a/openspec/changes/codebase-improvements/proposal.md b/openspec/changes/archive/2026-03-02-codebase-improvements/proposal.md similarity index 100% rename from openspec/changes/codebase-improvements/proposal.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/proposal.md diff --git a/openspec/changes/codebase-improvements/specs/custom-dictionary/spec.md b/openspec/changes/archive/2026-03-02-codebase-improvements/specs/custom-dictionary/spec.md similarity index 100% rename from openspec/changes/codebase-improvements/specs/custom-dictionary/spec.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/specs/custom-dictionary/spec.md diff --git a/openspec/changes/codebase-improvements/specs/generator-validation/spec.md b/openspec/changes/archive/2026-03-02-codebase-improvements/specs/generator-validation/spec.md similarity index 100% rename from openspec/changes/codebase-improvements/specs/generator-validation/spec.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/specs/generator-validation/spec.md diff --git a/openspec/changes/codebase-improvements/specs/output-formatting/spec.md b/openspec/changes/archive/2026-03-02-codebase-improvements/specs/output-formatting/spec.md similarity index 100% rename from openspec/changes/codebase-improvements/specs/output-formatting/spec.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/specs/output-formatting/spec.md diff --git a/openspec/changes/codebase-improvements/specs/seed-handling/spec.md b/openspec/changes/archive/2026-03-02-codebase-improvements/specs/seed-handling/spec.md similarity index 100% rename from openspec/changes/codebase-improvements/specs/seed-handling/spec.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/specs/seed-handling/spec.md diff --git a/openspec/changes/codebase-improvements/specs/word-list-quality/spec.md b/openspec/changes/archive/2026-03-02-codebase-improvements/specs/word-list-quality/spec.md similarity index 100% rename from openspec/changes/codebase-improvements/specs/word-list-quality/spec.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/specs/word-list-quality/spec.md diff --git a/openspec/changes/codebase-improvements/tasks.md b/openspec/changes/archive/2026-03-02-codebase-improvements/tasks.md similarity index 100% rename from openspec/changes/codebase-improvements/tasks.md rename to openspec/changes/archive/2026-03-02-codebase-improvements/tasks.md diff --git a/openspec/specs/custom-dictionary/spec.md b/openspec/specs/custom-dictionary/spec.md new file mode 100644 index 0000000..d2e978b --- /dev/null +++ b/openspec/specs/custom-dictionary/spec.md @@ -0,0 +1,25 @@ +### Requirement: Generator accepts a custom Dictionary +The `WithDictionary` option SHALL allow a caller to supply a `*Dictionary` instance, replacing the default embedded word lists. + +#### Scenario: Custom adjectives are used in generated names +- **WHEN** a caller provides a Dictionary with a custom adjective list +- **THEN** generated names only use words from that custom adjective list + +#### Scenario: Custom noun list is respected +- **WHEN** a caller provides a Dictionary with a custom noun list +- **THEN** generated names only use words from that custom noun list + +#### Scenario: Nil dictionary falls back to default +- **WHEN** a caller passes `nil` as the Dictionary to `WithDictionary` +- **THEN** the generator uses the default embedded Dictionary + +### Requirement: Dictionary can be constructed with custom word lists +The `NewDictionary` constructor (or an alternative constructor) SHALL accept optional word lists so callers can build a Dictionary without embedding data files. + +#### Scenario: Caller-provided word slices are used +- **WHEN** a caller constructs a Dictionary with custom adjective and noun slices +- **THEN** the Dictionary reports the correct lengths for those word categories + +#### Scenario: Empty word list for unused category is valid +- **WHEN** a caller constructs a 2-word Generator with a Dictionary that has an empty verb list +- **THEN** generation succeeds because verbs are not used for size-2 names diff --git a/openspec/specs/generator-validation/spec.md b/openspec/specs/generator-validation/spec.md new file mode 100644 index 0000000..2e8d7f4 --- /dev/null +++ b/openspec/specs/generator-validation/spec.md @@ -0,0 +1,29 @@ +### Requirement: Size validation occurs at option construction time +`WithSize` SHALL return an error immediately if the provided size is outside the valid range (2–4), so invalid generators cannot be constructed. + +#### Scenario: Invalid size is rejected at construction +- **WHEN** a library caller passes `WithSize(1)` to `NewGenerator` +- **THEN** `WithSize` returns a non-nil error before `NewGenerator` is called + +#### Scenario: Invalid size 5 is rejected at construction +- **WHEN** a library caller passes `WithSize(5)` to `NewGenerator` +- **THEN** `WithSize` returns a non-nil error + +#### Scenario: Valid sizes 2, 3, 4 are accepted +- **WHEN** a library caller passes `WithSize(2)`, `WithSize(3)`, or `WithSize(4)` +- **THEN** `WithSize` returns a nil error and the option applies successfully + +### Requirement: CLI rejects zero or negative quantity +The CLI SHALL return an error and non-zero exit code when `--quantity` is zero or negative, rather than producing no output silently. + +#### Scenario: Quantity zero prints an error +- **WHEN** a user runs `fname --quantity 0` +- **THEN** the CLI prints a descriptive error message and exits with a non-zero status + +#### Scenario: Negative quantity prints an error +- **WHEN** a user runs `fname --quantity -5` +- **THEN** the CLI prints a descriptive error message and exits with a non-zero status + +#### Scenario: Positive quantity works normally +- **WHEN** a user runs `fname --quantity 3` +- **THEN** the CLI prints 3 name phrases and exits with status 0 diff --git a/openspec/specs/output-formatting/spec.md b/openspec/specs/output-formatting/spec.md new file mode 100644 index 0000000..59da0b7 --- /dev/null +++ b/openspec/specs/output-formatting/spec.md @@ -0,0 +1,25 @@ +### Requirement: CLI supports structured JSON output +The CLI SHALL accept a `--format` flag that controls output format. The default value is `plain` (current behavior). When `json` is specified, output SHALL be a JSON array of name strings. + +#### Scenario: Default plain format is unchanged +- **WHEN** a user runs `fname --quantity 3` without `--format` +- **THEN** output is three names, one per line, as before + +#### Scenario: JSON format produces a valid JSON array +- **WHEN** a user runs `fname --format json --quantity 3` +- **THEN** output is a single JSON array, e.g. `["name1","name2","name3"]` + +#### Scenario: JSON format with quantity 1 produces a single-element array +- **WHEN** a user runs `fname --format json` +- **THEN** output is `["name"]` (an array, not a bare string) + +#### Scenario: Invalid format value produces an error +- **WHEN** a user runs `fname --format csv` +- **THEN** the CLI prints a descriptive error and exits with a non-zero status + +### Requirement: Short flag `-f` is the alias for `--format` +The `--format` flag SHALL have `-f` as its short-form alias. + +#### Scenario: Short flag produces same output as long flag +- **WHEN** a user runs `fname -f json` +- **THEN** output is identical to `fname --format json` diff --git a/openspec/specs/seed-handling/spec.md b/openspec/specs/seed-handling/spec.md new file mode 100644 index 0000000..7457f47 --- /dev/null +++ b/openspec/specs/seed-handling/spec.md @@ -0,0 +1,25 @@ +### Requirement: All int64 values are valid seeds +The generator seed option SHALL accept all int64 values, including negative values. No integer value SHALL be treated as a sentinel meaning "no seed." + +#### Scenario: Negative seed produces deterministic output +- **WHEN** a user provides `--seed -1` +- **THEN** the generator uses `-1` as the seed and produces deterministic output + +#### Scenario: Same negative seed produces same names +- **WHEN** two generators are created with the same negative seed value +- **THEN** both generators produce identical name sequences + +#### Scenario: Seed zero is valid +- **WHEN** a user provides `--seed 0` +- **THEN** the generator uses `0` as the seed and produces deterministic output + +### Requirement: Omitting seed produces random output +When no seed is provided, the generator SHALL use a time-based random seed, producing non-deterministic output across invocations. + +#### Scenario: No seed flag means random generation +- **WHEN** a user runs `fname` without `--seed` +- **THEN** repeated invocations produce different name sequences + +#### Scenario: WithSeed option is not applied when seed is absent +- **WHEN** a library caller creates a Generator without `WithSeed` +- **THEN** the generator behaves as if seeded randomly diff --git a/openspec/specs/word-list-quality/spec.md b/openspec/specs/word-list-quality/spec.md new file mode 100644 index 0000000..2237bed --- /dev/null +++ b/openspec/specs/word-list-quality/spec.md @@ -0,0 +1,21 @@ +### Requirement: Verb list uses consistent present-tense forms +All entries in the verb word list SHALL use 3rd-person singular present tense (e.g., "walks", "runs", "discovers"). Past tense, past participle, and bare infinitive forms SHALL be removed or converted. + +#### Scenario: Generated 3-word name uses a present-tense verb +- **WHEN** a user generates a size-3 name phrase multiple times +- **THEN** the verb component consistently reads as a present-tense action (ending in -s or -es for regular verbs) + +#### Scenario: No past-participle verbs appear in output +- **WHEN** a user generates a large batch of size-3 names +- **THEN** no verb component ends in "-ed" in a way that reads as past tense (e.g., "abandoned", "admired" are absent) + +### Requirement: No word appears in both the adjective and noun lists +Words that are dual-category (e.g., "blue", "dark", "cold") SHALL appear in at most one list. For each overlap word, the appropriate category SHALL be chosen based on how it reads in context as part of a generated name. + +#### Scenario: Adjective list contains no noun-list entries +- **WHEN** the adjective and noun data files are compared +- **THEN** there are zero words appearing in both files + +#### Scenario: Name quality is unaffected by overlap removal +- **WHEN** overlap words are removed from one list +- **THEN** the total combination space remains above 4 million for 2-word names From 3d2cb4f778225abecc264ff63ecb53543fbba65c Mon Sep 17 00:00:00 2001 From: Christopher Murphy Date: Mon, 2 Mar 2026 18:04:29 -0700 Subject: [PATCH 5/5] ci: Update Go version, install action. --- .github/workflows/build.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 486a4d1..44c4f0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,14 +4,12 @@ jobs: test: strategy: matrix: - go-version: [~1.17, ^1] + go-version: [~1.19, ^1] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} - env: - GO111MODULE: "on" steps: - name: Install Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - name: Checkout code