Files
suaveplan/openspec/archive/2026-05-11-enterprise-rules-migration/proposal.md
2026-05-27 23:29:53 +01:00

15 KiB

Enterprise migration — @suaveplan/rules + @suaveplan/rules-hono

Package: @suaveplan/rules, @suaveplan/rules-hono Status: PROPOSED Type: new-package (migration + enterprise harden)

Why

@suaveplan/rules is on the genesis Pending list (workflow plugin). Upstream ships a 63-file, ~17.5k-LOC rule engine (rule engine + builder + context + 30+ operators + 3 validator families + serializer + optimizer + action subsystem + event bus + Hono middleware) at 92.87% coverage and no Zod/OTel/SuaveplanError integration. Genesis ships every package at the bar set by crypto-rsa / webauthn / srp: 100% coverage, sibling .md per impl, Zod at every boundary, OTel hot-path instrumentation, defineCodes error registry extending SuaveplanError, injectable Clock/Rng, AbortSignal threaded through every async surface. This migration brings the package across at that bar in a single change rather than landing a parity port and a follow-up harden.

The Hono middleware is split into a sibling workflow-tier package @suaveplan/rules-hono. The core rules engine stays universal-tier with zero Node/browser/framework deps; the Hono integration lives next to its peer so consumers who do not use Hono pay nothing for it. This is the same split pattern used for crypto-rsa / crypto-bin / crypto-cli (core primitive vs. framework consumer).

What changes

