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 withevaluate/evaluateAll/evaluateRuleset/check, depth limit, evaluation cache, AbortSignal, OTel spans + histograms, error results carryingRulesErrorcodes - ADDED:
RuleBuilder/FieldBuilderfluent DSL —rule()/condition()/and()/or()/not()/group()factory functions - ADDED:
ContextBuilder/DefaultContextProvider/DefaultFieldResolver/createRequestContext/createScopedContext/getField/hasField/context/contextWith - ADDED:
Action/ActionRegistry/ActionExecutorwithcreateAction/createActionRegistry/createActionExecutor/validateActionConfig/builtInHandlers(log/set/delay/noop), timeout + AbortSignal, sequential + parallel execution - ADDED:
EventBus/HookManagerwithcreateEventBus/createHookManager, bounded history (injectableClock), priority hooks,waitForwith 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,creditCardValidatorw/ 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/validateSerializedRulewith function-placeholder strategy forCustomRule.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:
RulesErrorextendingSuaveplanErrorwithdefineCodes("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/combineRulesover HonoMiddlewareHandler - ADDED:
RuleMiddlewareOptions/ValidationMiddlewareOptions/AccessControlOptions/HonoValidationErrortypes - ADDED: Zod-validated options; default singleton
RuleEngineopt-in (not module-level); explicitengineinjection 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-registryintegration — enterprise plan §3 lists this; deferring because the schema-registry use case forrulesis 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/rulesfrom genesis yet. CHANGELOG entry covers the API delta for documentation. - i18n keying of error messages — error codes are i18n-ready (typed discriminants per
@suaveplan/errorcontract); 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 emissionsrc/engine/engine.spec-conformance.test.ts— RulesError code coverage, type-level invariantssrc/builder/builder.test.ts— fluent DSL round-tripsrc/context/context.test.ts— field resolution (dot path / array index), request-context derivationsrc/actions/actions.test.ts— sequential/parallel, timeout, AbortSignal, registrysrc/events/event-bus.test.ts— emission, priority hooks, bounded history, waitFor + abortsrc/operators/operators.test.ts— all 30 operators, property-based for commutative/associative onessrc/validators/schema/schema.test.ts— full format + transform + nested coveragesrc/validators/field/field.test.ts— preset chains + chainable surfacesrc/validators/custom/custom.test.ts— every built-in including Luhnsrc/serializer/serializer.test.ts— round-trip property tests, function-placeholder behavioursrc/optimizer/optimizer.test.ts— flatten/dedupe/simplify property testssrc/errors/rules-error.test.ts— every code thrown by ≥1 production path
rules-hono:
src/middleware/rule.test.tssrc/middleware/validation.test.tssrc/middleware/access-control.test.tssrc/middleware/response-validation.test.tssrc/middleware/rate-limit-rule.test.tssrc/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-honois created in this same change, depends on@suaveplan/rulesworkspace:^.