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>
6.4 KiB
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:
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:
go test ./...
Run a single test by name:
go test -v -run TestSearch_PhraseSearch ./...
Run benchmarks:
go test -bench=. -benchmem ./...
Run with the race detector:
go test -race ./...
Check for lint and vet issues:
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:
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.
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():
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:
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_typesequivalent: all Go code must passgo vet ./...cleanly before commit.- All exported symbols must have documentation comments.
- All parameters and return types must be explicitly typed (no use of
anyas a shortcut for typed returns). - Error values must be checked;
_assignments for errors must carry a//nolintcomment 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:
// 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):
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
- Add the
// SPDX-Licence-Identifier: EUPL-1.2header. - Add the
package helpdeclaration. - Write exported symbols with documentation comments.
- Create a corresponding
_test.gofile in the same package (package help). - Ensure
go test ./...andgo vet ./...pass before committing.
Adding a New Template
- Create the
.htmlfile intemplates/. - The file must extend
base.htmlvia{{template "base" .}}and define a{{define "content"}}block. - Add a corresponding data struct in
templates.goif the template requires non-trivial data. - Add a
renderPagecall site inserver.go(for live serving) andgenerate.go(for static output) as appropriate. - Add tests in
templates_test.goverifying that the template parses and renders without error.