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

12 KiB

Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/primitives/templates.

Tasks: enterprise-templates

1. Scaffold

  • 1.1 Create packages/universal/templates/ with package.json, tsconfig.json, tsconfig.lib.json, vite.config.ts, vitest.config.ts, README.md skeleton
  • 1.2 Register coverage thresholds at 100/100/100/100 in vitest.config.ts; provider = istanbul
  • 1.3 Add cross-cutting guard tests in tests/: dep-graph, no-disallowed-imports, tree-shake
  • 1.4 Add package to root CLAUDE.md, README.md, and STRUCTURE.yml

2. Error types

  • 2.1 Implement src/errors/template-error.ts — TemplateError base extends SuaveplanError; defineCodes for TEMPLATE.PARSE_ERROR, TEMPLATE.MISSING_VARIABLE, TEMPLATE.PLUGIN_CONFLICT, TEMPLATE.RENDER_ERROR
  • 2.2 Implement src/errors/plugin-error.ts — PluginError extends TemplateError with pluginName and match fields; code TEMPLATE.PLUGIN_ERROR
  • 2.3 Implement src/errors/max-depth-error.ts — MaxDepthError extends TemplateError with depth and maxDepth fields; code TEMPLATE.MAX_DEPTH
  • 2.4 Implement src/errors/error-codes.ts — single defineCodes call registering all TEMPLATE.* codes; auto-registered via side-effect import in src/index.ts
  • 2.5 Tests: error construction, code narrowing, field access, instanceof hierarchy checks
  • 2.6 Co-located .md for each error file

3. Types and Zod schemas

  • 3.1 Implement src/types/template.types.ts — all type and enum exports: TemplateContext, PluginPriority, ParseState, PluginMatch, PluginResult, SKIP_PLUGIN, Plugin<TContext>, PluginPreset<TContext>, TemplateEngineOptions, ParseOptions, CompiledTemplate<TContext>
  • 3.2 Implement src/types/schemas.ts — TemplateEngineOptionsSchema, ParseOptionsSchema, PluginPrioritySchema using Zod; all schemas exported for consumer validation
  • 3.3 Tests: schema parse/safeParse on valid and invalid inputs; type inference smoke tests
  • 3.4 Co-located .md for types and schemas files

4. ParseState

  • 4.1 Implement src/engine/parse-state.ts:
    • createParseState(maxDepth?) → ParseState
    • cloneParseState(state) → ParseState — deep-clones counters, cache, custom maps
    • clearCounters(state) → void
    • getCounter(state, name) → number
    • resetCounter(state, name) → void
    • incrementDepth(state) → void — internal helper used by engine
  • 4.2 Unit tests: initial state shape, counter CRUD, clone independence, depth tracking
  • 4.3 Co-located src/engine/parse-state.md

5. TemplateEngine core

  • 5.1 Implement src/engine/template-engine.ts — TemplateEngine<TContext> class:
    • constructor(options?: TemplateEngineOptions) — validates options via TemplateEngineOptionsSchema
    • .use(plugin) → this — registers plugin; detects name conflicts; re-sorts by priority descending; emits template.plugin.calls.total instrument setup
    • .usePreset(preset) → this
    • .remove(name) → this
    • .getPlugins() → readonly Plugin<TContext>[]
    • .parse(template, context, options?) → string — initializes plugins, calls parseInternal, runs cleanup; records template.render.total and template.render.duration_ms; wraps errors in TemplateError; honours throwOnError option
    • .parseAsync(template, context, options?) → Promise<string> — async variant; same instrumentation
    • .compile(template) → CompiledTemplate<TContext> — returns { source, parse, parseAsync }
    • Internal parseInternal / parseInternalAsync — depth-guard, plugin application loop with SKIP_PLUGIN handling
    • Internal applyPlugin / applyPluginAsync — collect all matches first, apply replacements in reverse-index order
  • 5.2 Unit tests: plugin registration order, priority sorting, conflict detection, throwOnError: false path, onError callback, compile reuse, depth guard, sync async parity
  • 5.3 Co-located src/engine/template-engine.md