@suaveplan/rules (universal-tier core)

  • ADDED: RuleEngine — async evaluator with evaluate / evaluateAll / evaluateRuleset / check, depth limit, evaluation cache, AbortSignal, OTel spans + histograms, error results carrying RulesError codes
  • ADDED: RuleBuilder / FieldBuilder fluent DSL — rule() / condition() / and() / or() / not() / group() factory functions
  • ADDED: ContextBuilder / DefaultContextProvider / DefaultFieldResolver / createRequestContext / createScopedContext / getField / hasField / context / contextWith
  • ADDED: Action / ActionRegistry / ActionExecutor with createAction / createActionRegistry / createActionExecutor / validateActionConfig / builtInHandlers (log/set/delay/noop), timeout + AbortSignal, sequential + parallel execution
  • ADDED: EventBus / HookManager with createEventBus / createHookManager, bounded history (injectable Clock), priority hooks, waitFor with timeout + abort
  • ADDED: OperatorRegistry + 30 built-in operators (equalsOperator, notEqualsOperator, greaterThanOperator, greaterThanOrEqualsOperator, lessThanOperator, lessThanOrEqualsOperator, betweenOperator, inOperator, notInOperator, containsOperator, notContainsOperator, containsAllOperator, containsAnyOperator, overlapsOperator, startsWithOperator, endsWithOperator, matchesOperator, existsOperator, isNullOperator, isNotNullOperator, isEmptyOperator, isNotEmptyOperator, isTypeOperator, hasPropertyOperator, lengthEqualsOperator, lengthGreaterThanOperator, lengthLessThanOperator, andOperator, orOperator, notOperator, xorOperator) + defaultOperatorRegistry
  • ADDED: SchemaValidator — JSON-schema-style recursive validator with formats (email/url/uuid/date/datetime/regex/ipv4/ipv6), transforms (trim/lowercase/uppercase/parseNumber/parseDate), nested objects, arrays, custom validator hooks
  • ADDED: FieldValidator + field() + 12 preset chainable validators (emailFieldValidator, passwordFieldValidator, usernameFieldValidator, phoneFieldValidator, urlFieldValidator, uuidFieldValidator, dateFieldValidator, integerFieldValidator, ageValidator, currencyValidator, percentageValidator, positiveNumberValidator) + createFieldValidator
  • ADDED: CustomValidator + ValidatorRegistry + 17 built-in validators (emailValidator, urlValidator, uuidValidator, isoDateValidator, creditCardValidator w/ Luhn, phoneValidator, strongPasswordValidator, alphanumericValidator, hexColorValidator, slugValidator, jsonValidator, enumValidator, lengthValidator, patternValidator, rangeValidator, asyncValidator, composeValidators) + createValidator + defaultValidatorRegistry
  • ADDED: Serializer — serializeRule / serializeRuleset / serializeRuleToObject / serializeRulesetToObject / deserializeRule / deserializeRuleset / deserializeRuleFromObject / deserializeRulesetFromObject / cloneRule / hashRule / areRulesEqual / validateSerializedRule with function-placeholder strategy for CustomRule.validate / ComputedRule.compute
  • ADDED: Optimizer — optimizeRule / analyzeRule / estimateCost / findFields / hasAsyncComponents / countRules (flatten nested AND/OR, dedupe, simplify constant branches, sort for short-circuit)
  • ADDED: Type guards — isConditionRule / isLogicalRule / isGroupRule / isCustomRule / isComputedRule
  • ADDED: RulesError extending SuaveplanError with defineCodes("RULES", [...]) registry covering: ENGINE_DEPTH_EXCEEDED, ENGINE_EVALUATION_FAILED, ENGINE_ABORTED, ENGINE_TIMEOUT, OPERATOR_NOT_FOUND, OPERATOR_INVALID, FIELD_RESOLUTION_FAILED, CONTEXT_INVALID, ACTION_TIMEOUT, ACTION_ABORTED, ACTION_HANDLER_FAILED, ACTION_NOT_FOUND, SCHEMA_INVALID, VALIDATOR_INVALID, VALIDATOR_NOT_FOUND, EVENT_TIMEOUT, EVENT_ABORTED, HOOK_INVALID, SERIALIZE_FAILED, DESERIALIZE_FAILED, RULE_INVALID, RULESET_INVALID, OPTIMIZER_FAILED
  • ADDED: Subpath exports — ./engine, ./builder, ./context, ./actions, ./events, ./operators, ./validators/schema, ./validators/field, ./validators/custom, ./serializer, ./optimizer, ./types, ./errors
  • ADDED: OTel surface — rules.engine.evaluate.duration_ms / rules.engine.evaluations_total{outcome} / rules.engine.depth / rules.validator.validate.duration_ms{kind} / rules.validator.validations_total{kind,outcome} / rules.serializer.duration_ms{op} / rules.action.execute.duration_ms / rules.action.executions_total{status} / rules.event.emitted_total{type} / rules.operator.unknown_total{name}

@suaveplan/rules-hono (workflow-tier consumer)

  • ADDED: ruleMiddleware / validationMiddleware / accessControl / responseValidation / rateLimitRule / combineRules over Hono MiddlewareHandler
  • ADDED: RuleMiddlewareOptions / ValidationMiddlewareOptions / AccessControlOptions / HonoValidationError types
  • ADDED: Zod-validated options; default singleton RuleEngine opt-in (not module-level); explicit engine injection encouraged
  • ADDED: Subpath exports — ./middleware, ./types
  • ADDED: OTel spans rules_hono.middleware.<name>.duration_ms{outcome}

Out of scope

  • Storybook / E2E — engine + validators are headless universal-tier; Hono middleware tested via in-process Hono app.request (no browser/E2E surface).
  • @suaveplan/schema-registry integration — enterprise plan §3 lists this; deferring because the schema-registry use case for rules is rule-format evolution across multiple deployed services, not present in genesis-internal consumers yet. Tracked separately when a consumer needs it.
  • Feature flags via @suaveplan/feature-flags — enterprise plan §3 lists this; the engine itself does not have risky-rollout entry points. Consumers compose feature-flags with rules where they need conditional rollouts.
  • Codemod for upstream callers — genesis is a fresh branch; no upstream consumers depend on @suaveplan/rules from genesis yet. CHANGELOG entry covers the API delta for documentation.
  • i18n keying of error messages — error codes are i18n-ready (typed discriminants per @suaveplan/error contract); user-facing message strings remain English-only in this change. Translation catalogues land per-consumer.

