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>
119 lines
7.6 KiB
Markdown
119 lines
7.6 KiB
Markdown
# Project History
|
|
|
|
## Phase 1: Core Node Types (initial scaffolding)
|
|
|
|
Commits: `d7bb0b2` through `c724094`
|
|
|
|
The module was scaffolded with the Go module path `forge.lthn.ai/core/go-html`. The foundational work established:
|
|
|
|
- `d7bb0b2` — Module scaffold, `Node` interface with `Render(ctx *Context) string`.
|
|
- `3e76e72` — `Text` node wired to `go-i18n` grammar pipeline with HTML escaping.
|
|
- `c724094` — Conditional nodes (`If`, `Unless`), entitlement gating (`Entitled`, deny-by-default), runtime dispatch (`Switch`), and type-safe iteration (`Each[T]`).
|
|
|
|
The `Raw` escape hatch was present from the first commit. The decision to make `Text` always escape and `Raw` never escape was made at this stage and has not changed.
|
|
|
|
## Phase 2: HLCRF Layout and Pipeline
|
|
|
|
Commits: `946ea8d` through `ef77793`
|
|
|
|
- `946ea8d` — `Layout` type with HLCRF slot registry. Semantic HTML elements (`<header>`, `<main>`, `<aside>`, `<footer>`) and ARIA roles assigned per slot.
|
|
- `d75988a` — Nested layout path chains. Block IDs computed as `{slot}-0` at root, extended with `{parent}-{slot}-0` for nested layouts.
|
|
- `40da0d8` — Deterministic attribute sorting and thread-safe nested layout cloning (clone-on-render pattern).
|
|
- `f49ddbf` — `Attr()` helper for setting element attributes with chaining.
|
|
- `e041f76` — `Responsive` multi-variant compositor with `data-variant` containers.
|
|
- `8ac5123` — `StripTags` single-pass rune scanner for HTML-to-text stripping.
|
|
- `76cef5a` — `Imprint()` full render-reverse-imprint pipeline using `go-i18n/reversal`.
|
|
- `ef77793` — `CompareVariants()` pairwise semantic similarity scoring across responsive variants.
|
|
|
|
## Phase 3: WASM Entry Point
|
|
|
|
Commits: `456adce` through `9bc1fa7`
|
|
|
|
- `456adce` — Makefile with `wasm` target. Size gate: `WASM_GZ_LIMIT = 1048576` (1 MB). Initial measurement revealed the binary was already too large at this stage.
|
|
- `5acf63c` — WASM entry point `cmd/wasm/main.go` with `renderToString` exported to `window.gohtml`.
|
|
- `2fab89e` — Integration tests refactored to use `Imprint` pipeline.
|
|
- `e34c5c9` — WASM browser test harness added.
|
|
- `18d2933` — WASM binary size reporting improvements.
|
|
- `9bc1fa7` — Variant name escaping in `Responsive`, single-pass `StripTags` optimisation, WASM security contract documented in source.
|
|
|
|
## Phase 4: Codegen and Web Components
|
|
|
|
Commits: `937c08d` through `ab7ab92`
|
|
|
|
- `37b50ab`, `496513e` — Phase 4 design documents and implementation plan.
|
|
- `937c08d` — `codegen` package with `GenerateClass`, `GenerateBundle`, `TagToClassName`. Web Component classes with closed Shadow DOM.
|
|
- `dcd55a4` — `registerComponents` export added to `cmd/wasm/main.go`, bridging JSON slot config to WC bundle JS. This was the source of the subsequent binary size problem.
|
|
- `ab7ab92` — Transitive `replace` directive added for `go-inference`.
|
|
|
|
## WASM Binary Size Reduction
|
|
|
|
Commits: `6abda8b`, `4c65737`, `aae5d21`
|
|
|
|
The initial WASM binary measured 6.04 MB raw / 1.58 MB gzip — 58% over the 1 MB gzip limit set in the Makefile. The root causes were three heavyweight stdlib imports pulled in by `registerComponents()` in the WASM binary:
|
|
|
|
| Import | Approx. gzip contribution |
|
|
|--------|--------------------------|
|
|
| `encoding/json` | ~200 KB |
|
|
| `text/template` | ~125 KB |
|
|
| `fmt` (via `layout.go`) | ~50 KB |
|
|
| `go-i18n/reversal` (via `pipeline.go`) | ~250 KB |
|
|
|
|
**Total bloat**: ~625 KB gzip over the core rendering requirement.
|
|
|
|
The fix was applied in three distinct steps:
|
|
|
|
### Step 1: Remove registerComponents from WASM (`4c65737`)
|
|
|
|
`cmd/wasm/register.go` received a `//go:build !js` build tag, completely excluding it from the WASM compilation unit. The `registerComponents` entry on the `gohtml` JS object was removed from `cmd/wasm/main.go`. The codegen function was moved to a standalone build-time CLI at `cmd/codegen/main.go`. This eliminated `encoding/json` and `text/template` from the WASM import graph.
|
|
|
|
### Step 2: Remove pipeline from WASM
|
|
|
|
`pipeline.go` received a `//go:build !js` build tag. The `Imprint()` and `CompareVariants()` functions depend on `go-i18n/reversal`, which is a heavyweight analysis library. These functions are server-side analysis tools and have no use in a client-side rendering module. The `renderToString` function in the WASM entry point never called them, so removal was non-breaking.
|
|
|
|
### Step 3: Eliminate fmt from WASM
|
|
|
|
`layout.go`'s `blockID()` method had used `fmt.Sprintf` for string construction. Replacing this with direct string concatenation (`l.path + string(slot) + "-0"`) removed `fmt` from the WASM import graph entirely.
|
|
|
|
**Result**: 2.90 MB raw, 842 KB gzip. 47% reduction in gzip size. Well within the 1 MB limit.
|
|
|
|
### Size gate test (`aae5d21`)
|
|
|
|
`cmd/wasm/size_test.go` was added to prevent regression. `TestWASMBinarySize_Good` builds the WASM binary in a temp directory, gzip-compresses it, and asserts:
|
|
|
|
- Gzip size < 1,048,576 bytes (1 MB).
|
|
- Raw size < 3,145,728 bytes (3 MB).
|
|
|
|
The test is skipped under `go test -short` and is guarded with `//go:build !js`.
|
|
|
|
## Test Coverage Milestones
|
|
|
|
- `7efd2ab` — Benchmarks added across all subsystems. Unicode edge case tests. Stress tests.
|
|
- `ab7ab92` — 53 passing tests across the package and sub-packages.
|
|
- `aae5d21` — 70+ tests passing (server-side); WASM size gate and codegen CLI tests added.
|
|
|
|
## Known Limitations (as of current HEAD)
|
|
|
|
These are not regressions; they are design choices or deferred work recorded for future consideration.
|
|
|
|
1. **Invalid layout variants are silent.** `NewLayout("XYZ")` produces empty output. No error, no warning. Adding validation would require changing the return type of `NewLayout` from `*Layout` to `(*Layout, error)`, which is a breaking API change.
|
|
|
|
2. **No WASM integration test.** `cmd/wasm/size_test.go` tests binary size only. The `renderToString` behaviour is tested by building and running the WASM binary in a browser, not by an automated test. A `syscall/js`-compatible test harness would be needed.
|
|
|
|
3. **Responsive accepts only Layout.** `Responsive.Variant()` takes `*Layout` rather than `Node`. The rationale is that `CompareVariants` in the pipeline needs access to the slot structure. Accepting `Node` would require a different approach to variant analysis.
|
|
|
|
4. **Context.service is private.** The i18n service cannot be set after construction or swapped. This is a conservative choice; relaxing it requires deciding whether mutation should be safe for concurrent use.
|
|
|
|
5. **TypeScript definitions not generated.** `codegen.GenerateBundle()` produces JS only. A `.d.ts` companion would benefit TypeScript consumers of the generated Web Components.
|
|
|
|
6. **No CSS scoping helper.** Responsive variants are identified by `data-variant` attributes. Targeting them from CSS requires knowledge of the attribute name. An optional utility for generating scoped CSS selectors is deferred.
|
|
|
|
7. **Browser polyfill matrix not documented.** Closed Shadow DOM is well-supported but older browsers require polyfills. The support matrix is not documented.
|
|
|
|
## Future Considerations
|
|
|
|
These items were captured during the WASM size reduction work and expert review sessions. They are not committed work items.
|
|
|
|
- **TypeScript type definitions** alongside `GenerateBundle()` for typed Web Component consumers.
|
|
- **Accessibility helpers** — `aria-label` builder, `alt` text helpers, focus management nodes. The layout has semantic HTML and ARIA roles but no API for fine-grained accessibility attributes beyond `Attr()`.
|
|
- **Layout variant validation** — return a warning or sentinel error from `NewLayout` when the variant string contains unrecognised slot characters.
|
|
- **Daemon mode for codegen** — watch mode for regenerating the JS bundle when slot config changes, for development workflows.
|