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>
7.6 KiB
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,Nodeinterface withRender(ctx *Context) string.3e76e72—Textnode wired togo-i18ngrammar 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—Layouttype 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}-0at root, extended with{parent}-{slot}-0for 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—Responsivemulti-variant compositor withdata-variantcontainers.8ac5123—StripTagssingle-pass rune scanner for HTML-to-text stripping.76cef5a—Imprint()full render-reverse-imprint pipeline usinggo-i18n/reversal.ef77793—CompareVariants()pairwise semantic similarity scoring across responsive variants.
Phase 3: WASM Entry Point
Commits: 456adce through 9bc1fa7
456adce— Makefile withwasmtarget. Size gate:WASM_GZ_LIMIT = 1048576(1 MB). Initial measurement revealed the binary was already too large at this stage.5acf63c— WASM entry pointcmd/wasm/main.gowithrenderToStringexported towindow.gohtml.2fab89e— Integration tests refactored to useImprintpipeline.e34c5c9— WASM browser test harness added.18d2933— WASM binary size reporting improvements.9bc1fa7— Variant name escaping inResponsive, single-passStripTagsoptimisation, 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—codegenpackage withGenerateClass,GenerateBundle,TagToClassName. Web Component classes with closed Shadow DOM.dcd55a4—registerComponentsexport added tocmd/wasm/main.go, bridging JSON slot config to WC bundle JS. This was the source of the subsequent binary size problem.ab7ab92— Transitivereplacedirective added forgo-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.
-
Invalid layout variants are silent.
NewLayout("XYZ")produces empty output. No error, no warning. Adding validation would require changing the return type ofNewLayoutfrom*Layoutto(*Layout, error), which is a breaking API change. -
No WASM integration test.
cmd/wasm/size_test.gotests binary size only. TherenderToStringbehaviour is tested by building and running the WASM binary in a browser, not by an automated test. Asyscall/js-compatible test harness would be needed. -
Responsive accepts only Layout.
Responsive.Variant()takes*Layoutrather thanNode. The rationale is thatCompareVariantsin the pipeline needs access to the slot structure. AcceptingNodewould require a different approach to variant analysis. -
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.
-
TypeScript definitions not generated.
codegen.GenerateBundle()produces JS only. A.d.tscompanion would benefit TypeScript consumers of the generated Web Components. -
No CSS scoping helper. Responsive variants are identified by
data-variantattributes. Targeting them from CSS requires knowledge of the attribute name. An optional utility for generating scoped CSS selectors is deferred. -
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-labelbuilder,alttext helpers, focus management nodes. The layout has semantic HTML and ARIA roles but no API for fine-grained accessibility attributes beyondAttr(). - Layout variant validation — return a warning or sentinel error from
NewLayoutwhen 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.