Files
2026-05-27 23:29:53 +01:00

4.3 KiB

OpenSpec Change: templates-spec-conformance

  • Change ID: templates-spec-conformance
  • Package: @suaveplan/templates
  • Status: PROPOSED
  • Date: 2026-04-26

Summary

Bring @suaveplan/templates into spec conformance with openspec/specs/templates/spec.md. Closes the parsing gap (the previous spec used ### <Section> plus REQ-TPL-NNN bullets so the conformance parser saw zero requirements), wires the missing OpenTelemetry instruments the spec mandated but the engine never emitted, and adds boundary validation of TemplateEngineOptions / ParseOptions so the exported Zod schemas are actually called by the engine.

Current state

@suaveplan/templates ships an enterprise-grade plugin engine (TemplateEngine<TContext>, four preset families, ten plugins, a structured error hierarchy backed by defineCodes("TEMPLATE", …), fast-check property tests, and a mitata bench suite). The spec gaps are mechanical / observability:

  • The folded spec used non-parseable section headings (### Package identity, ### Dependencies, …) so bun scripts/openspec-conformance.ts @suaveplan/templates reported "0 requirements parsed".
  • The constructor advertised TemplateEngineOptionsSchema but never invoked it; same for ParseOptionsSchema. Both schemas were exported yet had zero non-test call sites.
  • The OTel contract (template.render.total, template.render.duration_ms, template.plugin.calls.total) was specced but the engine had no @opentelemetry/api import at all.
  • tasks.md of 2026-04-19-enterprise-templates/ carried the bulk-archive marker (> Archived YYYY-MM-DD: all tasks …) banned by AGENTS.md section 12.
  • Several behaviours were exercised only indirectly: ParseState clone independence, reverse-index splice ordering inside applyPlugin, throwOnError=false → onError forwarding, Clock / Rng injection on dynamic plugins, and preset super-set relationships.

What changes

  1. Add @opentelemetry/api as a runtime dependency (catalog:observability).
  2. Add src/otel/templates-otel.ts exporting recordRender, recordPluginCall, startRenderTimer, and _resetInstruments. Wire it from TemplateEngine.parse / parseAsync (render counter + duration histogram) and from applyPlugin / applyPluginAsync (plugin counter, attribute outcome ∈ {"success","skip","error"}).
  3. TemplateEngine constructor now calls TemplateEngineOptionsSchema.safeParse(options) and throws TemplateError(TEMPLATE.PARSE_ERROR) on failure. parse / parseAsync call ParseOptionsSchema.safeParse(options) when options are passed.
  4. Mark PluginPrioritySchema as // schema: boundary-only (consumer facing only; the engine accepts any numeric priority).
  5. Rewrite openspec/specs/templates/spec.md from ### Package identity + REQ-TPL-NNN bullets to fourteen ### TPL-NN — name requirement headings, each with a **Verified by:** line that names the specific test case.
  6. Add seven *.spec-conformance.test.ts files pinning the implicit scenarios:
    • src/__tests__/package-shape.spec-conformance.test.ts — TPL-01 / TPL-02.
    • src/__tests__/types.spec-conformance.test.ts — TPL-04.
    • src/engine/ParseState.spec-conformance.test.ts — TPL-05.
    • src/engine/TemplateEngine.spec-conformance.test.ts — TPL-06 / TPL-07 / TPL-09.
    • src/engine/apply-plugin.spec-conformance.test.ts — TPL-08.
    • src/errors/template-error.spec-conformance.test.ts — TPL-03.
    • src/otel/templates-otel.spec-conformance.test.ts — TPL-13.
    • src/plugins/plugins.spec-conformance.test.ts — TPL-10.
    • src/presets/presets.spec-conformance.test.ts — TPL-11.
  7. Replace the retroactively ticked line in archive/2026-04-19-enterprise-templates/tasks.md with prose pointing at this change folder.

Impact

  • Public API gains recordRender, recordPluginCall, startRenderTimer, _resetInstruments, RenderOutcome, PluginOutcome re-exports. No removed exports.
  • TemplateEngine constructor now validates options at the boundary — invalid configs throw TemplateError(TEMPLATE.PARSE_ERROR) instead of silently coercing.
  • Nine new test files (~600 lines combined).
  • One new module + sibling .md (src/otel/).
  • @opentelemetry/api added as runtime dep; ships no-op when no MeterProvider is registered.