# 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 ``` 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 ``` ## 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.