Parity checklist

Capability Upstream Migrated Notes
RuleEngine evaluate/evaluateAll/evaluateRuleset/check ✅ ⬆️ Enhanced: AbortSignal, OTel, RulesError codes, depth limit enforced via typed code
RuleBuilder + FieldBuilder + factories ✅ ✅
ContextBuilder + DefaultContextProvider + DefaultFieldResolver ✅ ⬆️ Enhanced: injectable Clock, no new Date() at module scope
Action / ActionRegistry / ActionExecutor ✅ ⬆️ Enhanced: AbortSignal threaded into execute, OTel histogram, typed ACTION_TIMEOUT / ACTION_ABORTED codes
EventBus + HookManager + waitFor ✅ ⬆️ Enhanced: AbortSignal on waitFor, bounded history via injectable Clock, typed EVENT_TIMEOUT / EVENT_ABORTED codes
OperatorRegistry + 30 built-ins ✅ ✅
SchemaValidator (JSON-schema-like) ✅ ⬆️ Enhanced: Zod-validated Schema boundary, OTel histogram
FieldValidator + 12 preset chains ✅ ⬆️ Enhanced: AbortSignal on validate, OTel histogram
CustomValidator + 17 built-in validators + ValidatorRegistry ✅ ⬆️ Enhanced: AbortSignal on async validators, OTel histogram
Serializer (serialize/deserialize/clone/hash/equality) ✅ ⬆️ Enhanced: Zod-validated SerializedRule boundary, function-placeholder strategy documented per RFC 8259
Optimizer (flatten/dedupe/simplify/sort/analyze/estimateCost/findFields) ✅ ✅
Type guards (isConditionRule etc.) ✅ ✅
Hono middleware (6 functions) ✅ 🔄 Promoted to sibling @suaveplan/rules-hono per user direction; surface 1:1
Module-level singleton defaultEngine in middleware ✅ 🚫 Scoped out: module-level singleton is a hidden global. Consumers pass engine explicitly or call getDefaultEngine() lazily (memoised per-process).
console.log builtin handler ✅ ⬆️ Replaced with LoggerLike injection — console.log is banned in genesis src/ per AGENTS.md §2
Math.random / Date.now at module scope ✅ ⬆️ Replaced with injectable Clock / Rng from @suaveplan/types

Source mapping

This change derives from ../suaveplan/enterprise/rules.md and the upstream source at ../suaveplan/packages/universal/rules/.

Requirement Source lines Notes
Zero browser/Node/React deps enterprise/rules.md L37 verbatim — preserved by keeping rules universal-tier
Zod schemas at public boundaries enterprise/rules.md L38 verbatim
Property-based tests for pure functions enterprise/rules.md L39 applied to operators + optimizer + serializer round-trips
Deterministic behaviour — no Math.random/Date.now at module scope enterprise/rules.md L40 verbatim — drives Clock/Rng injection
OTel instrumentation via @suaveplan/opentelemetry for all public entrypoints enterprise/rules.md L41 verbatim
Structured errors extending @suaveplan/error with stable codes enterprise/rules.md L42 drives RulesError registry
AbortSignal everywhere async enterprise/rules.md L43 verbatim
i18n-ready, all messages keyed enterprise/rules.md L46 drives typed code discriminants (messages translatable per code)
Test coverage 100% enterprise/rules.md L50 verbatim
Docs coverage 100% per docs:check enterprise/rules.md L51 verbatim — sibling .md per impl
File-size budget 300 hard / 500 test enterprise/rules.md L52 verbatim
No mocks — real services enterprise/rules.md L53 applied via injectable interfaces (Clock/Rng/Logger), no external IO in rules itself
sideEffects: false tree-shake enterprise/rules.md L55 verbatim
Plugin/extension interface enterprise/rules.md L69 applied to OperatorRegistry + ValidatorRegistry + ActionRegistry + RuleEnginePlugin
Observability surface — counters/histograms/spans enterprise/rules.md L70 drives OTel instrument list above
Benchmark suite in src/__benchmarks__ enterprise/rules.md L72 mitata benchmarks shipped per AGENTS.md §9
Hono middleware upstream src/middleware/hono/hono.ts L1-L350 promoted to sibling package per user direction
30 built-in operators upstream src/operators/operator-registry/operator-registry.ts L1-L682 full surface preserved
17 custom-validator built-ins upstream src/validators/custom-validator/custom-validator.ts full surface preserved
12 field-validator preset chains upstream src/validators/field-validator/field-validator.ts full surface preserved
Serializer function-placeholder strategy upstream src/utils/rule-serializer/rule-serializer.ts preserved; documented as a deliberate limitation

