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>
226 lines
6.4 KiB
Markdown
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.
|