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

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_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:

// 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

  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.