Files
alkgen/tasks/implementation/w1-render-entry.md
T
glm-5.3-flash e361b4181a feat(render): add the TemplateSet render entry point (both sinks, deterministic, strict)
- public TemplateSet::render (String sink) and
  TemplateSet::render_to_write (io::Write sink), synchronous and
  deterministic, both surfacing the wave-1 Error enum
- render-name rule enforced behaviorally: the entry resolves names
  against the set-level bookkeeping before the substrate is
  consulted, so a partial's name or an unknown name is a typed
  Error::Render (substrate TemplateNotFound reason), never a
  rendered partial
- strict-mode missing variable is a typed error in both sinks,
  never silently-empty output; the write sink's streaming
  (non-atomic) semantics on mid-render failure documented
- determinism pinned by tests: byte-identical repeated renders,
  byte-identical output across registry rebuilds in opposite
  registration order, and byte-for-byte agreement between sinks

Verification: cargo test (19 passed), cargo clippy --all-targets
-- -D warnings, cargo fmt --check, cargo doc --no-deps, and
cargo build --target wasm32-unknown-unknown --release all green.
2026-11-06 20:01:57 +00:00

6.6 KiB

id, name, status, depends_on, scope, risk, impact, level
id name status depends_on scope risk impact level
w1-render-entry Wave 1: the render entry point (both sinks, deterministic, strict) completed
w1-registry-construction
narrow low project implementation

Description

Land the render entry point (core.md §Render entry point), the component every consumer actually calls:

  • Synchronous and deterministic: render a registered template by name over a serde-serializable context. Same registry contents plus same context produce byte-identical text (ADR-003): test-pinned here (repeated renders over cloned and rebuilt registries), deepened by wave 2's battery. No I/O, no time reads, no hash-map iteration order leaking into output.
  • Two sinks: render into a String, and render into an io::Write target (both wasm-clean; the substrate exposes both, render and render_to_write; the core passes them through so a large page or large generated file need not force intermediate allocation). Strict errors through both sinks surfacing the wave-1 error enum.
  • Templates are the render targets: rendering by a partial's name is rejected per the wave-1 registry task's recorded mechanism (bookkeeping or convention plus tests; whatever landed there, the render entry is where the behavior is visible and tested).
  • Strict errors: a missing context variable in strict mode is a typed error through the public enum, never silent-empty output (ADR-002 decision 2).
  • src/lib.rs re-exports the surface.

Acceptance Criteria

  • String-sink and io::Write-sink render entry points, both surfacing the public error enum
  • determinism pinned by tests (byte-identical repeated renders, including across registry rebuilds)
  • strict-mode missing-variable render is a typed error in both sinks, never empty output
  • render-by-partial behavior matches the registry task's recorded decision and is pinned by a test
  • cargo test, cargo clippy --all-targets -- -D warnings, cargo fmt --check pass

References

  • docs/architecture/core.md §Render entry point
  • docs/architecture/decisions/002-handlebars-substrate-strict-mode.md
  • docs/architecture/decisions/003-core-posture-sync-wasm-deterministic.md
  • docs/architecture/emit.md §Cross-posture obligations (the determinism row every posture leans on)
  • /workspace/@alkdev/alkminer/reference/alkminer-v1/src/compute/ kernel.rs (render-then-compile precedent: purity for downstream caching)

Notes

Agent fills this during implementation.

Landed surface. Two public methods on TemplateSet (src/registry.rs): render<T: serde::Serialize>(name, context) -> Result<String, Error> and render_to_write<T: serde::Serialize, W: std::io::Write>(name, context, writer) -> Result<(), Error>. Both are synchronous, &self, and wrap the substrate's render / render_to_write (registry.rs:786, :804 in the vendored checkout); both surface the wave-1 Error enum, mapping the substrate RenderError through the existing #[from] arm. No new dependency, no new error variant, no change to lib.rs re-exports (the entry points are methods on the already-re-exported TemplateSet). The #[cfg(test)] render shim the registry task left behind is removed; the earlier registry tests now exercise the public entry.

Render-name rule enforcement (the registry task's decision 1, behavioral half). The substrate cannot enforce the rule (register_partial is literally register_template_string, its registry is one namespace), so the render entry resolves the name against the set-level bookkeeping (template_names) before the substrate is consulted. A name registered as a partial, or never registered, is rejected as Error::Render wrapping the substrate's RenderErrorReason::TemplateNotFound, constructed through the public non-deprecated path (From<RenderErrorReason> for RenderError). Unknown names surface through the same guard, which also removes any chance of the substrate's own template-not-found path diverging. Pinned by render_by_partial_name_and_unknown_name_are_rejected_typed, covering both sinks.

Strict errors through both sinks. A missing context variable fails both sinks as Error::Render, never silent empty output: pinned by strict_missing_variable_is_a_typed_error_in_both_sinks (the string sink must return Err, never an empty Ok). Recorded interpretation about the write sink: the substrate streams to the writer incrementally, so a render that fails partway may leave bytes already written to the target before the typed error returns. This is documented in render_to_write's doc comment as streaming semantics (consumers wanting atomicity buffer first); the test pins the typed error, not the exact partial byte count, to avoid pinning substrate internals.

Determinism. Pinned by two tests: a fixture set (template + partial + {{#each}} loop) renders byte-identical across five repeated renders and across a rebuilt registry where the same registrations land in the opposite order (no registration order leaks into output); the write sink agrees with the string sink byte for byte. No time, no I/O, no hash-map iteration order in the path: the only additions are two method bodies delegating to the substrate plus the name guard.

Verification (all green). cargo test (19 passed), cargo clippy --all-targets -- -D warnings, cargo fmt --check, cargo doc --no-deps (warning-free; one broken intra-doc link during development fixed), cargo build --target wasm32-unknown-unknown --release. No em dashes in the added prose.

Summary

Agent fills this on completion.

Render entry point landed as the public surface of TemplateSet (src/registry.rs): render (String sink) and render_to_write (io::Write sink), synchronous and deterministic, both surfacing the wave-1 Error enum. The render-name rule from the registry task's recorded mechanism is now behavior: the entry resolves names against the set-level bookkeeping before the substrate is consulted, so a partial's name (or a never-registered name) is a typed Error::Render in both sinks, never a rendered partial. Strict mode makes a missing variable a typed error in both sinks, never empty output; the write sink's streaming (non-atomic) semantics on mid-render failure are documented at the method. Determinism is pinned by tests: byte-identical repeated renders, byte-identical output across a rebuilt registry registering in the opposite order, and byte-for-byte agreement between the two sinks. Verification: 19 tests pass, clippy -D warnings clean, fmt --check clean, cargo doc --no-deps clean, wasm release build to wasm32-unknown-unknown clean.