Requirements with no source line

Requirement Justification
RulesError code registry of 24 codes Genesis policy — every error path discriminable. Upstream returns ad-hoc RuleError objects without a stable code set; genesis maps every failure mode to a typed code.
Subpath exports per feature module Genesis tree-shaking policy — AGENTS.md §6.
Pattern B feature-module layout Genesis structure policy — CLAUDE.md "Package Structure" section.
@suaveplan/testing/runner for every test import Genesis cross-runner policy — AGENTS.md §7, CLAUDE.md rule 11.
Explicit named re-exports in feature barrels Genesis barrel policy — AGENTS.md §6a, CLAUDE.md rule 10.
Removal of module-level defaultEngine singleton Hidden globals across module boundaries violate the deterministic / injection-first design. Replaced with explicit engine parameter or lazy getDefaultEngine().

Verification approach

Every requirement in specs/rules/spec.md and specs/rules-hono/spec.md carries a **Verified by:** line at authoring time per AGENTS.md §13. Test files:

rules:

  • src/engine/engine.test.ts — engine evaluation, depth limit, AbortSignal, OTel emission
  • src/engine/engine.spec-conformance.test.ts — RulesError code coverage, type-level invariants
  • src/builder/builder.test.ts — fluent DSL round-trip
  • src/context/context.test.ts — field resolution (dot path / array index), request-context derivation
  • src/actions/actions.test.ts — sequential/parallel, timeout, AbortSignal, registry
  • src/events/event-bus.test.ts — emission, priority hooks, bounded history, waitFor + abort
  • src/operators/operators.test.ts — all 30 operators, property-based for commutative/associative ones
  • src/validators/schema/schema.test.ts — full format + transform + nested coverage
  • src/validators/field/field.test.ts — preset chains + chainable surface
  • src/validators/custom/custom.test.ts — every built-in including Luhn
  • src/serializer/serializer.test.ts — round-trip property tests, function-placeholder behaviour
  • src/optimizer/optimizer.test.ts — flatten/dedupe/simplify property tests
  • src/errors/rules-error.test.ts — every code thrown by ≥1 production path

rules-hono:

  • src/middleware/rule.test.ts
  • src/middleware/validation.test.ts
  • src/middleware/access-control.test.ts
  • src/middleware/response-validation.test.ts
  • src/middleware/rate-limit-rule.test.ts
  • src/middleware/combine.test.ts

Migration impact

None — additive only. No genesis consumers of @suaveplan/rules exist yet (Pending status per STATUS.md). Future consumers (form-builder, workflow) will import directly; this change is published before they land.

Impact

Affected dependents

Run bunx turbo ls --filter=...@suaveplan/rules.

  • No dependents on genesis (Pending → Completed). New package — no behavior change to propagate.
  • @suaveplan/rules-hono is created in this same change, depends on @suaveplan/rules workspace:^.