Documentation
¶
Overview ¶
Package rules is wowapi's rule/configuration engine: modules register rule points (a key, a RuleValueSchema'd value, a default, allowed scopes, and whether changes require approval); values are stored as versioned rows with temporal validity; and resolution picks the most specific active value for a (tenant, org, at) — org-ancestry → tenant → platform → code default. Versions are immutable (never mutated, only superseded), so any historical `at` resolves deterministically. Contract: blueprint 02 §2.
Rule points are the ONLY sanctioned place for values that must change without a deploy (feature flags, tenant overrides); framework config holds only their platform defaults (12 §6).
RuleValueSchema ¶
A Point's ValueSchema is NOT JSON Schema — it never was a full implementation, and as of B3 the contract is corrected to say so plainly. It is RuleValueSchema: a small, closed, framework-specific grammar (ratified Decision 2 — a strict limited grammar, no JSON-Schema library dependency) recognizing exactly these top-level keywords:
- "type": one of integer/number/string/boolean/object/array/null (any other value is rejected at Register — B3 defect 1);
- "enum": a JSON array of allowed literal values;
- "minimum" / "maximum" / "exclusiveMinimum" / "exclusiveMaximum": numeric bounds;
- "minLength" / "maxLength" / "pattern" (RE2): string constraints;
- "minItems" / "maxItems": array length bounds;
- "required": a shallow presence check for object keys (NOT recursive per-property validation — there is no nested "properties" schema).
Any keyword outside this list — "multipleOf", "additionalProperties", "items" sub-schemas, "properties", "patternProperties", etc — is REJECTED at Register, not silently ignored (B3 defect 2: json.Unmarshal into an unexported struct used to drop unrecognized keys, so a schema author could write a constraint the framework never enforced without any error). A rule point needing per-property typing should declare separate top-level rule points instead of one object-shaped point with nested constraints.
Register also validates that a Point's Default conforms to its own ValueSchema (B3 defect 3) — a schema that can't even validate its own default is broken by construction and must never boot. Resolver.Resolve re-validates the winning STORED value against the point's CURRENT schema before returning it (B3 defect 4): a value that conformed to an earlier, looser schema can drift out of conformance after a module upgrade tightens the schema, and Resolve surfaces that as an error rather than silently handing back a non-conforming value.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func SyncDefinitions ¶ added in v1.1.0
SyncDefinitions upserts every point in the registry into rule_definitions — the persisted mirror blueprint 02 §2.1 describes ("makes points introspectable/auditable in the DB"), and the FK rule_versions.rule_key depends on. It is the rule-registry analogue of kernel/seeds.Sync: it must run on a platform-privileged connection (rule_definitions is app_platform SELECT/INSERT/UPDATE, app_rt SELECT-only — migration 00008), and it is idempotent — re-running converges the schema/default/scopes/approval/ description columns onto whatever the Go registry currently declares, never producing duplicate rows (ON CONFLICT (key) DO UPDATE).
Call this from the generated migrate main after module migrations (so the table exists) and before any rule_versions writes — mirroring seeds.Sync's lifecycle position exactly (GAP-003 → GAP-007). Unlike seed catalogs (YAML the framework CLI can load off disk), rule points exist only as Go declarations inside a booted product process, so there is no standalone `wowapi rules sync` subcommand; a product with a custom migrate main calls this itself the same way.
SyncDefinitions does not itself check module ownership beyond what Registry.Register already enforced at registration time (a rule point can only be registered under its own module prefix); a registry that failed Err() should never be passed here.
Types ¶
type OrgAncestry ¶
type OrgAncestry func(ctx context.Context, db database.TenantDB, orgID uuid.UUID) ([]uuid.UUID, error)
OrgAncestry resolves an org's ancestor chain (self-first) so the resolver can walk org scope upward. Implemented against the DB by the caller-provided func so kernel/rules need not import the org store.
type Point ¶
type Point struct {
Key string
Module string
// ValueSchema is a RuleValueSchema document (see the package doc above) —
// a small closed grammar, NOT JSON Schema. Validated at Register (schema
// well-formedness + Default conformance), at Propose (write time), and at
// Resolve (defense in depth against post-write schema drift).
ValueSchema json.RawMessage
Default json.RawMessage // compiled default value; must conform to ValueSchema (checked at Register)
AllowedScopes []ScopeKind
RequiresApproval bool
Description string
}
Point is a registered rule point: the schema + default + policy for a key.
type Proposal ¶
type Proposal struct {
Key string
Scope ScopeKind
ScopeID uuid.UUID // org id for org scope; zero otherwise
Value json.RawMessage
EffectiveFrom time.Time // zero → now
}
Proposal is a requested rule value change at a scope.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry collects rule points during module registration; SyncDefinitions persists it to rule_definitions (the generated migrate main calls it right after seed sync, GAP-007), and it is consulted by the resolver for defaults + policy.
func (*Registry) Register ¶
Register adds a rule point. Malformed keys, a module-prefix mismatch, a missing schema/default, a schema that is malformed or names an unknown type/keyword outside the RuleValueSchema grammar (B3 defect 1/2), a default that violates its own schema (B3 defect 3), or a duplicate are recorded as errors surfaced by Err() — the boot-error-accumulation gate (app/boot.go calls k.Rules.Err()) turns any of these into a boot failure, so a silently-unenforced or self-contradictory rule point can never go live.
type Resolved ¶
type Resolved struct {
Key string
Value json.RawMessage
Scope ScopeKind // scope the winning version was set at (or "" for the code default)
VersionID uuid.UUID // zero when the code default won
IsDefault bool
}
Resolved is a rule value plus provenance.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver resolves rule values. It runs on the caller's TenantDB (one snapshot), reads active versions, and falls back to the registered default.
func NewResolver ¶
func NewResolver(reg *Registry, ancestry OrgAncestry) *Resolver
NewResolver builds a resolver over the rule registry. ancestry may be nil (org-scope resolution then falls back to tenant/platform/default).
func (*Resolver) Resolve ¶
func (r *Resolver) Resolve(ctx context.Context, db database.TenantDB, key string, org uuid.UUID, at time.Time) (Resolved, error)
Resolve returns the effective value of key for (tenant, org, at): the most specific active version wins — org-ancestry (nearest first) → tenant → platform → code default. Versions are immutable, so any historical `at` resolves deterministically (blueprint 02 §2.2). An unregistered key is a programming error. Resolve validates the winning value against the point's CURRENT value_schema before returning it (B3 defect 4): the value was validated against whatever schema was live at Propose time, but a point's schema can be tightened later (module upgrade) — a stored value that conformed to an earlier, looser schema can drift out of conformance with the schema the point is registered under now. Re-checking here is cheap (pure in-memory, no extra I/O — the point's schema is already loaded from the registry) and turns silent schema drift into a loud KindInternal error naming the rule key, rather than handing a caller a value that violates the very contract it is being served under. The code default itself is never re-checked here — it was already validated against this same schema at Register (B3 defect 3), so it is trusted by construction.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store persists rule versions. Writes run on the caller's TenantDB (draft proposals as app_rt) or a platform connection (activation as app_platform).
func (*Store) Activate ¶
func (s *Store) Activate(ctx context.Context, db database.DBTX, versionID, approvedBy uuid.UUID) error
Activate approves a draft version: it supersedes any active version at the same scope and marks the draft active, recording the approver — all in one tx. Runs with platform privilege (rule activation is a kernel/platform concern). Returns an error if the version is not in draft/pending.
func (*Store) Propose ¶
Propose inserts a DRAFT rule version in the caller's tenant tx (app_rt may INSERT). A draft never resolves — it must be Activate'd (a platform/kernel operation, app_platform) to take effect. This keeps rule ACTIVATION — which changes runtime behavior — off the module-facing app_rt role, consistent with the config-write posture (SEC-13). The RequiresApproval flag governs whether a human/workflow approval must precede Activate; the store mechanics are uniform.