From 53edd804766a383f2560e10dd113ac95f0dcc1af Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 20 Feb 2026 15:01:55 +0000 Subject: [PATCH] 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 --- CLAUDE.md | 74 +++++++------ FINDINGS.md | 35 ------ TODO.md | 45 -------- docs/architecture.md | 217 ++++++++++++++++++++++++++++++++++++ docs/development.md | 257 +++++++++++++++++++++++++++++++++++++++++++ docs/history.md | 119 ++++++++++++++++++++ 6 files changed, 632 insertions(+), 115 deletions(-) delete mode 100644 FINDINGS.md delete mode 100644 TODO.md create mode 100644 docs/architecture.md create mode 100644 docs/development.md create mode 100644 docs/history.md diff --git a/CLAUDE.md b/CLAUDE.md index 35ecb72..80099f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,54 +1,38 @@ # CLAUDE.md -## Project - -`go-html` is an HLCRF DOM compositor with grammar pipeline. Module path: `forge.lthn.ai/core/go-html` +Agent instructions for `go-html`. Module path: `forge.lthn.ai/core/go-html` ## Commands ```bash -go test ./... # Run all tests -go test -run TestName ./... # Single test -go test -bench . ./... # Benchmarks -go vet ./... # Static analysis -GOOS=js GOARCH=wasm go build -o gohtml.wasm ./cmd/wasm/ # WASM build +go test ./... # Run all tests +go test -run TestName ./... # Single test +go test -short ./... # Skip slow WASM build test +go test -bench . ./... # Benchmarks +go vet ./... # Static analysis +GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o gohtml.wasm ./cmd/wasm/ # WASM build +echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ # Codegen CLI ``` ## Architecture +See `docs/architecture.md` for full detail. Summary: + - **Node interface**: `Render(ctx *Context) string` — El, Text, Raw, If, Unless, Each[T], Switch, Entitled - **HLCRF Layout**: Header/Left/Content/Right/Footer compositor with ARIA roles and deterministic `data-block` IDs - **Responsive**: Multi-variant breakpoint wrapper (`data-variant` attributes) -- **Pipeline**: Render → StripTags → go-i18n/reversal Tokenise → GrammarImprint -- **Codegen**: Web Component classes with closed Shadow DOM -- **WASM**: `cmd/wasm/` exports `renderToString()` only (lean client-side renderer, ~830KB gzip) -- **Codegen CLI**: `cmd/codegen/` reads slot JSON from stdin, writes WC bundle JS to stdout (build-time tool, not in WASM) +- **Pipeline**: Render → StripTags → go-i18n/reversal Tokenise → GrammarImprint (server-side only) +- **Codegen**: Web Component classes with closed Shadow DOM, generated at build time by `cmd/codegen/` +- **WASM**: `cmd/wasm/` exports `renderToString()` only — 2.90 MB raw / 842 KB gzip ## Server/Client Split -Files guarded with `//go:build !js` are excluded from WASM builds: -- `pipeline.go` — Imprint/CompareVariants use `go-i18n/reversal` (heavyweight, server-side only) +Files guarded with `//go:build !js` are excluded from WASM: + +- `pipeline.go` — Imprint/CompareVariants use `go-i18n/reversal` (server-side only) - `cmd/wasm/register.go` — encoding/json + codegen (replaced by `cmd/codegen/` CLI) -WASM binary contains only: node types, layout, responsive, context, render, path, and go-i18n core (translation). - -## Dependencies - -- `forge.lthn.ai/core/go-i18n` (replace directive → `../go-i18n`) -- go-i18n must be present alongside this repo for builds - -## Coding Standards - -- UK English (colour, organisation, centre) -- All types annotated -- Tests use `testify` assert/require -- Licence: EUPL-1.2 -- Safe-by-default: HTML escaping on Text nodes, void element handling, entitlement deny-by-default -- Deterministic output: sorted attributes, reproducible paths - -## Test Conventions - -No specific suffix pattern — use table-driven subtests with `t.Run()`. +Never import `encoding/json`, `text/template`, or `fmt` in WASM-linked code. Use string concatenation instead of `fmt.Sprintf` in `layout.go` and any other file without a `!js` guard. ## Key Files @@ -56,9 +40,29 @@ No specific suffix pattern — use table-driven subtests with `t.Run()`. |------|---------| | `node.go` | All node types (El, Text, Raw, If, Unless, Each, Switch, Entitled) | | `layout.go` | HLCRF compositor | -| `pipeline.go` | StripTags, Imprint, CompareVariants | +| `pipeline.go` | StripTags, Imprint, CompareVariants (!js only) | | `responsive.go` | Multi-variant breakpoint wrapper | | `context.go` | Rendering context (Identity, Locale, Entitlements, i18n Service) | | `codegen/codegen.go` | Web Component class generation | -| `cmd/wasm/main.go` | WASM entry point (`renderToString` only) | +| `cmd/wasm/main.go` | WASM entry point (renderToString only) | | `cmd/codegen/main.go` | Build-time CLI for WC bundle generation | +| `cmd/wasm/size_test.go` | WASM binary size gate (< 1 MB gzip, < 3 MB raw) | + +## Dependencies + +- `forge.lthn.ai/core/go-i18n` (replace directive → `../go-i18n`) +- `go-i18n` and `go-inference` must be present alongside this repo for builds + +## Coding Standards + +- UK English (colour, organisation, centre) +- All types annotated +- Tests use `testify` assert/require +- Licence: EUPL-1.2 — add `// SPDX-Licence-Identifier: EUPL-1.2` to new files +- Safe-by-default: HTML escaping on Text nodes, void element handling, entitlement deny-by-default +- Deterministic output: sorted attributes, reproducible paths +- Commits: conventional commits + `Co-Authored-By: Virgil ` + +## Test Conventions + +No specific suffix pattern. Use table-driven subtests with `t.Run()`. Integration tests that use `Text` nodes must call `i18n.SetDefault(svc)` before rendering. diff --git a/FINDINGS.md b/FINDINGS.md deleted file mode 100644 index e9b6a3c..0000000 --- a/FINDINGS.md +++ /dev/null @@ -1,35 +0,0 @@ -# Findings - -## Code Quality - -- **53 tests, 100% pass** — excellent coverage ratios across all packages -- **Zero TODOs/FIXMEs** in codebase — clean -- **`go vet` clean** — no static analysis warnings -- **Safe-by-default design** — XSS prevention verified in render_test.go, HTML escaping on all Text nodes, void elements self-close, entitlements deny-by-default - -## Architecture Strengths - -- Clean minimal API: 9 public constructors + Node interface -- Type-safe generics: `Each[T]` for iteration -- Deterministic output: sorted attributes, reproducible block IDs -- Fluent builder pattern: `NewLayout("HLCRF").H(node).C(node).F(node)` -- Pipeline bridges rendering to privacy layer (GrammarImprint via go-i18n reversal) - -## Known Issues - -1. **WASM size blocker** — 6.0 MB raw / 1.58 MB gzip. Root cause: stdlib imports (json, encoding, text/template) bloat the WASM binary. Makefile rejects at 1 MB gzip threshold. -2. **No WASM main_test.go** — cmd/wasm/ has register_test.go but no integration test for the JS exports. -3. **Layout accepts invalid variants silently** — `NewLayout("XYZ")` renders nothing, no error returned. -4. **Context.service is private** — Must use `NewContextWithService()`. Limits custom i18n adapter injection. -5. **Responsive only accepts *Layout** — Cannot nest arbitrary nodes in variants, must wrap in Layout first. - -## Coverage Gaps - -| File | Lines | Tests | -|------|-------|-------| -| node.go | 254 | 206 lines of tests (81%) | -| layout.go | 119 | 116 lines (97%) | -| pipeline.go | 83 | 128 lines (154%) | -| responsive.go | 39 | 89 lines (228%) | -| codegen.go | 90 | 54 lines (60%) | -| cmd/wasm/main.go | 78 | **0 lines (0%)** | diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 32c4297..0000000 --- a/TODO.md +++ /dev/null @@ -1,45 +0,0 @@ -# TODO - -## High Priority — WASM Binary Size Fix - -~~Current: 6.04 MB raw / 1.58 MB gzip.~~ Fixed: 2.90 MB raw / 830 KB gzip. Target: < 1 MB gzip (Makefile gate: WASM_GZ_LIMIT 1048576). - -Root cause: `registerComponents()` pulled in `encoding/json` (~200KB gz), `text/template` (~125KB gz), and `fmt` (~50KB gz). Plus `pipeline.go` linked `go-i18n/reversal` (~250KB gz). These were heavyweight imports for code that doesn't need to run client-side. - -### Step 1: Remove `registerComponents()` from WASM - -- [x] **Move `cmd/wasm/register.go` out of WASM** — Added `//go:build !js` build tag. The `registerComponents()` JS bridge in `main.go` removed. This removes `encoding/json` and `text/template` from the binary. -- [x] **Move codegen to build-time CLI** — Created `cmd/codegen/main.go` that reads slot config from stdin (JSON) and writes generated JS to stdout. Usage: `echo '{"H":"nav-bar","C":"main-content"}' | go run ./cmd/codegen/ > components.js`. Consumers pre-generate during build. -- [x] **Update `cmd/wasm/main.go`** — Removed `registerComponents` from the `gohtml` JS object. Only exposes `renderToString`. - -### Step 2: Remove Pipeline from WASM - -- [x] **Guard `pipeline.go` with build tag** — Added `//go:build !js` to `pipeline.go`. The `Imprint()` and `CompareVariants()` functions use `go-i18n/reversal` which is heavyweight. Server-side analysis only. -- [x] **Update `cmd/wasm/main.go`** — No references to `pipeline.go` functions. `renderToString` never used them. - -### Step 3: Minimise `fmt` Usage - -- [x] **Replace `fmt.Sprintf` in WASM-linked code** — Replaced `fmt.Sprintf` in `layout.go` `blockID()` with string concatenation. `fmt` eliminated from the WASM import graph. - -### Step 4: Verify Size - -- [x] **Build and measure** — 2,900,777 bytes raw, 830,314 bytes gzip (842,146 via `make wasm`). Well under 1 MB limit. -- [x] **Document the server/client split** — Updated CLAUDE.md with new architecture: WASM = `renderToString()` only, codegen = build-time CLI. - -### Step 5: Tests - -- [x] **WASM build gate test** — `TestWASMBinarySize` in `cmd/wasm/size_test.go`: builds WASM, gzips, asserts < 1MB gzip and < 3MB raw. Result: 2.90MB raw, 842KB gzip. `//go:build !js` guarded. -- [x] **Codegen CLI test** — `cmd/codegen/main_test.go`: pipe JSON stdin, verify JS output matches `GenerateBundle()` -- [x] **renderToString still works** — Existing WASM tests for `renderToString` pass (build-tag guarded) -- [x] **Existing tests still pass** — `go test ./...` (non-WASM) all 70+ tests pass, pipeline/codegen tests unaffected - -## Medium Priority - -- [ ] **TypeScript type definitions** — Add `.d.ts` generation alongside `GenerateBundle()` for Web Component consumers. -- [ ] **Accessibility helpers** — Layout has semantic HTML + ARIA roles but no `aria-label` builder, alt text helpers, or focus management nodes. -- [ ] **Layout variant validation** — `NewLayout("XYZ")` silently produces empty output. Add warning or error for invalid slot characters. - -## Low Priority - -- [ ] **Browser polyfill documentation** — Document closed Shadow DOM support matrix. -- [ ] **CSS scoping helper** — Optional utility for responsive variant CSS targeting. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..405da0c --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,217 @@ +# Architecture + +`go-html` is an HLCRF DOM compositor with grammar pipeline integration. It provides a pure-Go, type-safe HTML rendering library designed for server-side generation with an optional lightweight WASM client module. + +Module path: `forge.lthn.ai/core/go-html` + +## Node Interface + +All renderable units implement a single interface: + +```go +type Node interface { + Render(ctx *Context) string +} +``` + +Every node type is a private struct with a public constructor. The API surface is intentionally small: nine public constructors plus `Attr()` and `Render()` helpers. + +| Constructor | Description | +|-------------|-------------| +| `El(tag, ...Node)` | HTML element with children | +| `Attr(Node, key, value)` | Set attribute on an El node; chainable | +| `Text(key, ...any)` | Translated, HTML-escaped text via go-i18n | +| `Raw(content)` | Unescaped trusted content | +| `If(cond, Node)` | Conditional render | +| `Unless(cond, Node)` | Inverse conditional render | +| `Each[T](items, fn)` | Type-safe iteration with generics | +| `Switch(selector, cases)` | Runtime dispatch to named cases | +| `Entitled(feature, Node)` | Entitlement-gated render; deny-by-default | + +### Safety guarantees + +- `Text` nodes are always HTML-escaped. XSS via user-supplied strings fed through `Text()` is not possible. +- `Raw` is an explicit escape hatch for trusted content only. Its name signals intent. +- `Entitled` returns an empty string when no entitlement function is set on the context. Access is denied by default, not granted. +- `El` attributes are sorted alphabetically before output, producing deterministic HTML regardless of insertion order. +- Void elements (`br`, `img`, `input`, etc.) never emit a closing tag. + +## HLCRF Layout + +The `Layout` type is a compositor for five named slots: **H**eader, **L**eft, **C**ontent, **R**ight, **F**ooter. Each slot maps to a specific semantic HTML element and ARIA role: + +| Slot | Element | ARIA role | +|------|---------|-----------| +| H | `
` | `banner` | +| L | `