rules

package
v1.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 10, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

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

func SyncDefinitions(ctx context.Context, db database.DBTX, reg *Registry) error

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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty rule registry.

func (*Registry) Err

func (r *Registry) Err() error

Err returns accumulated registration errors joined, or nil.

func (*Registry) Get

func (r *Registry) Get(key string) (Point, bool)

Get returns the registered point.

func (*Registry) Keys

func (r *Registry) Keys() []string

Keys returns registered keys, sorted.

func (*Registry) Points

func (r *Registry) Points() map[string]Point

Points returns the registered points keyed by key.

func (*Registry) Register

func (r *Registry) Register(module string, p Point)

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.

func (Resolved) Decode

func (r Resolved) Decode(out any) error

Decode unmarshals the resolved value into out.

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 ScopeKind

type ScopeKind string

ScopeKind is the level a rule value applies at.

const (
	ScopePlatform ScopeKind = "platform"
	ScopeTenant   ScopeKind = "tenant"
	ScopeOrg      ScopeKind = "org"
)

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 NewStore

func NewStore(reg *Registry, idgen model.IDGen) *Store

NewStore builds the version store over the rule registry.

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

func (s *Store) Propose(ctx context.Context, db database.TenantDB, p Proposal) (uuid.UUID, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL