Epic #89: built-in declarative CEL rules engine + skill-reference sweep#116
Merged
Conversation
Stand up the schema-forge-cel crate and the acceptance oracle the from-scratch CEL evaluator will be built against (epic #89, decision #91: own minimal CEL over DynamicValue — no cel crate, no Cedar). The harness vendors the google/cel-spec `simple` corpus filtered to the SchemaForge-relevant subset (proto-message sections excluded by design, since our value domain is DynamicValue), plus the protos needed to decode it. build.rs pre-encodes each textproto to binary via protoc so the test binary is protoc-free; generated proto types stay confined to the test and never touch the engine's public API. It decodes and runs 1,665 tests across 18 features, compares each against its expected value/error, fails on any unclassified corpus file (no silent coverage gaps), logs excluded features, and enforces a ratcheting pass baseline that #107/#108/#109 will raise as they turn sections green. The engine itself is a stub: evaluate() returns EvalError::unimplemented, so the oracle currently reports an honest red baseline (0 passing).
Hand-written byte-level lexer and precedence-climbing recursive-descent parser for the full CEL expression grammar, producing a typed AST. No parser-generator dependency (FIPS/airgap-clean supply chain); pure std. - lexer: positions (offset/line/col), hex/uint/double numbers, all string quote forms (single/double/triple), raw and bytes prefixes, every escape class, comments. - ast: typed Expr/Literal with dedicated Unary/Binary/Ternary nodes, Comprehension model, and an invertible unparse() for debugging. - parser: iteration macros (all/exists/exists_one/map/filter) and has() lower to comprehension / test-only Select nodes at parse time, using the canonical @Result accumulator name so #108 and the conformance corpus align. - error: ParseError gains optional Position (offset + line/col). Acceptance: tests/parse_smoke.rs parses every expr in the parse + plumbing corpus (224/224, 0 skipped); 43 unit/integration tests green; clippy clean with no lint suppression. evaluate() stays stubbed and the #90 conformance baseline stays 0 — the parse section is end-to-end and flips green in #108. Bumps schema-forge-cel 0.1.0 → 0.2.0 (additive public API).
Evaluate the #107 AST to CelValue. Pure, no I/O, guaranteed-terminating, with a recursion depth guard (DEFAULT_MAX_DEPTH=250) returning an error instead of overflowing the stack (government-production DoS hardening). - eval/mod.rs: scoped recursive evaluator; logical &&/|| with commutative error/short-circuit absorption; ternary (only taken branch); comprehension evaluation with @not_strictly_false-style error deferral for all/exists, verified against the corpus cases. - eval/ops.rs: pure arithmetic (checked int/uint overflow → "return error for overflow"; divide/modulus by zero), cross-type numeric comparison with exact i64/u64/f64 boundary handling, cel_equals (the CEL == operator, distinct from CelValue's type-exact PartialEq used for result matching; NaN != NaN; recursive list/map; cross-type numeric map keys), indexing, membership, size_of. - eval/funcs.rs: function-dispatch seam + the core fns the acceptance sections need: size, dyn, type. Broad stdlib deferred to #109. - value/mod.rs: corrected the PartialEq-vs-operator-equality comment. Conformance: 815 passing (was 0). lists fully green; macros 42/44; basic, fp_math, integer_math, parse light up via evaluation. MIN_PASS_BASELINE ratcheted 0 → 815. 78 tests green, clippy clean, no lint suppression. Bumps schema-forge-cel 0.2.0 → 0.3.0.
Fill the #108 funcs::dispatch seam with the CEL standard library over DynamicValue, raising conformance 815 → 1018. - funcs/convert.rs: int/uint/double/string/bytes/bool/timestamp/duration conversions with CEL range/format error semantics; Go-style duration parser. - funcs/strings.rs: contains/startsWith/endsWith and matches (RE2-style unanchored regex search; linear-time, no ReDoS — see comment). - funcs/time.rs: timestamp/duration accessors (getFullYear..getMilliseconds) honoring CEL's 0-/1-based conventions, with named-IANA and fixed-offset timezone args. - ops.rs: timestamp/duration arithmetic (ts±dur, ts−ts, dur±dur) with cel-go's int64-nanosecond range checks; specific (double,double) `%` overload message. - eval/mod.rs: bare type identifiers (int, string, …) denote `type` values per the CEL standard environment, as a fallback after binding lookup. - conformance.rs: separator-insensitive eval-error matching (reclaims no_such_overload/divide-by-zero spelling variants; never affects value matching); MIN_PASS_BASELINE 815 → 1018. Sections now fully green: string (51/51), fp_math (30/30), macros (44/44), lists. conversions 108/109, timestamps 73/76, integer_math 59/64 — remaining reds are #114 (i64::MIN literal) and namespace-qualified type names, not stdlib bugs. Deferred *_ext functions are listed in funcs/mod.rs (no silent gaps). Adds deps: regex (RE2 matcher), chrono-tz (IANA timezones) — both pure-Rust, no native deps, consistent with the #91 from-scratch decision. Bumps schema-forge-cel 0.3.0 → 0.4.0.
CEL has no negative integer literal token, so i64::MIN is unary minus applied to the magnitude 2^63 — which does not fit i64. The lexer now emits a dedicated IntMinMagnitude token for exactly 2^63 (decimal or hex); the parser folds a unary `-` immediately applied to it into Int(i64::MIN) (rather than negating, which would overflow). A bare 2^63 with no minus stays an out-of-range error. Conformance 1018 → 1037: integer_math now fully green (64/64); the fix also lifts basic (42/43) and comparisons (+13). MIN_PASS_BASELINE ratcheted to 1037. 98 tests green, clippy clean.
Lower and evaluate the cel-spec two-variable iteration macros, turning the macros2 conformance section fully green (0 → 46/46). - ast: Comprehension gains iter_var2: Option<String>. None = single-var (unchanged); Some = two-var. unparse inverts the two-var and transformList/transformMap shapes (camelCase names). - eval: range_elements yields (var1, Option<var2>) pairs — list → (int index, element), map → (key, value) for two-var; single-var path preserved. New Scope::with3 + bind_iteration; the existing @not_strictly_false error-deferral loop is reused unchanged. - funcs: internal @mapInsert(map, key, value) backs transformMap's accumulator step (@-prefix is unspellable in surface CEL). - parser: lower all/exists/existsOne (3-arg), transformList (3/4-arg), transformMap (3/4-arg) to two-var Comprehension nodes; iteration vars must be bare idents; wrong arity falls back to a method Call. Conformance 1037 → 1083; macros (single-var) stays 44/44, no regressions. 116 tests green, clippy clean, no lint suppression. Bumps schema-forge-cel 0.4.0 → 0.5.0.
Wire the owned CEL engine into the SchemaForge DSL as first-class field annotations, with file:line:col diagnostics on malformed expressions and full schema round-trip. Grammar + parsing only — runtime enforcement (#92/#93/#94) and apply-time type-checking (#104) are separate. Syntax (positional, CEL as a double-quoted string): age: Integer @require("age >= 18", "must be 18 or older") name: Text @compute("first + ' ' + last") ts: DateTime @default("now()") - core: FieldAnnotation gains Require { expr, message }, Compute { expr }, Default { expr } (the expression default, distinct from the literal `default(...)` FieldModifier). serde + Display round-trip with DSL string escaping; core stays cel-free (stores raw expr source only). - dsl: parser arms validate the CEL source syntactically via schema_forge_cel::parse; on error the intra-expression position is mapped through DSL string escapes to an absolute line:column so the diagnostic points into the expression. New DslError::InvalidCelExpression. printer + round-trip restored. Adds schema-forge-cel as a path dep (no cycle). 628 tests pass, clippy clean, no lint suppression. Bumps schema-forge-core 0.15.0 → 0.16.0, schema-forge-dsl 0.9.0 → 0.10.0.
Add write-time evaluation of `@require("<expr>", "<message>")` CEL
validation rules, rejecting entity writes with HTTP 422 when a predicate
is not `true` — in-transaction, before persistence, with no gRPC hook
service deployed.
- cel: new `value::bridge` with pure `dynamic_to_cel` / `cel_to_dynamic`
conversions across the `DynamicValue` / `CelValue` boundary (the inverse
direction is used by #93/#94). Refs surface as id strings; CEL-internal
types without a storage representation (bytes/duration/type) and uint
overflow fail with `ConversionError`. Bumps cel 0.5.0 → 0.6.0.
- acton: new HTTP-free `rules` module — `build_bindings` (entity fields
plus a `principal` map from claims) and `check_requires`. Fail-closed
for a government audit target: a predicate passes only on `Bool(true)`;
a definite `false` is collected as a 422 rejection (all messages, in
schema-declaration order); a non-bool result or an evaluation error
blocks the write as a 500, never letting it through.
- Wire `check_requires` into create/update/patch handlers after the
before_change hook and before persistence (PATCH evaluates the full
post-patch view). `RuleError → ForgeError` mapping kept at the handler
boundary so `rules` stays HTTP-free.
Tests: 17 bridge unit tests, 9 rules unit tests (incl. cross-field
invariant, principal predicate, and every fail-closed branch), and an
end-to-end integration test (201 valid / 422 invalid with message).
Add write-time evaluation of `@compute("<expr>")` CEL expressions,
storing the derived value into the entity before persistence — no gRPC
hook service deployed.
- acton: new `rules::apply_computed` plus a pure `coerce_to_field_type`
helper. Computed values are STORED (not virtual) and OVERWRITE any
client-supplied value for the field, so a client cannot smuggle a value
into a server-derived field. Fields evaluate in schema-declaration
order with bindings rebuilt from the current field set before each
compute, so a later computed field can read an earlier one (chainable).
Fail-closed: an eval/conversion/coercion failure returns a 500 and
stores nothing for that field — never a half-evaluated value.
- `coerce_to_field_type` handles the safe, lossless cases the natural
CEL→DynamicValue mapping leaves open: Float field + Integer → Float;
Enum field + Text → validated Enum variant (else error); DateTime field
+ Text → parsed RFC 3339 (else error). Every other pair passes through.
Strict apply-time type-checking against the field type is #104.
- Wire `apply_computed` before `check_requires` in create/update/patch
(PATCH on the merged post-patch view), so `@require` validates the
computed values and the PATCH delta picks up computed changes.
Tests: 9 rules unit tests (numeric/string compute, client-value
overwrite, chained compute, principal reference, eval error, enum
coercion success/failure, datetime parse) and an end-to-end integration
test asserting derivation and overwrite.
Add write-time evaluation of `@default("<expr>")` CEL expression
defaults, seeding absent fields on entity creation — no gRPC hook
service deployed.
- acton: new `rules::apply_defaults`, insert-only (wired into
create_entity only, never PUT/PATCH). Fills a field only when it is
absent or explicitly null; a non-null value already present (client,
@owner/tenant/audit stamp, or before-hook) is left untouched. Runs
first in the rule order (default → compute → require) so a computed
field can read a defaulted one and @require validates the finalized
entity. Fail-closed: eval/conversion/coercion failure → 500, nothing
stored. Reuses the pure `coerce_to_field_type` helper.
- Distinct from the literal `FieldModifier::Default` (storage-layer SQL
DEFAULT), whose behavior is unchanged. Precedence with @owner is
documented and tested: @owner stamps before hooks, defaults fill only
absent/null, so @owner always wins.
- The CEL engine is intentionally pure (no I/O, no ambient authority) and
exposes no `now()` function. Instead each handler captures one
request-time instant (`rules_now`, reused for audit columns) and the
rule API binds it as a `now` timestamp variable, available to all three
rule families. So the issue's `now()` example is spelled `@default("now")`
— deterministic and auditable, one instant per write. `now: DateTime<Utc>`
is threaded through build_bindings/check_requires/apply_computed/apply_defaults.
Tests: 10 new rules unit tests (now/principal defaults, absent-vs-null,
no-override, chained default, @owner precedence, eval error, and a
@require referencing `now`) and an end-to-end integration test.
…104) Add apply-time static type-checking of CEL rule expressions (@require/@compute/@default) against schema field types, wired into the DSL parse pipeline so a type-incorrect rule fails before deploy with a file:line diagnostic instead of on a live request. New pure `check` module in schema-forge-cel: - `infer` is a TOTAL function over the typed AST returning `InferredType` (`Dyn` | `Known(CelType)`); it yields `Dyn` for anything it cannot pin down (unknown idents, selects, indexes, unmodeled calls), so the checker is conservative and never rejects a valid rule. - `field_accepts` mirrors the runtime coercions in rules.rs/bridge.rs (Float accepts Int; DateTime accepts String; etc.) so a rule that would succeed at write time is never flagged. - `check_rule` errors ONLY when the top-level result type is definitely known and definitely incompatible: @require must be bool; @compute and @default must be assignable to the field type. `#[non_exhaustive]` FieldType/Cardinality variants fail open. - `rule_type_env` binds every sibling field plus `principal` (Map) and `now` (Timestamp), matching rules.rs::build_bindings. DSL: capture each rule expression's source span, then run a per-schema type-check pass in parse_schema, surfacing failures as a new `DslError::RuleTypeError { message, line, column, span }`. The CLI renders it with a span underline and fix suggestion. cel 0.6.0 -> 0.7.0; dsl 0.10.0 -> 0.11.0; cel pin bumped in dsl + acton.
…105) Establish the canonical in-transaction write-path ordering for create/update/patch: @default -> @compute -> @require -> before_* hooks -> PERSIST -> { after_* hooks, webhook dispatch } Previously the before_validate/before_change gRPC hooks ran *before* the three rule phases. Since rules are pure and cheap, they now run first so a @require rejection (422) short-circuits the whole write before any hook network round-trip and before persistence. Post- persistence fan-out (after_* hooks via HookDispatchActor, webhook delivery) stays detached and is suppressed entirely on rejection. Reorder is surgical (move the rule block ahead of the hook block in all three handlers); no API, type, or actor-message changes. Existing hook integration tests carry no rule annotations, so behavior for them is unchanged. Tests: - rule_phase_order_default_then_compute_then_require_is_observable (integration.rs): a compute reads a defaulted sibling and a require validates the computed value, observable end-to-end. - require_rejection_fires_no_before_or_after_hook_and_persists_nothing and passing_require_reaches_before_and_after_hooks (hooks_integration.rs): a @require 422 fires no before_*/after_* hook and persists nothing; a valid write reaches both hooks. Docs: authoritative ordering stated on the rules.rs and hooks module docs, plus docs/rule-ordering-reference.md.
…106) Declarative rules (@require/@compute/@default) live as annotation text inside the .schema file, so the per-file signature and the manifest's pinned sha256 already cover them like any other byte — the audit win over an opaque out-of-band hook binary. Add enforce_mode_rejects_tampered_rule_annotation: sign a schema whose gating logic is entirely declarative, then weaken only the @require threshold (>= 0 -> >= -999999) in the .schema bytes and confirm verify under enforce rejects it with VerifyError::HashMismatch. No production code change — this formalizes and pins existing signing behavior. The reviewer-facing audit story (how to enumerate every rule gating an entity: read the signed .schema / sf parse, then sf verify for provenance) is documented in docs/rule-ordering-reference.md.
Add a first-class `duration` scalar end-to-end so a duration field
round-trips DSL -> core -> cel -> storage -> API, and rules can do
timestamp/duration arithmetic (e.g. now - created_at < duration('220752000s')).
- core: FieldType::Duration + DynamicValue::Duration(chrono::TimeDelta);
new types::duration module with Go-style format/parse helpers and a
custom DurationParseError; query.rs type-name/compat arms.
- dsl: 'duration' keyword (lexer/token), parse_type, printer round-trip.
- cel: bridge both directions (DynamicValue::Duration <-> CelValue::Duration);
check.rs infers/accepts CelType::Duration.
- postgres: BIGINT column storing signed nanoseconds (num_nanoseconds),
bind/read/array round-trip, fail-closed on i64-nanosecond overflow.
- surrealdb: native duration column + Value::Duration round-trip. SurrealDB's
native duration is unsigned, so a negative duration is REJECTED fail-closed
on write (BackendError::ValidationFailed -> HTTP 422) rather than silently
stored as NULL; the check recurses through arrays and composites.
- acton: REST request->persist->response round-trip; canonical JSON wire
form is the Go-style seconds string (e.g. '220752000s').
Add a first-class `bytes` scalar (inline binary) end-to-end so a bytes field round-trips DSL -> core -> cel -> storage -> API, and rules can size/compare bytes and use base64.encode/base64.decode. Models the #96 `duration` precedent; `bytes` additionally carries an optional max_size. - core: FieldType::Bytes(BytesConstraints { max_size: Option<usize> }) + DynamicValue::Bytes(Vec<u8>); new types::bytes_constraints and a shared types::base64 codec (standard padded encode/decode + a padding-indifferent decode for CEL); query.rs type-name/compat arms; Display = standard base64. - dsl: `bytes` / `bytes(max: N)` keyword (lexer/token), parse_bytes_params, printer round-trip. - cel: bridge both directions (DynamicValue::Bytes <-> CelValue::Bytes, replacing the Unsupported("bytes") stub); check.rs infers/accepts CelType::Bytes and models base64.encode->String / base64.decode->Bytes. - cel stdlib: encoders extension base64.encode/base64.decode (cel-spec), shared codec, parser lowers `base64.encode(x)`/`decode(x)` to namespaced global calls; encoders_ext conformance section now fully green (4/4), baseline 1083 -> 1146. Adds the `base64` crate to schema-forge-cel. - postgres: BYTEA column; bind/read/array Vec<u8> round-trip; emits `CHECK (octet_length(col) <= N)` when max_size is set; JSON = base64. - surrealdb: native `bytes` column + Value::Bytes round-trip; max_size enforced fail-closed on write (BackendError::ValidationFailed -> 422), recursing through arrays and composites. - acton: JSON wire form is standard base64 with padding on both request parse (base64 -> Vec<u8>, invalid/oversized -> 422) and response serialize; schema POST accepts "Bytes" and {"type":"Bytes","data": {"max_size":N}}.
Add a typed, open-keyed `map<string, V>` field type end-to-end, distinct from `Composite` (fixed declared field set) and `Json` (untyped). A map field round-trips DSL parse/print -> core serde -> cel bridge both ways -> Postgres JSONB + SurrealDB object -> the acton REST API, and CEL map comprehensions (m.all(k,v,...), m.exists(...)) type-check (#104) and evaluate over it. Key type is constrained to `string` for this first cut: JSON, Postgres JSONB, and SurrealDB objects are all string-keyed, so non-string keys (int/uint/bool) cannot round-trip without lossy string key-encoding. The type is modeled as `FieldType::Map { key, value }` for forward-compat, but the DSL parser rejects a non-string key with an actionable error. Non-string keys are a noted follow-up. - core: FieldType::Map { key: Box<FieldType>, value: Box<FieldType> } (Display = Map<Key, Value>, serde) + DynamicValue::Map(BTreeMap<String, DynamicValue>), a NEW variant distinct from Composite. query.rs type-name and compat arms. - dsl: `map` keyword + `<`/`>` tokens; parse_map_type recursively parses key and value type expressions and rejects a non-string key with DslError::MapKeyNotString; printer round-trips `map<text, V>`. - cel: bridge DynamicValue::Map -> CelValue::Map (string keys, recursing values); check.rs infers/accepts CelType::Map so a map-comprehension @require rule type-checks to Bool. - postgres: JSONB column; bind/read recurse values per V (json_to_map decodes datetime/duration/bytes/enum/nested values against the declared value type); typed JSONB NULL. - surrealdb: native string-keyed object; max_size/negative-duration validation recursion now walks Map values fail-closed. - acton: JSON object wire form both directions; request parse validates each value against V (mismatch -> 422); schema POST accepts {"type":"Map","data":{"value":<FieldType>}}. - docs: site-guide.md field-types table + a note distinguishing map<K,V> (typed, open-keyed, homogeneous values) from composite (fixed declared field set) and json (untyped).
Make the value-lattice projection decisions for the three non-obvious field
types explicit, consistent across the bridge and the type-checker, and
documented:
- Enum -> String (the variant name; storage is string-backed, so ordering is
lexical/by-name). Already bridged; documented and exercised.
- Relation{One} (Ref) -> opaque id String; Relation{Many} (RefArray) ->
List<String>. Tighten field_accepts so a single ref accepts only a string
and a many-ref accepts only a list, matching field_type_to_inferred and the
bridge. Cross-entity dereference stays out of scope (#95).
- File -> metadata map only. A File field is stored as its FileAttachment
object (DynamicValue::Json) and surfaces as a CEL map; the blob bytes are
never projected as a value. field_accepts(File) accepts only a map.
Adds a projection table to the bridge module doc and unit tests covering each
projection, including a fail-closed check that no blob bytes leak into a File
projection and stricter relation type-checking.
Update the DSL skill reference to reflect the epic #89 grammar additions: - duration, bytes(max), and map<text, V> field types (#96/#97/#99) — EBNF, field-type sections, lexer keywords, and the backend DDL mapping table. - @require/@compute/@default CEL rule annotations (#92/#93/#94) — a dedicated rule_annotation production, available bindings (fields, now, principal), fail-closed semantics, and the @default annotation vs default(...) modifier distinction. - New validation-table rows (MapKeyNotString, CEL parse/type-check errors).
Add a reserved `related.<F>.<col>` namespace usable only inside `@require`
rule expressions. `related.F` dereferences a `Relation{One}` field F to its
committed, tenant-scoped related row, bound as a CEL map; `.col` reads a
column on it. The bare field F remains the opaque id string (#102).
Engine purity is preserved: the CEL evaluator stays 100% pure (no backend
handle, no async, no I/O in `evaluate`; its signature is unchanged). Cross-
entity reads use the same prefetch-and-bind pattern as the request clock
`now` — the route layer resolves the I/O before evaluation and injects a
`related` binding next to `principal`/`now`.
- schema-forge-cel: pure AST walker `related_paths` + `RelatedPath` extract
every `related.<F>.<…>` path anywhere in an expression.
- schema-forge-dsl: `check_rule_types` rejects at apply-time, with span→line:
col diagnostics — `related.*` in @compute/@default, a to-many `related.F`,
and a non-relation/undeclared `related.F`. New `#[non_exhaustive]` DslError
variants carry the audit-focused messages.
- schema-forge-acton: `check_requires_with_bindings` is the pure core;
`check_requires_with_related` (route layer) collects distinct Relation{One}
references, reads each FK, loads the row through the supervised forge actor
with `inject_tenant_scope` (never the unscoped GetEntity), projects it with
`dynamic_to_cel`, and binds `related`. Wired into create/update/patch.
Fail-closed: an absent/null FK, a missing related row, or a tenant-hidden row
leaves `related.F` unbound, so the predicate hits an absent reference and is
rejected — never a silent pass or null coercion. Multi-hop
(`related.F.G.<…>` crossing a second relation) is rejected at runtime with a
clear message; the resolver has every target schema via the batch fetch.
Tests: 12 cel walker units, 6 dsl apply-time units, 7 acton surrealdb
integration tests (pass/reject, null FK, missing row, cross-tenant isolation,
to-many, multi-hop). Docs: new section in docs/rule-ordering-reference.md.
Minor version bumps: cel 0.8.0→0.9.0, dsl 0.11.0→0.12.0, acton 0.33.0→0.34.0.
…s engine Sweep the skill references for staleness after epic #89: - SKILL.md: add the CEL rule annotations (@require/@compute/@default) and the duration/bytes/map field types to the frontmatter, When-to-Use, quick-ref tables, and a new "Write-Time Rules" section; add the schema-forge-cel crate and refresh crate versions; correct the now-dispatched before_validate event. - dsl-reference.md: document the related.<F>.<col> single-hop cross-entity read primitive (#95) — limits, tenant-scope, fail-closed. - hooks-reference.md: before_validate is now wired (was reserved); add a rules-vs-hooks guidance note (prefer @require for in-process validation). - patterns.md: new Write-Time Rules pattern (rules vs hooks, bindings, cross-entity reads, gotchas). - query-api-reference.md: add Duration (Go-style) and Bytes (base64) filter coercion rows; note Map/Json/relation/File have no scalar coercion. - examples.md: add a worked example using rules + duration/bytes/map. - storage-reference.md: cross-reference the bytes field type vs file fields. All grammar/semantics grounded in the parser/codegen, not the issue shorthand; rule annotations correctly shown as field-level (not schema-level).
The installed skill (~/.claude/skills/schemaforge/) carried five reference files the repo never tracked: cli-reference, config-reference, dsl-quickref, rest-api-reference, signing-reference. Import them so the repo is the single source of truth, and apply the epic #89 staleness sweep to them: - dsl-quickref.md: add duration/bytes/map field types and @require/@compute/ @default annotations to the quick-ref tables, add a Write-Time Rules section, correct the now-dispatched before_validate event. - signing-reference.md: note that rule-annotation text (@require/@compute/ @default) is covered by the schema signature — tampering a predicate fails verify under enforce (#106). - rest-api-reference.md: document the write-time rule phases on entity create/update (422 on @require rejection; server-derived @compute/@default). - cli-reference.md: parse syntax-validates rule expressions; apply type-checks them against field types and validates related.* cross-entity references. - config-reference.md: imported unchanged (the rules engine has no config).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merges the completed epic #89 (built-in declarative rules engine) and the skill-reference documentation sweep from
devintomain.What's included (25 commits)
CEL rules engine (epic #89 — now closed):
DynamicValue(ADR-0002), parser/evaluator/stdlib, conformance-tested@require/@compute/@defaultwrite-time rule annotations (in-process, fail-closed, ahead of hooks)related.<field>.<col>) in@require(Rules: constrained cross-entity reads in expressions (single relation hop) #95)duration,bytes(+base64stdlib),map<text, V>(Type: duration (records retention / TTL / SLA) + timestamp arithmetic #96/Type: bytes (inline binary — hashes, signatures, key material) #97/Type: typed map<K,V> (vs Composite struct / Json) #99); value-lattice projection (Type: value-lattice projection for Enum / Relation·Ref / File #102)before_validatehook now dispatched; canonical write-path ordering (Integration: rule evaluation ordering & precedence vs hooks (in-transaction) #105)Skill references (
skills/schemaforge/):before_validateNotes
devandmaindiverged (main carries the demo-console PRs feat(serve): embed and serve the ops console at /console #110–112), so this is a 3-way merge — expect to resolve conflicts inCargo.lock, crate versions, and any shared demo schema/template files.--features surrealdbondev.Follow-on (not in this PR): #115 Kani verification of the CEL scalar core.