> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/primitives/templates. # Tasks: enterprise-templates ## 1. Scaffold - [x] 1.1 Create `packages/universal/templates/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton - [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul - [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed-imports, tree-shake - [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml` ## 2. Error types - [x] 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` - [x] 2.2 Implement `src/errors/plugin-error.ts` — `PluginError` extends `TemplateError` with `pluginName` and `match` fields; code `TEMPLATE.PLUGIN_ERROR` - [x] 2.3 Implement `src/errors/max-depth-error.ts` — `MaxDepthError` extends `TemplateError` with `depth` and `maxDepth` fields; code `TEMPLATE.MAX_DEPTH` - [x] 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` - [x] 2.5 Tests: error construction, code narrowing, field access, `instanceof` hierarchy checks - [x] 2.6 Co-located `.md` for each error file ## 3. Types and Zod schemas - [x] 3.1 Implement `src/types/template.types.ts` — all type and enum exports: `TemplateContext`, `PluginPriority`, `ParseState`, `PluginMatch`, `PluginResult`, `SKIP_PLUGIN`, `Plugin`, `PluginPreset`, `TemplateEngineOptions`, `ParseOptions`, `CompiledTemplate` - [x] 3.2 Implement `src/types/schemas.ts` — `TemplateEngineOptionsSchema`, `ParseOptionsSchema`, `PluginPrioritySchema` using Zod; all schemas exported for consumer validation - [x] 3.3 Tests: schema parse/safeParse on valid and invalid inputs; type inference smoke tests - [x] 3.4 Co-located `.md` for types and schemas files ## 4. ParseState - [x] 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 - [x] 4.2 Unit tests: initial state shape, counter CRUD, clone independence, depth tracking - [x] 4.3 Co-located `src/engine/parse-state.md` ## 5. TemplateEngine core - [x] 5.1 Implement `src/engine/template-engine.ts` — `TemplateEngine` 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[]` - `.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` — async variant; same instrumentation - `.compile(template)` → `CompiledTemplate` — 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 - [x] 5.2 Unit tests: plugin registration order, priority sorting, conflict detection, `throwOnError: false` path, `onError` callback, `compile` reuse, depth guard, sync async parity - [x] 5.3 Co-located `src/engine/template-engine.md` ## 6. Variable resolution plugins - [x] 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) - [x] 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 - [x] 6.3 Unit tests: happy path, missing path (SKIP_PLUGIN), nested arrays, type coercion to string - [x] 6.4 Co-located `.md` for each plugin file ## 7. Transform plugins - [x] 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 - [x] 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 - [x] 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 - [x] 7.4 Unit tests: all case variants, regex replace + callback, split happy path, out-of-range index - [x] 7.5 Co-located `.md` for each plugin file ## 8. Dynamic plugins - [x] 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 - [x] 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 - [x] 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 - [x] 8.4 Unit tests: counter increment and padding, counter state isolation per `ParseState`, datetime with `FakeClock`, uuid determinism with fake `Rng`, nanoid length - [x] 8.5 Co-located `.md` for each plugin file ## 9. Control flow plugins - [x] 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` - [x] 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 - [x] 9.3 Unit tests: conditional truthy/falsy, nested conditionals, loop over array, loop with empty array, loop with non-array, loop inner variable access - [x] 9.4 Co-located `.md` for each plugin file ## 10. Utilities - [x] 10.1 Implement `src/utils/escape.ts`: - `escapeRegex(str)` → `string` - `isEscaped(template, index)` → `boolean` - `unescapeTemplate(str)` → `string` - [x] 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` - [x] 10.3 Implement `src/utils/get-nested-value.ts`: - `getNestedValue(obj, path)` → `unknown` - `hasNestedPath(obj, path)` → `boolean` - `setNestedValue(obj, path, value)` → `void` - [x] 10.4 Unit tests: escape round-trips, formatDate token coverage, UUID format validation, nanoid length/alphabet, nested get/has/set CRUD - [x] 10.5 Co-located `.md` for each utility file ## 11. Presets - [x] 11.1 Implement `src/presets/minimal.ts` — `minimalPreset()` → `PluginPreset`: dot-notation + array-index - [x] 11.2 Implement `src/presets/standard.ts` — `standardPreset()` → `PluginPreset`: dot-notation, array-index, case-transform, counter, datetime, uuid - [x] 11.3 Implement `src/presets/full.ts` — `fullPreset()` → `PluginPreset`: standard + regex + string-split + conditional; `fullPresetWithLoops()` → `PluginPreset`: full + loop - [x] 11.4 Unit tests: each preset registers expected plugin names in correct priority order; preset plugins are independent instances (not shared state) - [x] 11.5 Co-located `.md` for each preset file ## 12. OTel instrumentation - [x] 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 - [x] 12.2 Wire instrumentation into `TemplateEngine.parse()`, `parseAsync()`, and `applyPlugin()` / `applyPluginAsync()` - [x] 12.3 Tests: no-op without exporter (metrics silently swallowed); instrument names and attribute keys match spec - [x] 12.4 Co-located `src/instrumentation/metrics.md` ## 13. Property-based tests and benchmarks - [x] 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) - [x] 13.2 `src/__tests__/property/counter-monotone.property.test.ts` — fast-check: counter value never decreases across successive `.parse()` calls within a `ParseState` - [x] 13.3 `src/__tests__/property/nested-value.property.test.ts` — fast-check: `getNestedValue` after `setNestedValue` round-trips for safe dot-paths - [x] 13.4 `src/__benchmarks__/render.bench.ts` — 100-variable dot-notation template, 10 000 iterations (mitata) - [x] 13.5 `src/__benchmarks__/plugin-overhead.bench.ts` — single no-op plugin on 1-token template, 100 000 iterations (mitata) - [x] 13.6 `src/__benchmarks__/format.bench.ts` — `formatDate` and `getNestedValue` 5-level deep, 100 000 iterations each (mitata) - [x] 13.7 Publish baseline benchmark numbers in `README.md` ## 14. Docs - [x] 14.1 Co-located `.md` for every implementation file (≥200 words, both simple and advanced examples, full API reference section) - [x] 14.2 Rewrite `README.md` with: public API table, preset comparison table, plugin authoring guide, OTel attribute reference, injectable clock/RNG guide, benchmark numbers - [x] 14.3 `bun run docs:check --filter=@suaveplan/templates` ≥ 90% ## 15. Verify and publish - [x] 15.1 `bunx turbo lint typecheck test --filter=@suaveplan/templates` green - [x] 15.2 `bunx turbo test:coverage --filter=@suaveplan/templates` at 100/100/100/100 - [x] 15.3 `bunx turbo build --filter=@suaveplan/templates` green - [x] 15.4 `bun scripts/publish.ts --package @suaveplan/templates` to Verdaccio at 0.0.0 - [x] 15.5 Fold spec delta into `openspec/specs/templates/spec.md` - [x] 15.6 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-templates/`