Pyric
Navigate

pyric/rules compatibility matrix

This is a Pyric-native API with no Firebase counterpart, so coverage is measured against its own public exports rather than a Firebase surface.

Status legend

Conforming — the simulator matches the production verdict, locked by a replayed observation Diverged (documented) — a known simulator divergence from production with a written reason

Firestore rules engine — production simulator conformance (rules-firestore corpus)

Rules simulatorCEL builtins math./timestamp./duration.* (FM3) — arithmetic, date, and duration comparisons in rules
oracle:rules-firestore-builtins-time-and-math — production Firestore Rules Test API verdicts for corpus scenario "builtins-time-and-math", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorBytes, String.toUtf8(), and hashing.{md5,sha256,crc32,crc32c}() (Item 5.3) in rules
oracle:rules-firestore-bytes-toutf8-and-hashing — a 2026-07-21 production Firestore Rules Test API recapture supplies 20 verdicts, including paired negative and positive witnesses for padded base64url, the URL-safe alphabet, uppercase hexadecimal MD5/SHA-256, and production's little-endian CRC32/CRC32C byte serialization. unit:rules/oracle-conformance.test.ts replays all verdicts against the local simulator; unit:rules/simulator/wrappers/bytes.test.ts pins the exact representations and reference digests.
Rules simulatorTyped cross-type operator overloads for Timestamp/Duration (Item 2) in rules — no silent numeric coercion / type-identity loss
oracle:rules-firestore-cross-type-operator-overloads — production Firestore Rules Test API verdicts for corpus scenario "cross-type-operator-overloads", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorCEL tri-state error absorption in ||/&& (RULES-B3) — error || true → ALLOW, error && false → DENY, commutative absorption (not JS left-to-right short-circuit)
oracle:rules-firestore-error-absorption-and-or — production Firestore Rules Test API verdicts for corpus scenario "error-absorption-and-or", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorgetAfter()/existsAfter() expose the projected post-write target and cross-document atomic batch state; create exists, delete does not, and an unrelated unchanged document falls back to its committed existence
oracle:rules-firestore-get-after-and-exists-after records authenticated Web SDK verdicts against temporarily deployed production rules, including target create/delete and unrelated-path semantics. Its lifecycle proves exact rules release restoration; retained diagnostics document why the hosted Rules Test API was not sufficient. Replayed verdict-for-verdict by unit:rules/oracle-conformance.test.ts; get-after.test.ts and get-after-batch.test.ts cover target, sibling-write, solo-write denial, delete, and atomicity locally.
Rules simulatorget() of a missing document (RULES-B8) in rules — resource identity (id/__name__) exposure on a mocked/missing get() result
oracle:rules-firestore-get-missing-doc — production Firestore Rules Test API verdicts for corpus scenario "get-missing-doc", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts. Serializable functionMocks now retain their production data-only shape, so mocked get() results leave id/__name__ absent and both identity reads error to DENY; real DocStore lookups retain their separate identity-bearing path. All 8 cases match production.
Rules simulatorrequest.path/request.query/resource.id/resource.__name__ globals (Item 6) in rules
oracle:rules-firestore-globals-request-path-and-resource-id — production Firestore Rules Test API verdicts for corpus scenario "globals-request-path-and-resource-id", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts. request.query is now absent on non-list operations, so reading it on create errors to DENY instead of false-ALLOWing through an invented empty map. request.path and the create-time absent-resource controls remain unchanged; all 6 cases match production. Resource identity on an EXISTING document is scoped to firestore-rules#183.
Rules simulatorint/float division and type distinction (RULES-B5) in rules — truncating int÷int, float division stays float, div-by-zero denies, is int/is float distinct
oracle:rules-firestore-int-float-and-division — production Firestore Rules Test API verdicts for all ten corpus cases, replayed verdict-for-verdict by unit:rules/oracle-conformance.test.ts. The simulator now revives non-integral JSON wire numbers as RulesFloat values, preserving production's float payload type while retaining bare integral numbers as ints; unit:rules/simulator/handler.test.ts pins both literal and payload distinctions.
Rules simulatorList.concat()/removeAll()/toSet() (Item 5.2) in rules
oracle:rules-firestore-list-methods-concat-removeall-toset — production Firestore Rules Test API verdicts for all nine cases, replayed verdict-for-verdict by unit:rules/oracle-conformance.test.ts. The seven positive List witnesses and the concat DENY witness match; the historical toSet().difference(list) case remains DENY because production requires a Set argument.
Rules simulatorMap.get(key, default), including list-form nested-path traversal (Item 3), in rules
oracle:rules-firestore-map-get-string-and-list-form — production Firestore Rules Test API verdicts for corpus scenario "map-get-string-and-list-form", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatormatches() as an anchored full-string RE2 match (RULES-B4) in rules — a pattern matching only a substring is false
oracle:rules-firestore-matches-full-string-regex — production Firestore Rules Test API verdicts for corpus scenario "matches-full-string-regex", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorPath wrapper, path() constructor, and Path.bind() (Item 5.4) in rules
oracle:rules-firestore-path-constructor-and-bind — production Firestore Rules Test API verdicts for all nine corpus cases in "path-constructor-and-bind", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts, including production's rejection of a Path argument to path().
Rules simulatorOwn-keys-only map membership and constructor-access denial (RULES-B7) in rules — 'toString' in map is false, .constructor access errors (no JS prototype-chain leakage)
oracle:rules-firestore-prototype-chain-keys — production Firestore Rules Test API verdicts for corpus scenario "prototype-chain-keys", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorRange-slice [i:j] syntax for List and String (Item 4) in rules
oracle:rules-firestore-range-slice-list-and-string — production Firestore Rules Test API verdicts for all ten corpus cases in "range-slice-list-and-string", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts, including out-of-bounds slice-end rejection for both lists and strings.
Rules simulatorSet.difference()/union()/intersection() (Item 5.1) in rules
oracle:rules-firestore-set-algebra-difference-union-intersection captures the receiver and argument boundary in twelve production cases: Map.keys() is a List and rejects Set-only algebra, explicit toSet() receivers ALLOW difference/union/intersection with Set arguments, and a wrong-result control DENIES. The local simulator replays every verdict and the per-construct acceptance probes use the same positive toSet() shapes. (Structured evidence: oracle-conformance.test.ts)
Rules simulatorString-literal escape handling feeding matches() (Class B) in rules — \\. is unescaped before RE2 compilation, not forwarded raw to a JS RegExp
oracle:rules-firestore-string-literals-and-regex — production Firestore Rules Test API verdicts for corpus scenario "string-literals-and-regex", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorMissing-field access as a runtime error (RULES-B2) in rules — typo == null denies (missing access is not null); !(key in map) is the real absence check
oracle:rules-firestore-undefined-field-access — production Firestore Rules Test API verdicts for corpus scenario "undefined-field-access", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorExplicit UNSUPPORTED reporting for unimplemented built-ins (Item 0.A) in rules — an unimplemented built-in abstains rather than silently DENYing
oracle:rules-firestore-unsupported-feature-witness — production Firestore Rules Test API verdicts for corpus scenario "unsupported-feature-witness", replayed verdict-for-verdict against the local rules simulator by unit:rules/oracle-conformance.test.ts; all cases match production.
Rules simulatorRequired-fields validation idiom in rules — request.resource.data.keys().hasAll()/hasOnly() and the diff().addedKeys()/removedKeys()/changedKeys()/affectedKeys()/unchangedKeys() MapDiff family gating field-level mutations
NEW ROW, 2026-07-12: production capture proves the simulator matches the production Firestore Rules Test API verdict-for-verdict on all 7 cases (exact required-field set on create, field-scoped update via MapDiff). oracle:rules-firestore-required-fields-and-mapdiff — replayed by unit:rules/oracle-conformance.test.ts.
Rules simulatorString.split()/lower()/upper()/trim()/replace()/size(), List.hasAll()/hasAny()/hasOnly()/join(), and Map.values() in rules
NEW ROW, 2026-07-12: production capture proves the simulator matches the production Firestore Rules Test API verdict-for-verdict on all 4 cases (CSV-split membership, slug/title/code normalization). oracle:rules-firestore-list-and-string-methods — replayed by unit:rules/oracle-conformance.test.ts.
Rules simulatorrequest.time Timestamp accessors (year/month/day/hours/minutes/seconds/nanos/dayOfWeek/dayOfYear/toMillis/date/time), math.floor/round/sqrt/pow/isNaN, int()/float()/string() casts, request.method, and >= / * operators in rules
NEW ROW, 2026-07-12: production capture proves the simulator matches the production Firestore Rules Test API verdict-for-verdict on all 3 cases (request.time pinned via requestTime for determinism). oracle:rules-firestore-time-math-and-casts — replayed by unit:rules/oracle-conformance.test.ts.
Rules simulatorlatlng.value().latitude()/longitude()/distance() and duration.value()/time()/abs() in rules
Production captures from 2026-07-12 and the 2026-07-21 recapture prove the simulator matches the Firestore Rules Test API verdict-for-verdict on all 3 cases: coordinate and duration bounds plus exact duration.value(1500, 'ms').seconds() / .nanos() components. oracle:rules-firestore-duration-and-latlng — replayed by unit:rules/oracle-conformance.test.ts.
Rules simulatorUser-defined function/let, granular allow get/list/update/write verbs, the ternary operator, and a recursive {document=**} match in rules
NEW ROW, 2026-07-12: production capture proves the simulator matches the production Firestore Rules Test API verdict-for-verdict on all 7 cases (role-gated docs + public recursive subtree). oracle:rules-firestore-functions-verbs-and-recursive — replayed by unit:rules/oracle-conformance.test.ts.
Rules simulatorResource identity (resource.id/resource.__name__/request.resource.id) on an EXISTING document (RULES-B12) — production leaves the identity keys ABSENT, so reading one errors → DENY, the error survives negation, and only a determining || operand absorbs it
NEW ROW, 2026-07-12: production capture proves the simulator matches the production Firestore Rules Test API verdict-for-verdict on all 13 cases. Production builds resource (and request.resource) from the supplied document alone and derives NO identity from the request path, so resource.id/resource.__name__/request.resource.id read as absent properties — verbatim Property id is undefined on object. / Property __name__ is undefined on object. — which absorb to DENY. The simulator previously SYNTHESIZED id/__name__ from the request path on get/list/update/delete, so resource.id == id ALLOWed where production DENIES: an over-permissive divergence, now removed. The negation cases (resource.id != 'zzz', !(resource.id == 'zzz')) pin that the absent read is a propagating error rather than a false, which an absent-as-undefined model would false-ALLOW. oracle:rules-firestore-resource-document-identity — replayed by unit:rules/oracle-conformance.test.ts.
Rules simulatorRequest-target resource on a missing document (RULES-B13) — get/update/delete existence guards, null comparison, and data access all error to DENY
NEW ROW, 2026-07-12: production capture proves the simulator matches the Firestore Rules Test API verdict-for-verdict on all 9 cases. A missing request target makes the resource global an error value: resource != null, resource == null, and resource.data.value all DENY. The simulator previously constructed { data: {} } for missing get/update/delete requests, so the ordinary resource != null existence guard false-ALLOWed all three operations. Modeling the target as raw null fixes that guard but makes resource == null false-ALLOW, so the evaluator now raises Null value error. when the absent resource identifier itself is evaluated. Existing-document get/update/delete and data-access controls still ALLOW. oracle:rules-firestore-resource-missing-document — replayed by packages/pyric/test/rules/oracle-conformance.test.ts.
Rules simulatorA ruleset with NO rules_version declaration — production accepts it and defaults to v1
NEW ROW, 2026-07-17: production capture proves a version-less ruleset deploys and evaluates (production defaults rules_version to v1; the shared grammar previously rejected the file at parse — PR #333 grammar fix, the same evidence that reclassified corpus file 001-missing-version.rules from invalid to edge-cases). oracle:rules-firestore-optional-rules-version matches the local simulator verdict-for-verdict on all 3 cases. (Structured evidence: oracle-conformance.test.ts)
Rules simulatorFunction declarations at GLOBAL and SERVICE scope in a Firestore ruleset, with match-scope shadowing
ROW CORRECTED, 2026-07-17 (same day): the simulator now seeds the match-tree walk with global- and service-scope function declarations (declaration order global → service → match preserves inner-shadows-outer), so the previously-abstaining cases evaluate. oracle:rules-firestore-global-and-service-scope-functions — production Firestore Rules Test API verdicts captured 2026-07-17 — now replays verdict-for-verdict against the local simulator with all 5 cases matching (shadowing included), asserted by unit:rules/oracle-conformance.test.ts. The prior unsupported row recorded the honest UNSUPPORTED abstention; that gap is closed (#346).
Rules simulatorHierarchical nested match composition — a child match resolves relative to its parent and grants only the exact nested document path
oracle:rules-firestore-hierarchical-match-cascade captures the exact nested child ALLOW plus parent, sibling, and over-deep DENY controls from the production Firestore Rules Test API, replayed verdict-for-verdict by packages/pyric/test/rules/oracle-conformance.test.ts.
Rules simulatorStrict boolean operands for &&, ||, and ternary control flow, plus create-time resource / request.resource semantics
oracle:rules-firestore-strict-boolean-control-flow captures three direct non-boolean DENYs, three tautological error discriminators that still DENY, a boolean ALLOW control, create-time resource == null DENY, and incoming request.resource.data ALLOW; replayed verdict-for-verdict by packages/pyric/test/rules/oracle-conformance.test.ts.
Rules simulatorThe exact six shared auth and membership function bodies preserve signed-in, owner, custom-claim, explicit membership-map, and role semantics under Firestore Rules
NEW ROW, 2026-07-21: one bounded, read-only Firestore Rules Test API request captured 12 verdicts for the exact bodies shipped from auth and membership; oracle:rules-firestore-common-auth-membership-firestore matches the local Firestore rules simulator verdict-for-verdict (6 ALLOW, 6 DENY). The cross-service source-lock test requires these six Firestore corpus bodies, the previously captured Storage corpus bodies, and the shipped modules to remain normalized-AST-identical. (Structured evidence: oracle-conformance.test.ts)
Rules simulatorIn multi-document batched writes and transactions, getAfter() deep-merges incoming updates with pre-existing sibling document state, preserving unmodified fields; updates targeting non-existent siblings project to null so existsAfter() returns false.
Captured production behavior via rules-firestore-atomic-batch-sibling-merge proves getAfter() evaluates against deep-merged sibling state during multi-document atomic updates. (Structured evidence: rules-firestore-atomic-batch-sibling-merge, oracle-conformance.test.ts, get-after-batch.test.ts)
Rules simulatorCompile-time AST verification rejects non-conforming casts (bool()), unsupported helper methods (debug(), math.isInfinite()), and bare Map membership assertions (map.hasAll/hasAny/hasOnly).
Captured production behavior via rules-firestore-ast-strictness-and-unsupported-casts proves compile-time AST verification and execution rejection of bool(), math.isInfinite(), debug(), and bare Map membership assertions. (Structured evidence: rules-firestore-ast-strictness-and-unsupported-casts, oracle-conformance.test.ts, hallucinations.test.ts)

Storage rules engine — parseStorageRules / evaluateStorageRules (rules-storage corpus)

RulesparseStorageRules(source) returns an opaque handle
unit:storage/sandbox/rules.test.ts ("parses the canonical session-archive ruleset")
RulesparseStorageRules rejects non-firebase.storage service headers
unit:storage/sandbox/rules.test.ts ("rejects unknown service header")
RulesevaluateStorageRules supports granular verbs (get/list/create/update/delete) alongside read/write umbrella expansion, comma-separated verb lists, and per-verb default-deny
STALE ROW, corrected 2026-07-10: production capture proves the evaluator already supports the full six-verb grant surface (umbrella read→{get,list}, write→{create,update,delete}, single granular grants, comma-separated grants, per-verb deny-by-default), matching production verdict-for-verdict on 12 of the scenario's 13 non-existence cases. oracle:rules-storage-verbs-umbrella-granular (all read/write/get/comma-verb cases). One related existence-semantics case in the same scenario diverges over-permissively and is carried by #118 Rules (the resource == null create-if-absent guard), not by this granular-verb row. (Structured evidence: rules-oracle-conformance.test.ts)
RulesparseStorageRules rejects unterminated string literals with SyntaxError
unit:storage/sandbox/rules.test.ts ("rejects unterminated strings")
RulesevaluateStorageRules matches match /sessions/{id} { allow read: if request.auth != null; } for an authed read
unit:storage/sandbox/rules-evaluator.test.ts ("allows authenticated reads of /sessions/{id}")
RulesevaluateStorageRules denies anonymous reads when the rule requires request.auth != null
RulesevaluateStorageRules supports request.resource.size < N constraints (with arithmetic literals like 10 1024 1024)
unit:storage/sandbox/rules-evaluator.test.ts ("allows JSON writes under 10MB")
RulesevaluateStorageRules supports request.resource.contentType == '<mime>' constraints
unit:storage/sandbox/rules-evaluator.test.ts (mime constraint inside the session-archive ruleset)
RulesMulti-segment wildcard {allPaths=**} matches zero-or-more remaining segments
unit:storage/sandbox/rules-evaluator.test.ts (parser + evaluator both honor the ** form)
RulesPath-parameter binding ({sessionId}) accessible inside the if expression
RulesUser-defined function definitions — let bindings, functions calling functions, and match-block-scoped helper functions (lexical scoping)
STALE ROW, corrected 2026-07-10: production capture proves the evaluator supports user-defined functions with let bindings, nested function calls, and block-scoped helpers. oracle:rules-storage-functions-let-scope matches production verdict-for-verdict on all 5 cases. Same-name shadowing and undefined-function calls are compile-time rejections in production and are covered by evaluator unit tests instead (they cannot be captured as a clean production verdict). (Structured evidence: rules-oracle-conformance.test.ts)
Rulesrequest.time compared against timestamp.date(y,m,d) and timestamp.value(ms) constructors
NEW ROW, 2026-07-10: production capture proves the evaluator supports request.time comparisons against both timestamp constructors. oracle:rules-storage-request-time-timestamp matches production verdict-for-verdict on all 4 cases (deadline-before/after via timestamp.date(), epoch-bound before/after via timestamp.value()). (Structured evidence: rules-oracle-conformance.test.ts)
Rulesstring.matches(regex) with whole-string anchoring (a partial match denies)
NEW ROW, 2026-07-10: production capture proves matches() is whole-string anchored, matching a RE2 pattern only when it covers the entire string. oracle:rules-storage-matches-regex matches production verdict-for-verdict on all 3 cases. RE2-inexpressible patterns are rejected at ruleset compile time by production and are covered by evaluator unit tests instead. (Structured evidence: rules-oracle-conformance.test.ts)
Rulesresource.metadata.<key> custom-metadata access in dotted (resource.metadata.owner) and bracket (resource.metadata['owner']) form, including missing-key deny
NEW ROW, 2026-07-10: production capture proves dotted and bracket metadata access resolve identically, and a missing key denies. oracle:rules-storage-metadata-access matches production verdict-for-verdict on all 5 cases. (Structured evidence: rules-oracle-conformance.test.ts)
RulesCross-service firestore.get() / firestore.exists() lookups from a Storage ruleset, with $(expr) path interpolation and qualified function-mock names
NEW ROW, 2026-07-10: production capture proves the evaluator resolves cross-service Firestore lookups from Storage rules, including interpolated document paths and both the map-returning get() and bool-returning exists() forms. oracle:rules-storage-firestore-lookup matches production verdict-for-verdict on all 4 cases. (Structured evidence: rules-oracle-conformance.test.ts)
Rulesresource.name (full object path), resource.timeCreated, resource.updated, resource.bucket — the object-identity and time fields of the existing-object binding, plus duration.value(n, unit)
ROW CORRECTED, 2026-07-12: the fields are now modeled, sourced from the persisted object record. oracle:rules-storage-resource-object-identity matches production verdict-for-verdict on all 9 cases — an extension guard (resource.name.matches), a freshness window (resource.timeCreated + duration.value(1, 'h')), an immutability check (resource.timeCreated == resource.updated), and an absent-property negation. Production semantics pinned by the capture: resource.name is the object's FULL path within the bucket (GCS object-name convention, sourced from the persisted record's fullPath, not the client SDK's last-segment name); timeCreated/updated are timestamps (production rejects an int with "Unsupported operation error. Received: int < timestamp"); the update-time field is updated — there is NO resource.timeUpdated. An absent property is an evaluation error that absorbs to DENY and SURVIVES negation, so resource.name != 'x' on an object with no name denies rather than false-allowing. The prior row recorded these fields as an unsupported gap; that gap is closed. (Structured evidence: rules-oracle-conformance.test.ts)
Rulesrequest.resource.size arithmetic bounds (+/-///, >=/<=), request/existing content-type and custom metadata checks, request.method/request.path, a request.auth.token custom-claim branch, granular allow list, and a recursive {allPaths=*} public read tree
NEW ROW, 2026-07-12: production capture proves the evaluator matches the production Storage Rules Test API verdict-for-verdict on all 8 cases (owner-scoped size-bounded upload, immutable-content-type update, list grant, public recursive read). oracle:rules-storage-metadata-verbs-and-arithmetic — replayed by packages/pyric/test/storage/rules-oracle-conformance.test.ts.
Rulesresource on a create where no object exists: the create-if-absent guard allow create: if resource == null
GAP CLOSED, 2026-07-21: production raises a null-value error when resource is referenced on a create where no object exists, including resource == null, so the expression denies. The evaluator now represents the missing binding as a Rules error rather than a usable null. oracle:rules-storage-verbs-umbrella-granular replays the captured DENY directly with no divergence pin; unit:storage/sandbox/rules-evaluator.test.ts covers the evaluator seam. (Structured evidence: rules-oracle-conformance.test.ts)
RulesTernary ?: — branch selection, error-in-condition deny, and error || true absorption
NEW ROW, 2026-07-17: production capture proves the evaluator selects ternary branches by condition, denies when the CONDITION errors (even with identical true branches — production errors the whole conditional rather than falling through to a branch), and still absorbs the same error through || true. oracle:rules-storage-ternary-and-error-absorption matches production verdict-for-verdict on all 5 cases. A division-by-zero in the condition previously yielded JS Infinity (no error) and false-ALLOWed the erroring-condition case; zero divisors now error-and-deny. (Structured evidence: rules-oracle-conformance.test.ts)
Rulesin on list literals (membership) and on maps (OWN-key membership) — prototype names (toString/constructor/hasOwnProperty) are never keys
NEW ROW, 2026-07-17: production capture proves in tests list membership and map OWN keys only — 'toString' in metadata is false unless the map literally carries that key, for both custom-metadata maps and map literals. oracle:rules-storage-in-membership-and-proto-keys matches production verdict-for-verdict on all 8 cases. Pre-fix the evaluator used JS in, which walks the prototype chain and false-ALLOWed the proto-key cases (the same bug class the Firestore simulator fixed under RULES-B7); the own-key check now guards it, pinned by unit:storage/sandbox/rules-values.test.ts JS-semantics guards. (Structured evidence: rules-oracle-conformance.test.ts)
Rulesis type tests over the storage bindings (size is int, contentType is string, metadata is map, split() is list) and int/float LITERAL typing (1.0 is float, 1 is int, cross checks false)
ROW CORRECTED, 2026-07-17 (same day): the evaluator now adopts the simulator's RULES-B5 float model — a RulesFloat wrapper tags float values (a numeric literal written with a decimal point converts to one) while a bare JS number means int — so 1.0 is float / !(1.0 is int) type by literal form exactly as production does. oracle:rules-storage-type-checks-is now matches production verdict-for-verdict on all 3 cases with no knownGap abstentions. The prior same-day row recorded the value-typed divergence; that gap is closed. (Structured evidence: rules-oracle-conformance.test.ts)
RulesList/map literals, range-slice access [i:j] on lists and strings (out-of-range bounds ERROR, no JS clamp), and structural == on lists
NEW ROW, 2026-07-17: production capture proves slices evaluate on split() lists and on strings, that an out-of-range slice END errors and denies (production does NOT clamp to length the way JS .slice() does — same semantics the Firestore capture pinned in rules-firestore-range-slice-list-and-string), and that a slice compares equal to a list literal structurally. oracle:rules-storage-list-map-literals-and-slice matches production verdict-for-verdict on all 6 cases. Pre-fix the evaluator clamped bounds JS-style and compared lists by reference identity; both fixed and pinned by unit:storage/sandbox/rules-values.test.ts JS-semantics guards. (Structured evidence: rules-oracle-conformance.test.ts)
RulesFloat literals, %, unary minus, float division, negative-dividend modulo (-7 % 2 == -1), and TRUNCATING int division (10 / 4 == 2, -7 / 2 == -3)
ROW CORRECTED, 2026-07-17 (same day): with the RULES-B5 float model adopted from the simulator, int ÷ int truncates toward zero, float division stays float (re-tagged), and int-op-float promotes to float. oracle:rules-storage-float-modulo-unary-minus now matches production verdict-for-verdict on all 6 cases with no knownGap abstentions. An int zero divisor errors-and-denies; a float zero divisor follows the simulator's CEL-pinned ±Infinity/NaN. The prior same-day row recorded the truncation divergence; that gap is closed. (Structured evidence: rules-oracle-conformance.test.ts)
RulesFunction declarations at GLOBAL scope (above service) and SERVICE scope (inside service, outside any match), with inner-shadows-outer resolution
NEW ROW, 2026-07-17: production capture proves production accepts functions declared at global and service scope and resolves a match-scope function over a same-named global (inner shadows outer). oracle:rules-storage-function-scopes-and-shadowing matches production verdict-for-verdict on all 5 cases. The shared grammar previously rejected both placements at parse (PR #333 grammar fix); the storage evaluator attaches global- and service-scope functions to the root match block, which the capture proves is verdict-equivalent to production's scoping for this surface. (Structured evidence: rules-oracle-conformance.test.ts)
Rules2+modules Storage rules resolve through service-aware contracts: the common auth and membership exports are admitted and retain their production auth, claim, map-membership, and role semantics; Firestore-only helpers are rejected before evaluation
NEW ROW, 2026-07-20; paired proof added 2026-07-21: one targeted production Rules Test API capture proves the exact six bundled function bodies (isAuthenticated, isOwner, hasClaim, hasClaimRole, isMemberOf, hasRole) under firebase.storage, matching the local evaluator verdict-for-verdict on 12 allow/deny cases. oracle:rules-storage-common-auth-membership supplies the Storage answer key; Firestore Rules row #189 independently captures the same exact bodies under cloud.firestore (12 matching verdicts). A separately authored, explicitly reconstructed normalized-AST digest locks the captured Storage corpus bodies, and the source-lock test requires the Firestore corpus and shipped modules to remain AST-identical. Resolver tests separately prove 2+modules lowers these imports to plain v2 and fails closed with INCOMPATIBLE_FUNCTION for a Firestore-only export and for a transitive helper that reads resource.data; the Storage service integration test proves normal setup invokes that checked resolver. (Structured evidence: storage-stdlib-source-lock.test.ts, rules-oracle-conformance.test.ts, resolver.test.ts, enforce.test.ts)
RulesStorage-native upload primitives at security boundaries: inclusive byte maximums, exact and whole-string MIME checks, metadata-only updates with unchanged bytes/owner, string path wildcards, generation/metageneration identity, and strict creation-time windows
NEW ROW, 2026-07-20: oracle:rules-storage-upload-primitives-boundaries captures 26 production cases in one Rules Test API request. The local evaluator matches every case scoped to this row: size 0/exact/+1, MIME exact/case/parameter and regex anchoring, metadata update invariants, filename wildcard typing, exact/absent generation identity, and request.time just before/at/after a strict 60-second boundary plus a future timeCreated fixture. Metadata collection methods and missing request.resource are separated into divergence rows #127/#128 rather than contaminating this credit. (Structured evidence: rules-oracle-conformance.test.ts)
RulesStorage custom-metadata collection methods: Map.keys().hasAll() for required-key policy and Map.get(key, default) for an absent key
GAP CLOSED, 2026-07-21: the Storage evaluator now implements own-key Map.keys(), structural hasAll(), and string-key Map.get(key, default). oracle:rules-storage-upload-primitives-boundaries replays the production required-key and absent-key-default cases directly with no divergence pin; public-seam unit cases cover both admitted forms. (Structured evidence: rules-oracle-conformance.test.ts)
Rulesrequest.resource on delete where no incoming object exists: request.resource == null
GAP CLOSED, 2026-07-21: production errors when request.resource is absent on delete, including a direct null comparison. The evaluator now carries the absent incoming binding as a Rules error. oracle:rules-storage-upload-primitives-boundaries replays the captured DENY directly with no divergence pin; unit:storage/sandbox/rules-evaluator.test.ts covers the evaluator seam. (Structured evidence: rules-oracle-conformance.test.ts)
RulesMocked Storage-to-Firestore lookup evaluation: get/exists return and missing-field errors, repeated and same-resolved-path calls, get+exists composition, and lazy ||/&&/ternary branches with hosted function-call diagnostics
DOCUMENTED MOCK-ORACLE DIVERGENCE, 2026-07-21: oracle:rules-storage-firestore-lookup-budget matches the local evaluator on 13 of 14 cases and hosted diagnostics prove lazy branches omit lookup calls. The one mismatch is intentional and pinned on both sides: the Rules Test API ALLOWs three distinct mocked exists calls, while the evaluator DENYs the third distinct path to match the separately captured real-resource production contract in row #131. This row remains diverged because its mocked observation is not verdict-identical; it supplies no budget credit. (Structured evidence: rules-oracle-conformance.test.ts)
RulesAnonymous request.auth != null used as a ternary condition before a cross-service lookup
GAP CLOSED, 2026-07-21: anonymous request.auth is now represented as an absent-property Rules error, so a ternary condition denies before selecting either lookup branch, matching hosted diagnostics. oracle:rules-storage-firestore-lookup-budget replays the captured DENY directly with no divergence pin; unit:storage/sandbox/rules-methods.test.ts covers the lookup evaluator seam. (Structured evidence: rules-oracle-conformance.test.ts)
RulesReal-resource Storage-to-Firestore contract: at most two distinct document paths per evaluation with repeated-path caching; typed document data and missing-document errors; default-database-only and owning-project isolation; lazy/helper/let composition; and independence from Firestore client rules
NEW ROW, 2026-07-21: four deploy-observe-restore captures establish the real-resource contract beyond Rules Test API mocks: oracle:stdlib-realstorage-p3-lookup-budget-iam-enabled, oracle:stdlib-realstorage-p3-advanced-iam-enabled, oracle:stdlib-realstorage-p3-named-database, and oracle:stdlib-realstorage-p3-project-isolation. unit:conformance/storage-stdlib-real-replay.test.ts executes the locally decidable lookup matrices and locks reviewed reconstructions of their normalized probe code; the lock records state whether the digest covers an injected block or a canonical synthetic wrapper. The surrounding deployed base rules were not retained, so no full-source digest is claimed for these historical captures. Future rigs emit both probe-block and full deployed-rules-file digests. unit:storage/enforce.test.ts proves the normal enforcement seam reads only its owning sandbox and bypasses Firestore client rules. The separate consistency capture remains bounded characterization and is not promoted into a latency guarantee.
RulesThe service-scoped storage/uploads, storage/metadata, storage/objects, and storage/time modules resolve through 2+modules and preserve their production upload-bound, MIME, metadata, operation-identity, and strict timestamp-window semantics
NEW ROW, 2026-07-21: one bounded Rules Test API batch captures 15 verdicts for the exact 13 deployable function bodies in the four shipped modules (oracle:rules-storage-stdlib-storage-modules). A separately authored, explicitly reconstructed normalized-AST digest locks the captured corpus bodies, and the source-lock test requires the shipped modules to remain AST-identical. The generic Storage stdlib fixture runner and oracle replay execute those bodies locally; resolver tests prove the modules are admitted only for firebase.storage, lower to plain v2, and fail closed under Firestore. The module surface intentionally excludes the advanced integrity, metadata-diff, and cross-service lookup candidates. (Structured evidence: storage-stdlib-source-lock.test.ts, rules-oracle-conformance.test.ts, stdlib-cases.test.ts, stdlib.test.ts)

RTDB rules engine — production simulator conformance (rules-rtdb corpus)

Rules simulatorauth != null gate on .read/.write — authed ops allow, signed-out ops deny
oracle:rules-rtdb-r1-auth-only — production verdicts captured by deploy-observe-restore against the live oracle database (RTDB has no server-side rules test API), replayed verdict-for-verdict against the in-process simulator by packages/pyric/test/rules/rtdb/rules-conformance.test.ts; all 4 cases match production.
Rules simulator$uid path-variable ownership ($uid === auth.uid) — the owner path allows, a foreign uid and an anonymous request deny
oracle:rules-rtdb-r2-own-uid — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict against the in-process simulator; all 5 cases match production, so the simulator binds the path variable against auth.uid exactly as production does. (Structured evidence: rules-conformance.test.ts)
Rules simulatorWrite-rule CASCADE — a truthy ancestor .write grants the write regardless of a deeper !data.exists() rule, so the populated-path write ALLOWS
oracle:rules-rtdb-r3-data-exists — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict. Production ALLOWS the write to the populated path even though the child rule is !data.exists(): the ancestor .write: auth != null already granted it, and RTDB never consults a deeper rule to revoke a grant. All 3 cases match; the simulator models the same cascade. (Structured evidence: rules-conformance.test.ts)
Rules simulator.validate VETO — a failing child .validate denies a write the .write rule would otherwise permit (validate does not cascade and cannot be overridden)
oracle:rules-rtdb-r4-validate-structure — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict; both cases match production. This row was the corpus's one historical simulator-vs-production divergence: at the 2026-05-18 agreement capture the simulator ALLOWED the missing-body write because it did not veto on the child .validate. The simulator now denies it, and this fresh capture re-confirms production's DENY, so the divergence is RESOLVED and needs no pin. (Structured evidence: rules-conformance.test.ts)
Rules simulatorCascade is GRANT-ONLY — a root .read: true grants every descendant read (authed and anonymous), and a deeper .write: false cannot revoke a truthy ancestor .write grant
oracle:rules-rtdb-r5-cascade-root-grant — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict; all 4 cases ALLOW in production, including the write to a node whose own .write is literal false. The simulator reproduces grant-only cascade rather than treating the deeper false as a veto. (Structured evidence: rules-conformance.test.ts)
Rules simulatorLiteral .read/.write: false — the deny-all baseline denies every op, authed or anonymous, read or write
oracle:rules-rtdb-r6-deny-everything — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict; all 3 cases DENY in production and in the simulator. (Structured evidence: rules-conformance.test.ts)
Rules simulatorNested path variable in an expression ($sessionId === auth.uid) — the matching session allows, a mismatched session denies, and an unmatched anonymous op denies with no matching rule
oracle:rules-rtdb-r7-pathvar-binding — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict; all 4 cases match. The simulator binds the nested variable identically and returns NO_MATCHING_RULE (read as deny) where production denies for want of a rule. (Structured evidence: rules-conformance.test.ts)
Rules simulatornewData is projected at the RULE node, not the written path — a root .write reading newData.hasChildren(['owner']) sees {item:{...}} for a write one level deeper, so the predicate is false and the write DENIES
oracle:rules-rtdb-r8-combined-check — production verdicts captured by deploy-observe-restore, replayed verdict-for-verdict. Production DENIES all three writes, including the "matching owner" case whose owner field lives under /item rather than at the rule node; only the auth-gated read allows. All 4 cases match, so the simulator projects newData at the same node production does — rule placement relative to the written path decides the outcome. (Structured evidence: rules-conformance.test.ts)
Rules simulatorArithmetic and comparison operators in quota rules — + - * / %, > >= < <=, !==, loose ==, ||, the ternary, and unary minus
NEW ROW, 2026-07-12: production capture proves the simulator evaluates every arithmetic and ordering operator as production does, across increment-only counters, doubling and halving deltas, a parity check, inclusive and exclusive range caps, a negative sentinel, loose equality against zero, a disjunction, and a ternary tier bound. oracle:rules-rtdb-r9-quota-arithmetic matches production verdict-for-verdict on all 23 cases. (Structured evidence: rules-conformance.test.ts)
Rules simulatorSnapshot structure and type guards — hasChild, parent(), isString, isNumber, isBoolean
NEW ROW, 2026-07-12: production capture proves the simulator resolves the type predicates and the parent() path climb as production does, including RTDB's rule that a child .validate runs only for children PRESENT in newData — the name-without-age write is denied by that field's own parent guard, not by the absent sibling's type rule, which never runs. oracle:rules-rtdb-r10-snapshot-type-guards matches production verdict-for-verdict on all 7 cases. (Structured evidence: rules-conformance.test.ts)
Rules simulatorString methods on a snapshot value — beginsWith, contains, endsWith, matches with a regex literal, .length, toLowerCase, toUpperCase, and replace-ALL replace
NEW ROW, 2026-07-12: production capture proves String.replace substitutes EVERY occurrence of the substring, not only the first — production ALLOWS the write whose rule is newData.val().replace('_', '-') === 'a-b-c' for the value a_b_c, which holds only under replace-all. The simulator delegated to JavaScript's String.prototype.replace, which given a string pattern substitutes only the first occurrence, and DENIED that write; the substitution now replaces every occurrence and the two-underscore case guards the regression. The same capture proves toUpperCase is a valid production rule method that the simulator's validator previously rejected outright as unknown. oracle:rules-rtdb-r11-string-validation matches production verdict-for-verdict on all 18 cases. (Structured evidence: rules-conformance.test.ts)
Rules simulatornow (the server evaluation clock) and auth.token claims, read both dotted (auth.token.firebase.sign_in_provider) and subscripted (auth.token['firebase']['sign_in_provider'])
NEW ROW, 2026-07-12: production capture proves the simulator resolves the server clock and the decoded ID-token claims as production does, through both access forms. The subscript's receiver is load-bearing: production's rules compiler ACCEPTS auth.token[...], a map indexed by key, but REJECTS the same subscript applied to a snapshot value — newData.val()['kind'] fails to compile with "No such method/property" — so [] is a map accessor, not a general indexing operator. oracle:rules-rtdb-r12-server-time-and-token matches production verdict-for-verdict on all 7 cases. (Structured evidence: rules-conformance.test.ts)
Rules simulatorgetPriority() on a plain set (an unset priority reads null) and the .indexOn query-planner directive
NEW ROW, 2026-07-12: production capture proves a plain set leaves a node's priority unset, so getPriority() reports null in production exactly as the simulator does, and that a ruleset carrying .indexOn deploys and evaluates with every verdict intact. The scope is exactly that: .indexOn never changes an allow/deny verdict, so the twin evidences only that production's compiler accepts and deploys the directive, and it says nothing about query behavior. A NON-null priority stays unverified because writing one requires setWithPriority, outside the corpus op vocabulary of read and set. oracle:rules-rtdb-r13-priority-and-index-directive matches production verdict-for-verdict on all 4 cases. (Structured evidence: rules-conformance.test.ts)
Rules simulatorThe root binding — root.child(...).exists() resolved against the database root
NEW ROW, 2026-07-12: production capture proves the simulator resolves the root binding and reports a missing child as absent exactly as production does. The scope is bounded by the rig: the capture runner mounts the scenario subtree under a per-run randomly-named namespace while the replay suite mounts it at the root, so a rule naming any PRESENT path through root would address a different node on each side and diverge for harness reasons. Only a sentinel key absent under BOTH roots is mount-invariant, so that is what this row verifies; a populated root lookup remains unverified and needs a rig that can seed outside the scenario subtree and mount both sides at the same depth. oracle:rules-rtdb-r14-root-lookup matches production verdict-for-verdict on all 3 cases. (Structured evidence: rules-conformance.test.ts)
Rules simulator.validate reaches ancestors of the written path — each applicable rule is evaluated against the merged post-write value at its own location
oracle:rules-rtdb-r15-validate-ancestor-scope — production DENIES a deep write that leaves its validated ancestor without the required child, while the same write under a rule-free ancestor ALLOWS and the write ALLOWS once the ancestor requirement is seeded. The simulator now walks validation from the root through the write location against the merged post-write tree and matches all 5 captured production cases. (Structured evidence: rules-conformance.test.ts)

Current gaps

Documented divergences

Known differences between Pyric and production Firebase. Each remains tracked as a non-conforming row.

RulesMocked Storage-to-Firestore lookup evaluation: get/exists return and missing-field errors, repeated and same-resolved-path calls, get+exists composition, and lazy ||/&&/ternary branches with hosted function-call diagnostics
DOCUMENTED MOCK-ORACLE DIVERGENCE, 2026-07-21: oracle:rules-storage-firestore-lookup-budget matches the local evaluator on 13 of 14 cases and hosted diagnostics prove lazy branches omit lookup calls. The one mismatch is intentional and pinned on both sides: the Rules Test API ALLOWs three distinct mocked exists calls, while the evaluator DENYs the third distinct path to match the separately captured real-resource production contract in row #131. This row remains diverged because its mocked observation is not verdict-identical; it supplies no budget credit. (Structured evidence: rules-oracle-conformance.test.ts)