6. Variable resolution plugins

  • 6.1 Implement src/plugins/variables/dot-notation-plugin.ts — createDotNotationPlugin() → Plugin:
    • Pattern: {path.to.value} using getNestedValue
    • Priority: PluginPriority.LOW
    • Returns SKIP_PLUGIN when path not found (does not throw)
  • 6.2 Implement src/plugins/variables/array-index-plugin.ts — createArrayIndexPlugin() → Plugin:
    • Pattern: {name[index]} — zero-based array element lookup
    • Priority: PluginPriority.LOW
    • Returns SKIP_PLUGIN when array or index not found
  • 6.3 Unit tests: happy path, missing path (SKIP_PLUGIN), nested arrays, type coercion to string
  • 6.4 Co-located .md for each plugin file

7. Transform plugins

  • 7.1 Implement src/plugins/transform/case-transform-plugin.ts — createCaseTransformPlugin() → Plugin:
    • Pattern: {value|upper}, {value|lower}, {value|title}, {value|camel}, {value|snake}, {value|kebab}
    • Priority: PluginPriority.MEDIUM
    • Value is resolved from context via dot-notation before transform is applied
  • 7.2 Implement src/plugins/transform/regex-plugin.ts — createRegexPlugin(patterns) → Plugin:
    • Accepts array of { pattern: RegExp; replacement: string | ((match: string) => string) } entries
    • Priority: PluginPriority.MEDIUM
    • Applied in order; caller is responsible for pattern safety
  • 7.3 Implement src/plugins/transform/string-split-plugin.ts — createStringSplitPlugin() → Plugin:
    • Pattern: {value|split:sep:index}
    • Priority: PluginPriority.MEDIUM
    • Returns empty string when index out of range
  • 7.4 Unit tests: all case variants, regex replace + callback, split happy path, out-of-range index
  • 7.5 Co-located .md for each plugin file

8. Dynamic plugins

  • 8.1 Implement src/plugins/dynamic/counter-plugin.ts — createCounterPlugin() → Plugin:
    • Pattern: {_counter:name:start:pad} — start default 1, pad default 0
    • Priority: PluginPriority.HIGH
    • Counter stored in ParseState.counters; increments each match; zero-pads to pad width
  • 8.2 Implement src/plugins/dynamic/datetime-plugin.ts — createDateTimePlugin(opts?) → Plugin:
    • opts?: { clock?: Clock; defaultPattern?: string }
    • Pattern: {_datetime:pattern} — uses formatDate; pattern defaults to YYYY-MM-DD when not supplied
    • Priority: PluginPriority.HIGH
    • Clock from @suaveplan/types; falls back to Date.now() when no clock injected
  • 8.3 Implement src/plugins/dynamic/uuid-plugin.ts — createUuidPlugin(opts?) → Plugin:
    • opts?: { rng?: Rng }
    • Patterns: {_uuid} → UUID v4; {_nanoid:length} → nano ID of given length
    • Priority: PluginPriority.HIGH
    • Rng from @suaveplan/types; falls back to crypto.randomUUID / Math.random when not injected
  • 8.4 Unit tests: counter increment and padding, counter state isolation per ParseState, datetime with FakeClock, uuid determinism with fake Rng, nanoid length
  • 8.5 Co-located .md for each plugin file

9. Control flow plugins

  • 9.1 Implement src/plugins/control/conditional-plugin.ts — createConditionalPlugin() → Plugin:
    • Pattern: {condition}...{/condition} — condition key resolved from context; truthy keeps block, falsy removes it
    • Priority: PluginPriority.CRITICAL
    • Supports dot-notation conditions: {user.isAdmin}...{/user.isAdmin}
    • Nested conditionals supported up to ParseState.maxDepth
  • 9.2 Implement src/plugins/control/loop-plugin.ts — createLoopPlugin() → Plugin:
    • Pattern: {loop:items}...{/loop} — items resolved to array from context; block rendered once per element with element bound as {item}
    • Priority: PluginPriority.CRITICAL
    • Non-array or missing values render as empty string
  • 9.3 Unit tests: conditional truthy/falsy, nested conditionals, loop over array, loop with empty array, loop with non-array, loop inner variable access
  • 9.4 Co-located .md for each plugin file

10. Utilities

  • 10.1 Implement src/utils/escape.ts:
    • escapeRegex(str) → string
    • isEscaped(template, index) → boolean
    • unescapeTemplate(str) → string
  • 10.2 Implement src/utils/format.ts:
    • formatDate(date, pattern) → string — tokens: YYYY MM DD HH mm ss SSS
    • generateUuid(rng?) → string — uses injected Rng or crypto.randomUUID
    • generateNanoid(length?, rng?) → string — URL-safe alphabet; uses injected Rng or Math.random
    • pad(value, length, char?, left?) → string
  • 10.3 Implement src/utils/get-nested-value.ts:
    • getNestedValue(obj, path) → unknown
    • hasNestedPath(obj, path) → boolean
    • setNestedValue(obj, path, value) → void
  • 10.4 Unit tests: escape round-trips, formatDate token coverage, UUID format validation, nanoid length/alphabet, nested get/has/set CRUD
  • 10.5 Co-located .md for each utility file

11. Presets

  • 11.1 Implement src/presets/minimal.ts — minimalPreset() → PluginPreset: dot-notation + array-index
  • 11.2 Implement src/presets/standard.ts — standardPreset() → PluginPreset: dot-notation, array-index, case-transform, counter, datetime, uuid
  • 11.3 Implement src/presets/full.ts — fullPreset() → PluginPreset: standard + regex + string-split + conditional; fullPresetWithLoops() → PluginPreset: full + loop
  • 11.4 Unit tests: each preset registers expected plugin names in correct priority order; preset plugins are independent instances (not shared state)
  • 11.5 Co-located .md for each preset file

12. OTel instrumentation

  • 12.1 Implement src/instrumentation/metrics.ts:
    • Counter template.render.total — attributes: outcome (success | error)
    • Histogram template.render.duration_ms
    • Counter template.plugin.calls.total — attributes: plugin_name, outcome (success | skip | error)
    • All instruments obtained from @opentelemetry/api meter; no-op when no exporter registered
  • 12.2 Wire instrumentation into TemplateEngine.parse(), parseAsync(), and applyPlugin() / applyPluginAsync()
  • 12.3 Tests: no-op without exporter (metrics silently swallowed); instrument names and attribute keys match spec
  • 12.4 Co-located src/instrumentation/metrics.md

13. Property-based tests and benchmarks

  • 13.1 src/__tests__/property/idempotent-render.property.test.ts — fast-check: re-parsing rendered output with same context returns same string (dot-notation + standard plugins, no counter/uuid)
  • 13.2 src/__tests__/property/counter-monotone.property.test.ts — fast-check: counter value never decreases across successive .parse() calls within a ParseState
  • 13.3 src/__tests__/property/nested-value.property.test.ts — fast-check: getNestedValue after setNestedValue round-trips for safe dot-paths
  • 13.4 src/__benchmarks__/render.bench.ts — 100-variable dot-notation template, 10 000 iterations (mitata)
  • 13.5 src/__benchmarks__/plugin-overhead.bench.ts — single no-op plugin on 1-token template, 100 000 iterations (mitata)
  • 13.6 src/__benchmarks__/format.bench.ts — formatDate and getNestedValue 5-level deep, 100 000 iterations each (mitata)
  • 13.7 Publish baseline benchmark numbers in README.md

14. Docs

  • 14.1 Co-located .md for every implementation file (≥200 words, both simple and advanced examples, full API reference section)
  • 14.2 Rewrite README.md with: public API table, preset comparison table, plugin authoring guide, OTel attribute reference, injectable clock/RNG guide, benchmark numbers
  • 14.3 bun run docs:check --filter=@suaveplan/templates ≥ 90%

15. Verify and publish

  • 15.1 bunx turbo lint typecheck test --filter=@suaveplan/templates green
  • 15.2 bunx turbo test:coverage --filter=@suaveplan/templates at 100/100/100/100
  • 15.3 bunx turbo build --filter=@suaveplan/templates green
  • 15.4 bun scripts/publish.ts --package @suaveplan/templates to Verdaccio at 0.0.0
  • 15.5 Fold spec delta into openspec/specs/templates/spec.md
  • 15.6 Archive OpenSpec change to openspec/archive/2026-04-19-enterprise-templates/