go-help/docs/development.md
Snider 142567a8f5 docs: graduate TODO/FINDINGS into production documentation
Replace internal task tracking (TODO.md, FINDINGS.md) with structured
documentation in docs/. Trim CLAUDE.md to agent instructions only.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 15:01:55 +00:00

226 lines
6.4 KiB
Markdown

# Development Guide — go-help
## Prerequisites
- Go 1.25 or later (the module uses `b.Loop()` from Go 1.25 in benchmarks)
- No C toolchain required — pure Go, no CGO
- No external services required at test time
Verify your Go version:
```bash
go version
```
## Repository Layout
```
go-help/
├── topic.go # Topic, Section, Frontmatter types
├── catalog.go # Catalog: Add, List, Get, Search
├── parser.go # ParseTopic, ExtractFrontmatter, ExtractSections, GenerateID
├── search.go # searchIndex, Search, tokenize, levenshtein, highlight
├── stemmer.go # stem, stemInflectional, stemDerivational
├── render.go # RenderMarkdown (goldmark)
├── templates.go # Embedded templates, template functions, groupTopicsByTag
├── server.go # HTTP server, six routes
├── generate.go # Static site generator, client-side search JS
├── ingest.go # ParseHelpText, IngestCLIHelp
├── templates/ # Embedded HTML templates
│ ├── base.html
│ ├── index.html
│ ├── topic.html
│ ├── search.html
│ └── 404.html
├── *_test.go # Tests alongside source files
├── go.mod
└── go.sum
```
## Build and Test
Run all tests:
```bash
go test ./...
```
Run a single test by name:
```bash
go test -v -run TestSearch_PhraseSearch ./...
```
Run benchmarks:
```bash
go test -bench=. -benchmem ./...
```
Run with the race detector:
```bash
go test -race ./...
```
Check for lint and vet issues:
```bash
go vet ./...
```
There is no Taskfile in this repository; all operations use the standard `go` toolchain directly.
## Test Patterns
### Naming convention
Tests use the `_Good`, `_Bad`, `_Ugly` suffix pattern:
- `_Good` — happy path, expected successful behaviour
- `_Bad` — expected error conditions (topic not found, empty query, malformed input)
- `_Ugly` — edge cases: nil inputs, Unicode boundary conditions, empty strings, very large inputs
Example:
```go
func TestGenerateID_Good(t *testing.T) { ... }
func TestGenerateID_Bad(t *testing.T) { ... }
func TestGenerateID_Ugly(t *testing.T) { ... }
```
### HTTP handler tests
Server tests use `httptest.NewRecorder()` and `httptest.NewServer()` from the standard library. No external HTTP testing library is used.
```go
func TestServer_HandleSearch_Bad_EmptyQuery(t *testing.T) {
catalog := DefaultCatalog()
srv := NewServer(catalog, "")
req := httptest.NewRequest("GET", "/search", nil)
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
}
```
### Benchmark convention
Benchmarks use `b.Loop()` (Go 1.25+) and `b.ReportAllocs()`:
```go
func BenchmarkSearch_SingleWord(b *testing.B) {
catalog := buildBenchCatalog(150)
b.ReportAllocs()
for b.Loop() {
catalog.Search("configuration")
}
}
```
### Static site tests
Generator tests create a `t.TempDir()` output directory and verify the presence and content of expected output files:
```go
func TestGenerate_Good_FileStructure(t *testing.T) {
dir := t.TempDir()
err := Generate(catalog, dir)
require.NoError(t, err)
assert.FileExists(t, filepath.Join(dir, "index.html"))
assert.FileExists(t, filepath.Join(dir, "search-index.json"))
assert.DirExists(t, filepath.Join(dir, "topics"))
}
```
## Coding Standards
### Language
UK English throughout: colour, organisation, behaviour, licence, catalogue, centre, serialise, initialise. Never American spellings.
### Go style
- `declare strict_types` equivalent: all Go code must pass `go vet ./...` cleanly before commit.
- All exported symbols must have documentation comments.
- All parameters and return types must be explicitly typed (no use of `any` as a shortcut for typed returns).
- Error values must be checked; `_` assignments for errors must carry a `//nolint` comment with justification.
- Internal helpers are unexported (`lowercase`). Only the public API is exported.
### File headers
Files that constitute original work carry the SPDX licence identifier:
```go
// SPDX-Licence-Identifier: EUPL-1.2
```
This line appears as the first line of the file, before the `package` declaration.
### Imports
Standard library imports first, then external imports, separated by a blank line. No dot imports. No blank imports without a comment.
### Error messages
Error strings are lowercase and do not end with punctuation (Go convention):
```go
fmt.Errorf("topic not found: %s", id)
```
### Naming
- Acronyms in exported names follow Go conventions: `ID`, `URL`, `HTTP`, `JSON`, `API`.
- Template data structs are named `{page}Data` (e.g. `indexData`, `topicData`, `searchData`) and are unexported.
- Scoring constants are named `score{What}` (e.g. `scoreTitleBoost`, `scoreExactWord`).
## Licence
EUPL-1.2. Add the SPDX header to every new file that contains original source code.
## Commit Convention
Commits follow Conventional Commits:
```
type(scope): description
```
Common types: `feat`, `fix`, `test`, `docs`, `chore`, `refactor`.
Scope is the file or subsystem affected (e.g. `search`, `server`, `generate`, `ingest`).
Every commit must include the co-author trailer:
```
Co-Authored-By: Virgil <virgil@lethean.io>
```
Example commit message:
```
feat(search): add Levenshtein fuzzy matching with edit distance 2
Words of 3+ characters are matched against index tokens using two-row
dynamic programming. Fuzzy matches score at 0.3, below prefix (0.5)
and exact (1.0), to prefer precise matches in ranking.
Co-Authored-By: Virgil <virgil@lethean.io>
```
## Adding a New Source File
1. Add the `// SPDX-Licence-Identifier: EUPL-1.2` header.
2. Add the `package help` declaration.
3. Write exported symbols with documentation comments.
4. Create a corresponding `_test.go` file in the same package (`package help`).
5. Ensure `go test ./...` and `go vet ./...` pass before committing.
## Adding a New Template
1. Create the `.html` file in `templates/`.
2. The file must extend `base.html` via `{{template "base" .}}` and define a `{{define "content"}}` block.
3. Add a corresponding data struct in `templates.go` if the template requires non-trivial data.
4. Add a `renderPage` call site in `server.go` (for live serving) and `generate.go` (for static output) as appropriate.
5. Add tests in `templates_test.go` verifying that the template parses and renders without error.