Skip to content

Commit 3f92167

Browse files
committed
bugfix: Fix issue with default seed value.
- Add codebase improvement spec docs.
1 parent 99fb9a4 commit 3f92167

12 files changed

Lines changed: 375 additions & 6 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
**/*.bak
33

44
.vscode/
5+
.claude/
6+
.codex/
57

68
dist/
9+
fname

cmd/fname/fname.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,16 @@ func main() {
4747
delimiter string = "-"
4848
help bool
4949
ver bool
50-
quantity int = 1
51-
size uint = 2
52-
seed int64 = -1
53-
// TODO: add option to use custom dictionary
50+
quantity int = 1
51+
size uint = 2
52+
seed int64
5453
)
5554

5655
pflag.StringVarP(&casing, "casing", "c", casing, "set the casing of the generated name <title|upper|lower>")
5756
pflag.StringVarP(&delimiter, "delimiter", "d", delimiter, "set the delimiter used to join words")
5857
pflag.IntVarP(&quantity, "quantity", "q", quantity, "set the number of names to generate")
5958
pflag.UintVarP(&size, "size", "z", size, "set the number of words in the generated name (minimum 2, maximum 4)")
60-
pflag.Int64VarP(&seed, "seed", "s", seed, "random generator seed")
59+
pflag.Int64VarP(&seed, "seed", "s", 0, "random generator seed")
6160
pflag.BoolVarP(&help, "help", "h", help, "show fname usage")
6261
pflag.BoolVarP(&ver, "version", "v", ver, "show fname version")
6362
pflag.Parse()
@@ -80,7 +79,7 @@ func main() {
8079
fname.WithDelimiter(delimiter),
8180
}
8281

83-
if seed != -1 {
82+
if pflag.Lookup("seed").Changed {
8483
opts = append(opts, fname.WithSeed(seed))
8584
}
8685
if size != 2 {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-03-02
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
## Context
2+
3+
`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.
4+
5+
Current pain points:
6+
- The seed `-1` sentinel makes a valid int64 input silently unusable
7+
- An index-based collision loop guards against a problem that doesn't exist
8+
- Size validation happens too late (at generate time, not construction time)
9+
- The word-list parser uses streaming I/O on in-memory strings
10+
- Verb tenses are inconsistent across the 448-word verb list
11+
- 72 words appear in both adjective and noun lists
12+
13+
## Goals / Non-Goals
14+
15+
**Goals:**
16+
- Fix the seed sentinel bug with a clean API that accepts all int64 values
17+
- Remove dead/incorrect logic (collision loop)
18+
- Validate generator options eagerly at construction time
19+
- Improve word list quality (verb tense consistency, cross-list overlap)
20+
- Minor performance improvements (split, casing switch)
21+
- Add `WithDictionary()` to fulfill the existing TODO
22+
- Add `--format` output flag to the CLI
23+
24+
**Non-Goals:**
25+
- Rewriting the generator architecture
26+
- Changing the default name format or word order
27+
- Expanding the size range beyond 2–4
28+
- Adding cryptographic randomness
29+
30+
## Decisions
31+
32+
### D1: Seed flag uses `*int64` instead of sentinel `-1`
33+
34+
**Decision**: Change the `seed` variable in `main()` from `int64 = -1` to `*int64` (nil = unset). Pass `fname.WithSeed(*seed)` only when non-nil.
35+
36+
**Alternatives considered**:
37+
- Separate `--no-seed` bool flag: adds a flag just to undo another flag, confusing
38+
- Use `0` as sentinel: same problem, `0` is a valid seed
39+
- `*int64` nil pointer: idiomatic Go for "optional value", clean, no reserved values
40+
41+
**Impact**: CLI-only change. The library's `WithSeed(int64)` signature is unchanged.
42+
43+
### D2: Remove collision-avoidance loop, no replacement
44+
45+
**Decision**: Delete the `for adjectiveIndex == nounIndex` loop entirely. No replacement needed.
46+
47+
**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.
48+
49+
**Alternatives considered**:
50+
- 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)
51+
- Keep loop as-is: incorrect semantics, wasted cycles
52+
53+
### D3: Eager size validation in `WithSize`
54+
55+
**Decision**: Return an error from `WithSize()`, changing its signature to `func WithSize(size uint) (GeneratorOption, error)`. Callers get immediate feedback on invalid sizes.
56+
57+
**Alternatives considered**:
58+
- Validate in `NewGenerator()` and return `(*Generator, error)`: larger API change, but cleaner; deferred for a future refactor
59+
- Keep deferred validation in `Generate()`: current state, poor library ergonomics
60+
- Panic in `WithSize()`: not idiomatic Go for user input validation
61+
62+
**Note**: This is a **BREAKING** change to the `WithSize` function signature.
63+
64+
### D4: Replace `bufio.Scanner` with `strings.Split`
65+
66+
**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.
67+
68+
**Rationale**: Simpler, faster, no reader allocation. The only edge case is a trailing newline on the embedded string, which `strings.TrimRight` handles.
69+
70+
### D5: Replace `casingMap` with a `switch` in `applyCasing`
71+
72+
**Decision**: Remove `casingMap` and replace the map lookup in `applyCasing` with a direct `switch` on `g.casing`.
73+
74+
**Rationale**: Three cases don't benefit from a map. A switch is zero-allocation, branch-predictor friendly, and more readable.
75+
76+
### D6: Document (not fix) goroutine safety
77+
78+
**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.
79+
80+
**Alternatives considered**:
81+
- Add a `sync.Mutex` around rand calls: adds lock contention overhead for the common single-goroutine case
82+
- Switch to `math/rand/v2` global rand: changes minimum Go version requirement and behavior
83+
84+
### D7: `WithDictionary()` takes a `*Dictionary`
85+
86+
**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.
87+
88+
**Rationale**: Minimal API surface, composable with existing options, fulfills the existing TODO with no breakage.
89+
90+
### D8: `CasingFromString``ParseCasing`
91+
92+
**Decision**: Rename `CasingFromString` to `ParseCasing`. Keep `CasingFromString` as a deprecated alias for one release cycle.
93+
94+
**Rationale**: `ParseX` is the idiomatic Go convention for string-to-value parsing (cf. `strconv.ParseInt`, `time.Parse`).
95+
96+
### D9: `--format` flag with `plain` (default) and `json`
97+
98+
**Decision**: Add `--format` / `-f` flag accepting `plain` (default, current behavior) or `json`. JSON output is an array of name strings: `["name1","name2"]`.
99+
100+
**Rationale**: Enables scripting without `xargs` / `sed` gymnastics. An array is more useful than newline-delimited JSON objects for batch generation.
101+
102+
## Risks / Trade-offs
103+
104+
- **`WithSize` signature change is breaking** → Mitigated by it being a small, focused library; version bump to communicate the change
105+
- **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
106+
- **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)
107+
108+
## Open Questions
109+
110+
- 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)
111+
- Should JSON output for `--format json` include metadata (seed used, size, count)? Or just the name array?
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## Why
2+
3+
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.
4+
5+
## What Changes
6+
7+
- Fix seed sentinel bug: `-1` is currently treated as "no seed provided," making it impossible to use `-1` as a seed value
8+
- Remove false collision-avoidance loop that compares indices across differently-sized arrays (solving a non-existent problem)
9+
- Move size validation to construction time so invalid generators fail eagerly
10+
- Add validation for `--quantity` flag to reject zero or negative values with a clear error
11+
- Replace `bufio.Scanner` with `strings.Split` for in-memory word list parsing
12+
- Replace `casingMap` map lookup with a direct `switch` in `applyCasing`
13+
- Add goroutine safety to `Generator` (or document that it is not safe for concurrent use)
14+
- Normalize verb tense across the verb word list (consistent 3rd-person singular present tense)
15+
- Audit and clean up adjective/noun word overlap (72 words appear in both lists)
16+
- Add `--format` flag to CLI for structured output (e.g., JSON, newline-delimited)
17+
- Implement `WithDictionary()` option to fulfill the existing `TODO` in `dictionary.go`
18+
- Rename `CasingFromString` to `ParseCasing` for idiomatic Go style
19+
20+
## Capabilities
21+
22+
### New Capabilities
23+
24+
- `seed-handling`: Correct, unambiguous seed input — use `*int64` or a separate flag rather than a sentinel value
25+
- `generator-validation`: Eager validation of generator options at construction time, including size and quantity
26+
- `custom-dictionary`: `WithDictionary()` option allowing callers to supply their own word lists
27+
- `output-formatting`: `--format` CLI flag supporting structured output (JSON, plain)
28+
- `word-list-quality`: Normalized verb tenses and cleaned adjective/noun overlap in data files
29+
30+
### Modified Capabilities
31+
32+
## Impact
33+
34+
- `generator.go`: seed logic, collision loop removal, size validation, `applyCasing` switch, concurrency docs
35+
- `cmd/fname/fname.go`: seed flag type change, quantity validation, `--format` flag
36+
- `dictionary.go`: `split()` implementation, `WithDictionary()` option, `ParseCasing` rename
37+
- `data/verb`, `data/adjective`, `data/noun`: word list data file edits
38+
- Library public API: `CasingFromString``ParseCasing` is a **BREAKING** rename; `WithDictionary` is additive
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Generator accepts a custom Dictionary
4+
The `WithDictionary` option SHALL allow a caller to supply a `*Dictionary` instance, replacing the default embedded word lists.
5+
6+
#### Scenario: Custom adjectives are used in generated names
7+
- **WHEN** a caller provides a Dictionary with a custom adjective list
8+
- **THEN** generated names only use words from that custom adjective list
9+
10+
#### Scenario: Custom noun list is respected
11+
- **WHEN** a caller provides a Dictionary with a custom noun list
12+
- **THEN** generated names only use words from that custom noun list
13+
14+
#### Scenario: Nil dictionary falls back to default
15+
- **WHEN** a caller passes `nil` as the Dictionary to `WithDictionary`
16+
- **THEN** the generator uses the default embedded Dictionary
17+
18+
### Requirement: Dictionary can be constructed with custom word lists
19+
The `NewDictionary` constructor (or an alternative constructor) SHALL accept optional word lists so callers can build a Dictionary without embedding data files.
20+
21+
#### Scenario: Caller-provided word slices are used
22+
- **WHEN** a caller constructs a Dictionary with custom adjective and noun slices
23+
- **THEN** the Dictionary reports the correct lengths for those word categories
24+
25+
#### Scenario: Empty word list for unused category is valid
26+
- **WHEN** a caller constructs a 2-word Generator with a Dictionary that has an empty verb list
27+
- **THEN** generation succeeds because verbs are not used for size-2 names
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Size validation occurs at option construction time
4+
`WithSize` SHALL return an error immediately if the provided size is outside the valid range (2–4), so invalid generators cannot be constructed.
5+
6+
#### Scenario: Invalid size is rejected at construction
7+
- **WHEN** a library caller passes `WithSize(1)` to `NewGenerator`
8+
- **THEN** `WithSize` returns a non-nil error before `NewGenerator` is called
9+
10+
#### Scenario: Invalid size 5 is rejected at construction
11+
- **WHEN** a library caller passes `WithSize(5)` to `NewGenerator`
12+
- **THEN** `WithSize` returns a non-nil error
13+
14+
#### Scenario: Valid sizes 2, 3, 4 are accepted
15+
- **WHEN** a library caller passes `WithSize(2)`, `WithSize(3)`, or `WithSize(4)`
16+
- **THEN** `WithSize` returns a nil error and the option applies successfully
17+
18+
### Requirement: CLI rejects zero or negative quantity
19+
The CLI SHALL return an error and non-zero exit code when `--quantity` is zero or negative, rather than producing no output silently.
20+
21+
#### Scenario: Quantity zero prints an error
22+
- **WHEN** a user runs `fname --quantity 0`
23+
- **THEN** the CLI prints a descriptive error message and exits with a non-zero status
24+
25+
#### Scenario: Negative quantity prints an error
26+
- **WHEN** a user runs `fname --quantity -5`
27+
- **THEN** the CLI prints a descriptive error message and exits with a non-zero status
28+
29+
#### Scenario: Positive quantity works normally
30+
- **WHEN** a user runs `fname --quantity 3`
31+
- **THEN** the CLI prints 3 name phrases and exits with status 0
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## ADDED Requirements
2+
3+
### Requirement: CLI supports structured JSON output
4+
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.
5+
6+
#### Scenario: Default plain format is unchanged
7+
- **WHEN** a user runs `fname --quantity 3` without `--format`
8+
- **THEN** output is three names, one per line, as before
9+
10+
#### Scenario: JSON format produces a valid JSON array
11+
- **WHEN** a user runs `fname --format json --quantity 3`
12+
- **THEN** output is a single JSON array, e.g. `["name1","name2","name3"]`
13+
14+
#### Scenario: JSON format with quantity 1 produces a single-element array
15+
- **WHEN** a user runs `fname --format json`
16+
- **THEN** output is `["name"]` (an array, not a bare string)
17+
18+
#### Scenario: Invalid format value produces an error
19+
- **WHEN** a user runs `fname --format csv`
20+
- **THEN** the CLI prints a descriptive error and exits with a non-zero status
21+
22+
### Requirement: Short flag `-f` is the alias for `--format`
23+
The `--format` flag SHALL have `-f` as its short-form alias.
24+
25+
#### Scenario: Short flag produces same output as long flag
26+
- **WHEN** a user runs `fname -f json`
27+
- **THEN** output is identical to `fname --format json`
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## ADDED Requirements
2+
3+
### Requirement: All int64 values are valid seeds
4+
The generator seed option SHALL accept all int64 values, including negative values. No integer value SHALL be treated as a sentinel meaning "no seed."
5+
6+
#### Scenario: Negative seed produces deterministic output
7+
- **WHEN** a user provides `--seed -1`
8+
- **THEN** the generator uses `-1` as the seed and produces deterministic output
9+
10+
#### Scenario: Same negative seed produces same names
11+
- **WHEN** two generators are created with the same negative seed value
12+
- **THEN** both generators produce identical name sequences
13+
14+
#### Scenario: Seed zero is valid
15+
- **WHEN** a user provides `--seed 0`
16+
- **THEN** the generator uses `0` as the seed and produces deterministic output
17+
18+
### Requirement: Omitting seed produces random output
19+
When no seed is provided, the generator SHALL use a time-based random seed, producing non-deterministic output across invocations.
20+
21+
#### Scenario: No seed flag means random generation
22+
- **WHEN** a user runs `fname` without `--seed`
23+
- **THEN** repeated invocations produce different name sequences
24+
25+
#### Scenario: WithSeed option is not applied when seed is absent
26+
- **WHEN** a library caller creates a Generator without `WithSeed`
27+
- **THEN** the generator behaves as if seeded randomly
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Verb list uses consistent present-tense forms
4+
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.
5+
6+
#### Scenario: Generated 3-word name uses a present-tense verb
7+
- **WHEN** a user generates a size-3 name phrase multiple times
8+
- **THEN** the verb component consistently reads as a present-tense action (ending in -s or -es for regular verbs)
9+
10+
#### Scenario: No past-participle verbs appear in output
11+
- **WHEN** a user generates a large batch of size-3 names
12+
- **THEN** no verb component ends in "-ed" in a way that reads as past tense (e.g., "abandoned", "admired" are absent)
13+
14+
### Requirement: No word appears in both the adjective and noun lists
15+
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.
16+
17+
#### Scenario: Adjective list contains no noun-list entries
18+
- **WHEN** the adjective and noun data files are compared
19+
- **THEN** there are zero words appearing in both files
20+
21+
#### Scenario: Name quality is unaffected by overlap removal
22+
- **WHEN** overlap words are removed from one list
23+
- **THEN** the total combination space remains above 4 million for 2-word names

0 commit comments

Comments
 (0)