@pyric/cli 0.1.0-alpha.28 → 0.1.0-alpha.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assurance/.generated/conformance-verdicts.d.ts +16 -0
- package/dist/assurance/.generated/conformance-verdicts.d.ts.map +1 -1
- package/dist/assurance/.generated/conformance-verdicts.js +16 -0
- package/dist/assurance/.generated/conformance-verdicts.js.map +1 -1
- package/dist/bridge/surface/methods/assurance/replaySession.d.ts.map +1 -1
- package/dist/bridge/surface/methods/assurance/replaySession.js +18 -6
- package/dist/bridge/surface/methods/assurance/replaySession.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +1 -1
- package/dist/conformance/.generated/can-i-use.js.map +1 -1
- package/dist/conformance/.generated/conformance-docs.d.ts.map +1 -1
- package/dist/conformance/.generated/conformance-docs.js +1 -1
- package/dist/conformance/.generated/conformance-docs.js.map +1 -1
- package/dist/serve/site-ui/_astro/{RulesCodeEditor.kBYcclwV.js → RulesCodeEditor.Cp_-FK3b.js} +1 -1
- package/dist/serve/site-ui/_astro/app.C--T2qDK.js +151 -0
- package/dist/serve/site-ui/_astro/app.DqyhRyrU.js +1 -0
- package/dist/serve/site-ui/_astro/{chess-showcase.CZe_3fJZ.js → chess-showcase.CVhCNUi_.js} +1 -1
- package/dist/serve/site-ui/_astro/{example-runtime.CXSZXHZF.js → example-runtime.zC1l-Yka.js} +1 -1
- package/dist/serve/site-ui/_astro/{sandbox-controls.Bp5PoonS.js → sandbox-controls.Bv6lfpIe.js} +39 -39
- package/dist/serve/site-ui/_astro/{seed.BO5CXcqE.js → seed.B4vza_Bv.js} +1 -1
- package/dist/serve/site-ui/_astro/{writes.DHSW22XD.js → writes.Bg0YEEA7.js} +1 -1
- package/dist/serve/site-ui/auth/index.html +1 -1
- package/dist/serve/site-ui/docs/database-compat/index.html +17 -17
- package/dist/serve/site-ui/docs/database-compat.md +17 -17
- package/dist/serve/site-ui/docs/index.json +10 -10
- package/dist/serve/site-ui/docs/pyric-rules-reference-api/index.html +119 -119
- package/dist/serve/site-ui/docs/pyric-rules-reference-api.md +53 -53
- package/dist/serve/site-ui/docs/rules-compat/index.html +67 -3
- package/dist/serve/site-ui/docs/rules-compat.md +67 -3
- package/dist/serve/site-ui/docs/secure/rtdb-rules-standard-library/index.html +17 -4
- package/dist/serve/site-ui/docs/secure/rtdb-rules-standard-library.md +19 -4
- package/dist/serve/site-ui/docs/secure/simulate-and-lint/index.html +4 -4
- package/dist/serve/site-ui/docs/secure/simulate-and-lint.md +8 -8
- package/dist/serve/site-ui/examples/chess/index.html +1 -1
- package/dist/serve/site-ui/firestore/index.html +1 -1
- package/dist/serve/site-ui/rtdb/index.html +1 -1
- package/dist/serve/site-ui/settings/index.html +1 -1
- package/dist/serve/site-ui/storage/index.html +1 -1
- package/dist/serve/site-ui/studio/index.html +1 -1
- package/dist/serve/site-ui/traffic/index.html +1 -1
- package/dist/serve/worker/client/rtdb-info.d.ts +0 -2
- package/dist/serve/worker/client/rtdb-info.d.ts.map +1 -1
- package/dist/serve/worker/client/rtdb-info.js +4 -17
- package/dist/serve/worker/client/rtdb-info.js.map +1 -1
- package/dist/serve/worker/client/rtdb-reads.d.ts.map +1 -1
- package/dist/serve/worker/client/rtdb-reads.js +3 -5
- package/dist/serve/worker/client/rtdb-reads.js.map +1 -1
- package/package.json +4 -4
- package/src/assurance/.generated/conformance-verdicts.ts +16 -0
- package/src/bridge/surface/methods/assurance/replaySession.ts +19 -6
- package/src/conformance/.generated/can-i-use.ts +1 -1
- package/src/conformance/.generated/conformance-docs.ts +1 -1
- package/src/serve/worker/client/rtdb-info.ts +4 -17
- package/src/serve/worker/client/rtdb-reads.ts +3 -4
- package/dist/serve/site-ui/_astro/app.BCNb6gMq.js +0 -1
- package/dist/serve/site-ui/_astro/app.DhoRPDdN.js +0 -147
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
</details>
|
|
379
379
|
<details class="compat-row" data-status="ok">
|
|
380
380
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>now</code> (the server evaluation clock) and <code>auth.token</code> claims, read both dotted (<code>auth.token.firebase.sign_in_provider</code>) and subscripted (<code>auth.token['firebase']['sign_in_provider']</code>)</span></span></span></summary>
|
|
381
|
-
<div class="compat-evidence"><div class="compat-probe">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 <code>auth.token[...]</code>, a map indexed by key, but REJECTS the same subscript applied to a snapshot value: <code>newData.val()['kind']</code> fails to compile with "No such method/property", so <code>[]</code> is a map accessor, not a general indexing operator. <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r12-server-time-and-token.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r12-server-time-and-token</code></a> matches production verdict-for-verdict on all 7 cases. Extended 2026-10-07: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r27-stdlib-core-patterns.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r27-stdlib-core-patterns</code></a> records production ALLOWING a written <code>{ ".sv": "timestamp" }</code> under <code>newData.val() == now</code> and DENYING a client clock time there: the server resolves the server value before the rules run. <code>simulate</code> resolves a written <code>{ ".sv": "timestamp" }</code> to its <code>now</code> and <code>{ ".sv": { "increment": n } }</code> to the stored value plus n before it evaluates, as the sandbox does at its write boundary (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/simulate-sandbox-agreement.test.ts#L43" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/simulate-sandbox-agreement.test.ts</code></a>), and the replay of that capture passes the client value through unchanged. Extended 2026-10-07: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r28-stdlib-presence-timing.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r28-stdlib-presence-timing</code></a> records production ALLOWING a written <code>serverTimestamp()</code> under <code>newData.val() == now</code> as a set of the node, as a child in a set, and in a root multi-path update next to the data it stamps, and DENYING a client clock time in each place. The sandbox resolves a write's server timestamps and evaluates its rules at one instant of its clock for set, update, multi-path update, onDisconnect registration and transactions (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/server-timestamp-now.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:database/server-timestamp-now.test.ts</code></a>, which advances the clock on every read); before, it read the clock twice and could deny the write. Production verdicts for a server timestamp in an onDisconnect write or a transaction are not captured. The simulator and the sandbox match production on all 28 cases of that capture. Extended 2026-10-07: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r34-stdlib-auth-membership-quota.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r34-stdlib-auth-membership-quota</code></a> signs in as Auth users the capture creates, through custom tokens, and records production reading the account's <code>auth.token.email_verified</code> and <code>auth.token.email</code> (ALLOWING a verified email and a verified email at the required domain, DENYING an unverified email, an email at another domain and an anonymous token without an email) and developer claims (ALLOWING <code>role</code> of 'admin' or 'editor' and <code>beta == true</code>, DENYING 'viewer', a missing claim and the string 'true'). The claim set follows <code>oracle:auth-id-token-standard-claims</code>. A token without a tenant fails <code>auth.token.firebase.tenant == 'acme'</code>; a tenant sign-in is not captured. The simulator and the sandbox match production on all 36 cases of that capture, which also cover roles and member lists stored in the database and a windowed quota. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#
|
|
381
|
+
<div class="compat-evidence"><div class="compat-probe">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 <code>auth.token[...]</code>, a map indexed by key, but REJECTS the same subscript applied to a snapshot value: <code>newData.val()['kind']</code> fails to compile with "No such method/property", so <code>[]</code> is a map accessor, not a general indexing operator. <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r12-server-time-and-token.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r12-server-time-and-token</code></a> matches production verdict-for-verdict on all 7 cases. Extended 2026-10-07: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r27-stdlib-core-patterns.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r27-stdlib-core-patterns</code></a> records production ALLOWING a written <code>{ ".sv": "timestamp" }</code> under <code>newData.val() == now</code> and DENYING a client clock time there: the server resolves the server value before the rules run. <code>simulate</code> resolves a written <code>{ ".sv": "timestamp" }</code> to its <code>now</code> and <code>{ ".sv": { "increment": n } }</code> to the stored value plus n before it evaluates, as the sandbox does at its write boundary (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/simulate-sandbox-agreement.test.ts#L43" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/simulate-sandbox-agreement.test.ts</code></a>), and the replay of that capture passes the client value through unchanged. Extended 2026-10-07: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r28-stdlib-presence-timing.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r28-stdlib-presence-timing</code></a> records production ALLOWING a written <code>serverTimestamp()</code> under <code>newData.val() == now</code> as a set of the node, as a child in a set, and in a root multi-path update next to the data it stamps, and DENYING a client clock time in each place. The sandbox resolves a write's server timestamps and evaluates its rules at one instant of its clock for set, update, multi-path update, onDisconnect registration and transactions (<a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/server-timestamp-now.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:database/server-timestamp-now.test.ts</code></a>, which advances the clock on every read); before, it read the clock twice and could deny the write. Production verdicts for a server timestamp in an onDisconnect write or a transaction are not captured. The simulator and the sandbox match production on all 28 cases of that capture. Extended 2026-10-07: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r34-stdlib-auth-membership-quota.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r34-stdlib-auth-membership-quota</code></a> signs in as Auth users the capture creates, through custom tokens, and records production reading the account's <code>auth.token.email_verified</code> and <code>auth.token.email</code> (ALLOWING a verified email and a verified email at the required domain, DENYING an unverified email, an email at another domain and an anonymous token without an email) and developer claims (ALLOWING <code>role</code> of 'admin' or 'editor' and <code>beta == true</code>, DENYING 'viewer', a missing claim and the string 'true'). The claim set follows <code>oracle:auth-id-token-standard-claims</code>. A token without a tenant fails <code>auth.token.firebase.tenant == 'acme'</code>; a tenant sign-in is not captured. The simulator and the sandbox match production on all 36 cases of that capture, which also cover roles and member lists stored in the database and a windowed quota. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L33" target="_blank" rel="noopener noreferrer"><code>rules-conformance.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/stdlib/corpus-lock.test.ts#L32" target="_blank" rel="noopener noreferrer"><code>corpus-lock.test.ts</code></a>)</div></div>
|
|
382
382
|
</details>
|
|
383
383
|
<details class="compat-row" data-status="ok">
|
|
384
384
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>getPriority()</code> on a plain <code>set</code> (an unset priority reads null) and the <code>.indexOn</code> query-planner directive</span></span></span></summary>
|
|
@@ -386,7 +386,7 @@
|
|
|
386
386
|
</details>
|
|
387
387
|
<details class="compat-row" data-status="ok">
|
|
388
388
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">The <code>root</code> binding — <code>root.child(...).exists()</code> resolved against the database root</span></span></span></summary>
|
|
389
|
-
<div class="compat-evidence"><div class="compat-probe">NEW ROW, 2026-07-12: production capture proves the simulator resolves the <code>root</code> 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 <code>root</code> 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 <code>root</code> lookup remains unverified and needs a rig that can seed outside the scenario subtree and mount both sides at the same depth. <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r14-root-lookup.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r14-root-lookup</code></a> matches production verdict-for-verdict on all 3 cases. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#
|
|
389
|
+
<div class="compat-evidence"><div class="compat-probe">NEW ROW, 2026-07-12: production capture proves the simulator resolves the <code>root</code> 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 <code>root</code> 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 <code>root</code> lookup remains unverified and needs a rig that can seed outside the scenario subtree and mount both sides at the same depth. <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r14-root-lookup.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r14-root-lookup</code></a> matches production verdict-for-verdict on all 3 cases. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L33" target="_blank" rel="noopener noreferrer"><code>rules-conformance.test.ts</code></a>)</div></div>
|
|
390
390
|
</details>
|
|
391
391
|
<details class="compat-row" data-status="ok">
|
|
392
392
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>.validate</code> runs on the ancestors of each written path and on every node the written value carries, each against the merged post-write value at its own location; a sibling node the write does not carry is not validated</span></span></span></summary>
|
|
@@ -454,7 +454,7 @@
|
|
|
454
454
|
</details>
|
|
455
455
|
<details class="compat-row" data-status="ok">
|
|
456
456
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Evaluating <code>root.parent()</code> at runtime fails the rule expression as production does, so neither <code>== null</code> nor <code>|| true</code> can evaluate to ALLOW (COV-1); and static type validation accepts member access <code>.exists()</code> on a <code>Snapshot | Null</code> ternary <code>(true ? data.parent() : null).exists()</code> as production deploy does (VAL-1)</span></span></span></summary>
|
|
457
|
-
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r39-root-parent-runtime.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r39-root-parent-runtime</code></a> (captured 2026-10-15) records 7 production verdicts for <code>root.parent()</code> in ternary and <code>.exists()</code> expressions: production DENIES all 6 <code>root.parent()</code> cases (<code>(true ? root.parent() : null) == null</code>, <code>(true ? root.parent() : null) != null || true</code>, <code>((true ? root.parent() : 1) == 1) || true</code>, <code>(true ? root.parent() : 1) != 1</code>, <code>(true ? root.parent() : '').length == null || true</code>, and <code>root.parent().exists() || true</code>) and ALLOWS the non-root <code>(true ? data.parent() : null).exists()</code> control at <code>/nested/child</code>. <code>DataSnapshot.prototype.parent()</code> raises <code>RtdbRuleRuntimeError('Cannot call parent() on root.')</code> on the root snapshot (<code>COV-1</code>), and <code>ternaryResult('Snapshot', 'Null')</code> in <code>grammar/types.ts</code> returns <code>NullableSnapshot</code> (<code>VAL-1</code>), matching all 7 verdicts in <code>r39</code>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#
|
|
457
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r39-root-parent-runtime.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r39-root-parent-runtime</code></a> (captured 2026-10-15) records 7 production verdicts for <code>root.parent()</code> in ternary and <code>.exists()</code> expressions: production DENIES all 6 <code>root.parent()</code> cases (<code>(true ? root.parent() : null) == null</code>, <code>(true ? root.parent() : null) != null || true</code>, <code>((true ? root.parent() : 1) == 1) || true</code>, <code>(true ? root.parent() : 1) != 1</code>, <code>(true ? root.parent() : '').length == null || true</code>, and <code>root.parent().exists() || true</code>) and ALLOWS the non-root <code>(true ? data.parent() : null).exists()</code> control at <code>/nested/child</code>. <code>DataSnapshot.prototype.parent()</code> raises <code>RtdbRuleRuntimeError('Cannot call parent() on root.')</code> on the root snapshot (<code>COV-1</code>), and <code>ternaryResult('Snapshot', 'Null')</code> in <code>grammar/types.ts</code> returns <code>NullableSnapshot</code> (<code>VAL-1</code>), matching all 7 verdicts in <code>r39</code>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L33" target="_blank" rel="noopener noreferrer"><code>rules-conformance.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/simulator.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>simulator.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/validator-types.test.ts#L153" target="_blank" rel="noopener noreferrer"><code>validator-types.test.ts</code></a>)</div></div>
|
|
458
458
|
</details>
|
|
459
459
|
<details class="compat-row" data-status="ok">
|
|
460
460
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Dynamic indexing on <code>auth.token[expr]</code> when <code>expr</code> evaluates to <code>null</code> (a missing child value) fails the rule as production does rather than coercing <code>null</code> to the claim key <code>"null"</code> via <code>String(idx)</code> (FUZ-4)</span></span></span></summary>
|
|
@@ -469,6 +469,70 @@
|
|
|
469
469
|
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r43-deploy-expression-and-structure-probes.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r43-deploy-expression-and-structure-probes</code></a> (captured 2026-10-15, 14 dry-run deploy cases) records production ACCEPTING <code>1e309 > 100</code> (COV-2), <code>(true ? root.parent() : null) == null</code> and <code>((true ? root.parent() : 1) == 1) || true</code> (COV-1), and <code>auth.token[data.child('k').val()] == true</code> (FUZ-4), and REJECTING <code>root.parent() != null || true</code> (COV-1), <code>auth[data]</code>, <code>auth[true]</code>, <code>auth[null]</code> (FUZ-4, "Invalid property access on 'auth' object."), <code>".valdiate": false</code>, child values <code>true</code>, <code>null</code>, <code>42</code>, <code>[{".read": true}]</code> (CON-6, "Expected '{'."), and <code>".read": [true]</code> (CON-6, "Invalid rule expression."). <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r44-deploy-string-and-regex-probes.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r44-deploy-string-and-regex-probes</code></a> (captured 2026-10-15, 9 dry-run deploy cases) records production ACCEPTING <code>"\'"</code>, <code>"\x41"</code>, and raw tab in double-quoted rule strings (FUZ-1), <code>/+/</code>, <code>/[z-a]/</code>, <code>/a{2,1}/</code> (FUZ-2), and <code>/^[^/]+$/</code> (FUZ-8), and REJECTING a raw newline in a double-quoted rule string (FUZ-1, "unterminated string literal") and <code>matches(/(/)</code> (FUZ-2, "Illegal regular expression, expected ')' at position 5"). Both <code>rtdbRulesSourceRejection</code> and <code>localDeployVerdict</code> (<code>checkRtdbRules(() => compileRtdbRules(...))</code>) agree with production on 14/14 in <code>r43</code> and 9/9 in <code>r44</code>. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/source-rejection.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>source-rejection.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-literals-and-constant-comparisons.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>regex-literals-and-constant-comparisons.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/validator-types.test.ts#L16" target="_blank" rel="noopener noreferrer"><code>validator-types.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/simulator.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>simulator.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>rules-rtdb-replay.test.ts</code></a>)</div></div>
|
|
470
470
|
</details>
|
|
471
471
|
<details class="compat-row" data-status="ok">
|
|
472
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>matches()</code> evaluates a regular expression as production does: lookahead <code>(?=</code>, negative lookahead <code>(?!</code>, a backreference <code>\1</code> and a word boundary <code>\b</code> never match as JavaScript reads them (<code>\b</code> reads as the letter <code>b</code> and <code>\1</code> as the digit <code>1</code>), so <code>!matches()</code> over them grants, and <code>.</code> matches <code>\n</code> and <code>\r</code> (SP-2b, SP-8)</span></span></span></summary>
|
|
473
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r47-regex-runtime.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r47-regex-runtime</code></a> (captured 2026-10-26, 16 cases) records production DENYING a lookahead requiring a digit on <code>ab1</code>, a negative lookahead refusing <code>admin</code> on <code>guest</code>, a backreference on <code>aa</code> and a word boundary on <code>a cat sat</code>, and ALLOWING <code>/^a.b$/</code> on <code>a\nb</code>. <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r50-regex-unsupported-semantics.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r50-regex-unsupported-semantics</code></a> (captured 2026-10-26, 12 cases) records production ALLOWING <code>!matches()</code> over a negative lookahead on <code>guest</code>, a lookahead on <code>ab1</code>, a backreference on <code>aa</code> and a word boundary on <code>a cat sat</code>, ALLOWING <code>/\bcat\b/</code> on <code>bcatb</code> and <code>/^(a)\1$/</code> on <code>a1</code>, and ALLOWING <code>/^a.b$/</code> on <code>a\rb</code>. The simulator now parses production's dialect (<code>rules/rtdb/grammar/regex.ts</code>: a leading <code>^</code> and trailing <code>$</code> stripped, <code>.*</code> for a missing one, a backslash making the next character literal except <code>\d \w \s</code> and their capitals, <code>.</code> matching every character) and matches it with the shared linear-time NFA (<code>rules/simulator/regex-nfa.ts</code>), so it agrees with all 28 r47 and r50 cases, including <code>$</code> not matching before a trailing newline (SP-7 refuted), the <code>i</code> flag, an unanchored pattern and a lazy quantifier; <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/rules-conformance.test.ts</code></a> replays them with no pin. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-dialect.test.ts#L57" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/grammar/regex-dialect.test.ts</code></a> covers the dialect and <code>^(a+)+$</code> in linear time (LD-6), and <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/database/sandbox/rules-regex-dialect.test.ts#L13" target="_blank" rel="noopener noreferrer"><code>unit:database/sandbox/rules-regex-dialect.test.ts</code></a> covers the in-page sandbox. Fixed in #1247 stream S1.</div></div>
|
|
474
|
+
</details>
|
|
475
|
+
<details class="compat-row" data-status="ok">
|
|
476
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check refuses a regular expression production refuses to deploy: a <code>^</code> that is not the first character or a <code>$</code> that is not the last, and a Unicode property class <code>\p{..}</code> (SP-1, SP-2a)</span></span></span></summary>
|
|
477
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r46-deploy-regex-constructs.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r46-deploy-regex-constructs</code></a> (captured 2026-10-26, 9 dry-run deploy cases) records production REJECTING <code>/^red$|^blue$/</code> and <code>/(^a)/</code> ("Illegal regular expression, unescaped ^, ^ can only appear at the end of regular expressions") and <code>/^\p{L}+$/</code> ("Illegal regular expression, integer expected at position 3"), and ACCEPTING lookahead, negative lookahead, a backreference, <code>\b</code>, a lazy quantifier and <code>/^[a-z0-9-]+$/</code> (SP-2c refuted). <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r51-deploy-anchor-index-and-length-probes.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r51-deploy-anchor-index-and-length-probes</code></a> (captured 2026-10-26) records production REJECTING <code>/a$|b/</code> and <code>/(a$)/</code> with the same text and ACCEPTING <code>/a\$b/</code>. Pyric's load-time check (<code>rtdbRegexError</code> in <code>rules/rtdb/grammar/regex.ts</code>) refuses all five with production's exact text and accepts the rest; <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:conformance/rules-rtdb-replay.test.ts</code></a> replays them with no pin, and <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-dialect.test.ts#L57" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/grammar/regex-dialect.test.ts</code></a> asserts the texts. Fixed in #1247 stream S1.</div></div>
|
|
478
|
+
</details>
|
|
479
|
+
<details class="compat-row" data-status="ok">
|
|
480
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>query</code> in a <code>.write</code> or <code>.validate</code> rule reads as it does on a plain read, so <code>query.orderByKey</code> is <code>false</code> and <code>!query.orderByKey</code> grants (SP-4)</span></span></span></summary>
|
|
481
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r49-value-edges.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r49-value-edges</code></a> (captured 2026-10-26) records production ALLOWING a write under <code>!query.orderByKey</code> and under <code>query.orderByKey == false</code>, and under the control <code>query.limitToFirst == null</code>. The simulator gives a write the plain-read <code>query</code> (<code>buildSimulatedQueryContext</code>), and <code>unit:rules/rtdb/rules-conformance.test.ts</code> replays all three verdicts. <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/simulation/handler.test.ts#L1547" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/simulation/handler.test.ts</code></a> checks <code>.write</code> and <code>.validate</code> both read it and that a write ignores a query passed with it. Row #26 records the same fields agreeing on reads. Fixed in #1247 stream S3.</div></div>
|
|
482
|
+
</details>
|
|
483
|
+
<details class="compat-row" data-status="ok">
|
|
484
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">A child named <code>__proto__</code> is an ordinary key: a write at <code>/__proto__</code> lands in the post-write tree and its <code>$wildcard</code> <code>.validate</code> runs (CS-1)</span></span></span></summary>
|
|
485
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r52-prototype-named-keys.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r52-prototype-named-keys</code></a> (captured 2026-10-26, 7 cases) records production DENYING a string set at <code>/path/__proto__</code> under a number-only <code>$k</code> <code>.validate</code>, as it denies one at <code>/path/plain</code> and <code>/ctor/constructor</code>. The simulator's post-write tree sets each written key as an own property (<code>post-write-tree.ts</code> <code>withValueAt</code>), so the key lands and its <code>.validate</code> runs; <code>unit:rules/rtdb/rules-conformance.test.ts</code> replays all 7 verdicts, and <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/simulation/prototype-named-keys.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/simulation/prototype-named-keys.test.ts</code></a> covers an update key, a listed update path, stored data and snapshot reads under <code>__proto__</code>. The object and update cases carrying an own <code>__proto__</code> key are allowed in production: the capture runner copies each case value by assignment, which drops the key, so production received an empty write. The SDK's <code>set()</code> also drops it (<code>ChildrenNode.val()</code> assigns each child), and the simulator writes such a value without the key. The SDK's <code>update()</code> sends its value as given, so the simulator keeps an own <code>__proto__</code> nested in an update value and runs its <code>.validate</code>. Fixed in #1247 stream S3.</div></div>
|
|
486
|
+
</details>
|
|
487
|
+
<details class="compat-row" data-status="ok">
|
|
488
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check refuses an <code>.indexOn</code> entry naming a child with a <code>.</code> in it, as production refuses to deploy it (CS-17)</span></span></span></summary>
|
|
489
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r51-deploy-anchor-index-and-length-probes.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r51-deploy-anchor-index-and-length-probes</code></a> (captured 2026-10-26) records production REJECTING <code>".indexOn": ["x.y"]</code> ("Invalid key: x.y. Index must be either .value or declared on a valid path") and ACCEPTING an empty entry <code>[""]</code> and a nested path <code>["x/y"]</code>. Pyric's load-time check refuses the dotted entry with production's text and accepts the other two; <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:conformance/rules-rtdb-replay.test.ts</code></a> replays all three. Fixed in #1247 stream S5 (CS-17).</div></div>
|
|
490
|
+
</details>
|
|
491
|
+
<details class="compat-row" data-status="ok">
|
|
492
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Under Node, <code>lint()</code> and <code>simulate()</code> read a <code>||</code> chain of 500, 501 and 2,000 terms, as production deploys each (LD-7)</span></span></span></summary>
|
|
493
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r51-deploy-anchor-index-and-length-probes.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r51-deploy-anchor-index-and-length-probes</code></a> (captured 2026-10-26) records production ACCEPTING <code>.read</code> rules that chain 500, 501 and 2,000 <code>auth.uid == 'u<n>'</code> terms with <code>||</code>. Before the fix, every semantics pass over the left-recursive grammar descended once per term, and a fresh Node process (22.15 and 24.16 alike) exhausted its default stack at 499 terms; whether 500 or 501 terms overflowed in a process that had already run other cases depended on JIT warm-up and case order, not the Node version. <code>lint()</code> then reported PARSE_ERROR and <code>simulate()</code> returned UNSUPPORTED. <code>||</code> and <code>&&</code> now match as flat chains folded in a loop, and <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/node-stack.test.ts#L17" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/node-stack.test.ts</code></a> runs each ruleset in its own fresh Node child against <code>dist</code> and asserts agreement with production for all three. The row scopes <code>rtdb.operator.or</code> because the r51 cases attribute themselves to it; the defect was the parser's recursion depth over a chain, not how <code>||</code> evaluates. Fixed in #1247 stream S5 (LD-7).</div></div>
|
|
494
|
+
</details>
|
|
495
|
+
<details class="compat-row" data-status="ok">
|
|
496
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>==</code>, <code>!=</code> and <code>===</code> with an object value from <code>val()</code> compare as production compares them: an object is never equal to anything, an identical object included, and <code>!=</code> is true, without failing the rule (SP-3 refuted)</span></span></span></summary>
|
|
497
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r48-object-equality.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r48-object-equality</code></a> (captured 2026-10-26, 10 cases) records production DENYING <code>data.val() == data.val()</code> on a stored object, <code>data.val() == newData.val()</code> and <code>===</code> over identical objects, ALLOWING <code>!=</code> over identical and different objects, and ALLOWING <code>!(newData.val() == 'x')</code> and <code>!(newData.val() == null)</code> for an object, which shows <code>==</code> with an object operand is false rather than a rule error. Number controls under <code>==</code> and <code>!=</code> allow. The simulator matches all 10 verdicts. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>rules-conformance.test.ts</code></a>)</div></div>
|
|
498
|
+
</details>
|
|
499
|
+
<details class="compat-row" data-status="ok">
|
|
500
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Members of <code>auth</code> read null when nobody is signed in, <code>child()</code> with a dotted or empty key and <code>hasChild('.priority')</code> read as production reads them, and <code>.length</code> counts UTF-16 code units (SP-5, SP-6 refuted)</span></span></span></summary>
|
|
501
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r49-value-edges.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r49-value-edges</code></a> (captured 2026-10-26, 13 cases) records production ALLOWING a signed-out read under <code>auth.token.admin != true</code> and under <code>auth.uid == null</code>, treating <code>child('a.b')</code> as a missing child, <code>child('')</code> as the node itself and <code>hasChild('.priority')</code> as true on a leaf with a priority, and DENYING three emoji under <code>length <= 3</code>. The simulator matches these 11 verdicts; row #36 records the two <code>query</code> cases where it does not. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>rules-conformance.test.ts</code></a>)</div></div>
|
|
502
|
+
</details>
|
|
503
|
+
<details class="compat-row" data-status="ok">
|
|
504
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check refuses a malformed regular expression with production's deploy error text: an unmatched <code>)</code> ("end-of-string expected"), an unclosed <code>(</code>, repeat count or character set at production's position, and an empty alternative left by the anchor strip, and it deploys a <code>)</code> after a leading <code>^</code> as a literal</span></span></span></summary>
|
|
505
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r53-deploy-regex-paren.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r53-deploy-regex-paren</code></a> (captured 2026-10-27, 10 dry-run deploy cases) records production REJECTING <code>/)/</code> and <code>/a)b/</code> ("Illegal regular expression, end-of-string expected"), <code>/^(/</code> ("expected ')' at position 3"), <code>/^(a$/</code> (position 2), <code>/a(b/</code> (position 7), <code>/a{2/</code> ("expected '}' at position 5"), <code>/^a|$/</code> ("unexpected end-of-string") and <code>/^[]$/</code> ("expected ']' at position 2"), and ACCEPTING <code>/^)/</code> and <code>/^(a)$/</code>. The positions count from the pattern production parses: the leading <code>^</code> and trailing <code>$</code> stripped, <code>.*</code> for a missing one. <code>rtdbRegexError</code> (<code>rules/rtdb/grammar/regex.ts</code>) agrees on all 10 with the exact texts; <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:conformance/rules-rtdb-replay.test.ts</code></a> replays the verdicts and <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-dialect.test.ts#L57" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/grammar/regex-dialect.test.ts</code></a> the texts. Found in #1247 stream S1.</div></div>
|
|
506
|
+
</details>
|
|
507
|
+
<details class="compat-row" data-status="ok">
|
|
508
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>matches()</code> applies production's anchor rewrite to a top-level alternation: with no group around the pattern, <code>/cat|dog/</code> reads as <code>.<em>cat|dog.</em></code> (a string ending in cat or starting with dog) and <code>/^a|b$/</code> as exactly <code>a</code> or <code>b</code>, where JavaScript reads both as "contains either side"</span></span></span></summary>
|
|
509
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r54-regex-alternation-runtime.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r54-regex-alternation-runtime</code></a> (captured 2026-10-27, 10 cases) records production DENYING <code>/cat|dog/</code> on <code>a cat here</code> and <code>hotdog</code> and ALLOWING it on <code>bobcat</code>, <code>dogs</code> and <code>cat</code>, DENYING <code>/^a|b$/</code> on <code>ax</code> and <code>xb</code> and ALLOWING it on <code>a</code>, and, for the control <code>/(cat|dog)/</code>, ALLOWING <code>a cat here</code> and DENYING <code>a cow here</code>. The simulator (<code>rules/rtdb/grammar/regex.ts</code>) matches all 10; <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/rules-conformance.test.ts</code></a> replays them. Found in #1247 stream S1. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-dialect.test.ts#L57" target="_blank" rel="noopener noreferrer"><code>regex-dialect.test.ts</code></a>)</div></div>
|
|
510
|
+
</details>
|
|
511
|
+
<details class="compat-row" data-status="ok">
|
|
512
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check accepts a regular expression the anchor rewrite leaves empty or nearly empty: <code>/^$/</code> (the empty string), <code>/^/</code>, <code>/$/</code> and <code>/^()$/</code></span></span></span></summary>
|
|
513
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r55-deploy-regex-empty.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r55-deploy-regex-empty</code></a> (captured 2026-10-27, 4 dry-run deploy cases) records production ACCEPTING <code>/^$/</code>, <code>/^/</code>, <code>/$/</code> and <code>/^()$/</code>. <code>rtdbRegexError</code> (<code>rules/rtdb/grammar/regex.ts</code>) accepts all four; <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:conformance/rules-rtdb-replay.test.ts</code></a> replays them and <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-dialect.test.ts#L57" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/grammar/regex-dialect.test.ts</code></a> checks that <code>/^$/</code> matches only the empty string. Found in #1247 stream S1.</div></div>
|
|
514
|
+
</details>
|
|
515
|
+
<details class="compat-row" data-status="ok">
|
|
516
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>matches()</code> reads <code>\s</code>, <code>\w</code> and <code>\d</code> as ASCII classes, folds case under <code>i</code> by adding each pattern character's own upper and lower case, and reads <code>& ~ # @ ></code>, an escaped <code>n</code> and the <code>|</code> of an empty alternative as plain characters</span></span></span></summary>
|
|
517
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r56-regex-classes-runtime.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r56-regex-classes-runtime</code></a> (captured 2026-10-27, 40 cases) records production ALLOWING <code>/^\s$/</code> only on space, tab, line feed, vertical tab, form feed and carriage return and DENYING it on U+00A0, U+2003, U+3000, U+FEFF, U+2028, U+0085 and U+001C (and ALLOWING <code>/^\S$/</code> on U+00A0); DENYING <code>\w</code> on <code>é</code> and <code>\d</code> on U+0663; under <code>i</code>, ALLOWING k/K, é/É, <code>[a-z]</code>/Q and dotless ı/I and DENYING k against the Kelvin sign and <code>[^a]</code>/A; ALLOWING <code>a&b</code>, <code>a~b</code>, <code>a#b</code>, <code>a@b</code> and <code>a>b</code> as literals and DENYING every near miss; matching the Zod email pattern on <code>a@b.co</code> only; reading <code>\n</code> as the letter n; and matching <code>/^(|a)$/</code> only on <code>|a</code>. The simulator (<code>rules/rtdb/grammar/regex-parser.ts</code>) matches all 40; <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/rules-conformance.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/rules-conformance.test.ts</code></a> replays them. Found in #1247 stream S1. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-dialect.test.ts#L57" target="_blank" rel="noopener noreferrer"><code>regex-dialect.test.ts</code></a>)</div></div>
|
|
518
|
+
</details>
|
|
519
|
+
<details class="compat-row" data-status="ok">
|
|
520
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check accepts <code>"</code> and <code><</code> in a regular expression, a <code>^</code> inside a character set that is not first, and a <code>$</code> alone in a set, as production does</span></span></span></summary>
|
|
521
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r57-deploy-regex-operators.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r57-deploy-regex-operators</code></a> (captured 2026-10-27 on pyric-oracle-rtdb-2, 4 dry-run deploy cases) records production ACCEPTING <code>/^a"b$/</code>, <code>/^a<b$/</code>, <code>/^[a^]$/</code> and <code>/^[$]$/</code>; <code>rtdbRegexError</code> accepts all four. Found in #1247 stream S1. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>rules-rtdb-replay.test.ts</code></a>)</div></div>
|
|
522
|
+
</details>
|
|
523
|
+
<details class="compat-row" data-status="ok">
|
|
524
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check refuses a repeat count past 2^31 - 1 with production's text, Java's <code>For input string: "2147483648"</code></span></span></span></summary>
|
|
525
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r58-deploy-regex-count-past-int.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r58-deploy-regex-count-past-int</code></a> (captured 2026-10-27 on pyric-oracle-rtdb-2, 1 dry-run deploy case) records production REJECTING <code>/^a{2147483648}$/</code> with <code>For input string: "2147483648"</code>, the text of Java's NumberFormatException, without the "Illegal regular expression" prefix. <code>rules/rtdb/grammar/regex-limits.ts</code> reports the same text. Found in #1247 stream S1. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>rules-rtdb-replay.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-limits.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>regex-limits.test.ts</code></a>)</div></div>
|
|
526
|
+
</details>
|
|
527
|
+
<details class="compat-row" data-status="ok">
|
|
528
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check refuses <code>a{100000}</code> with production's "could not determinize efficiently" text</span></span></span></summary>
|
|
529
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r59-deploy-regex-count-100000.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r59-deploy-regex-count-100000</code></a> (captured 2026-10-27 on pyric-oracle-rtdb-2, 1 dry-run deploy case) records production REJECTING <code>/^a{100000}$/</code> with "Illegal regular expression, could not determinize efficiently". Production's exact threshold is unobserved: pyric refuses a pattern with more than 10,000 character positions once its repeats are expanded, and every accepted corpus pattern has at most 12. Found in #1247 stream S1. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>rules-rtdb-replay.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-limits.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>regex-limits.test.ts</code></a>)</div></div>
|
|
530
|
+
</details>
|
|
531
|
+
<details class="compat-row" data-status="ok">
|
|
532
|
+
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior">Pyric's load-time check refuses <code>(a{1000}){1000}</code> with production's "could not determinize efficiently" text, without building its million states</span></span></span></summary>
|
|
533
|
+
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/conformance/observations/rtdb-rules/rules-rtdb-r60-deploy-regex-repeat-of-repeat.json" target="_blank" rel="noopener noreferrer"><code>oracle:rules-rtdb-r60-deploy-regex-repeat-of-repeat</code></a> (captured 2026-10-27 on pyric-oracle-rtdb-2, 1 dry-run deploy case) records production REJECTING <code>/^(a{1000}){1000}$/</code> with "Illegal regular expression, could not determinize efficiently". pyric counts positions while it parses and refuses before any matcher is built. Found in #1247 stream S1. (Structured evidence: <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>rules-rtdb-replay.test.ts</code></a>, <a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/grammar/regex-limits.test.ts#L18" target="_blank" rel="noopener noreferrer"><code>regex-limits.test.ts</code></a>)</div></div>
|
|
534
|
+
</details>
|
|
535
|
+
<details class="compat-row" data-status="ok">
|
|
472
536
|
<summary class="compat-line"><span class="compat-dot" data-status="ok" role="img" aria-label="Conforming" title="Conforming"></span><span class="compat-main"><code class="compat-api">Rules simulator</code><span class="compat-sub"><span class="compat-behavior"><code>rtdbRules(...).simulate(cases)</code> returns <code>coverage</code>: each <code>.read</code>, <code>.write</code>, <code>.validate</code> and <code>.indexOn</code> node is reported as allow, deny, error, mixed, unsupported or never evaluated, with per-outcome counts, per-file and per-kind totals that include unsupported nodes, the indexes a read case's query needed but no node declares, and, given the rules source text, the line of each rule key</span></span></span></summary>
|
|
473
537
|
<div class="compat-evidence"><div class="compat-probe"><a href="https://github.com/davideast/pyric/blob/main/packages/pyric/test/rules/rtdb/coverage.test.ts#L2" target="_blank" rel="noopener noreferrer"><code>unit:rules/rtdb/coverage.test.ts</code></a> pins a suite that exercises two of three rules (the third is never evaluated), a validate rule that only denied, one that allowed and denied, one that raised at evaluation, a rule the simulator cannot evaluate (status unsupported, counted in the totals so covered, never-evaluated and unsupported nodes sum to the total), a read cascade that stops at a granting ancestor (rules below it stay unevaluated), <code>.indexOn</code> use and <code>missingIndexes</code> derived from a read case's query, a suite that reaches full coverage, per-file totals and line numbers. Coverage is computed from the per-rule evaluation trace, whose agreement with the replayed production verdicts <a href="https://github.com/davideast/pyric/blob/main/packages/conformance/src/rules-rtdb-replay.test.ts#L4" target="_blank" rel="noopener noreferrer"><code>unit:conformance/rules-rtdb-replay.test.ts</code></a> checks. Rule coverage is Pyric tooling: production reports no rule evaluation record, so there is no production observation to replay.</div></div>
|
|
474
538
|
</details>
|
|
@@ -150,7 +150,8 @@ html{scrollbar-gutter:stable}
|
|
|
150
150
|
<span class="line"><span style="color:#E06C75">paths</span><span style="color:#ABB2BF">: {</span></span>
|
|
151
151
|
<span class="line"><span style="color:#98C379"> '/posts/$postId'</span><span style="color:#ABB2BF">: {</span></span>
|
|
152
152
|
<span class="line"><span style="color:#E06C75"> write</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">authenticated</span><span style="color:#ABB2BF">(),</span></span>
|
|
153
|
-
<span class="line"><span style="color:#
|
|
153
|
+
<span class="line"><span style="color:#7F848E;font-style:italic"> // The stamp check reads lastPost, not the post, so the post's own value needs a check too.</span></span>
|
|
154
|
+
<span class="line"><span style="color:#E06C75"> validate</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">all</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">timing</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">stampedInSameWrite</span><span style="color:#ABB2BF">(</span><span style="color:#D19A66">2</span><span style="color:#ABB2BF">, [</span><span style="color:#98C379">'lastPost'</span><span style="color:#ABB2BF">, { </span><span style="color:#E06C75">$</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">'auth.uid'</span><span style="color:#ABB2BF"> }]), </span><span style="color:#98C379">'newData.isString() && newData.val().length < 1000'</span><span style="color:#ABB2BF">),</span></span>
|
|
154
155
|
<span class="line"><span style="color:#ABB2BF"> },</span></span>
|
|
155
156
|
<span class="line"><span style="color:#7F848E;font-style:italic"> // A deleted stamp would reset the cooldown, so the owner may not delete it.</span></span>
|
|
156
157
|
<span class="line"><span style="color:#98C379"> '/lastPost/$uid'</span><span style="color:#ABB2BF">: { </span><span style="color:#E06C75">write</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">all</span><span style="color:#ABB2BF">(</span><span style="color:#61AFEF">ownPath</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">'$uid'</span><span style="color:#ABB2BF">), </span><span style="color:#E5C07B">lifecycle</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">noDelete</span><span style="color:#ABB2BF">()), </span><span style="color:#E06C75">validate</span><span style="color:#ABB2BF">: </span><span style="color:#E5C07B">timing</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">throttled</span><span style="color:#ABB2BF">(</span><span style="color:#D19A66">60_000</span><span style="color:#ABB2BF">) },</span></span>
|
|
@@ -158,7 +159,7 @@ html{scrollbar-gutter:stable}
|
|
|
158
159
|
<span class="line"></span>
|
|
159
160
|
<span class="line"><span style="color:#7F848E;font-style:italic">// Client: the post and the stamp in one multi-path update.</span></span>
|
|
160
161
|
<span class="line"><span style="color:#C678DD">await</span><span style="color:#61AFEF"> update</span><span style="color:#ABB2BF">(</span><span style="color:#61AFEF">ref</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">db</span><span style="color:#ABB2BF">), { </span><span style="color:#98C379">'posts/p1'</span><span style="color:#ABB2BF">: </span><span style="color:#E06C75">post</span><span style="color:#ABB2BF">, [</span><span style="color:#98C379">`lastPost/</span><span style="color:#C678DD">${</span><span style="color:#E06C75">uid</span><span style="color:#C678DD">}</span><span style="color:#98C379">`</span><span style="color:#ABB2BF">]: </span><span style="color:#61AFEF">serverTimestamp</span><span style="color:#ABB2BF">() });</span></span></code></pre>
|
|
161
|
-
<p><code>now</code> is the server clock in milliseconds, and the server replaces a written <code>serverTimestamp()</code> with <code>now</code> before it evaluates the rules. The stamp’s <code>.validate</code> requires the server timestamp and refuses it until 60 seconds after the stored one; the post’s <code>.validate</code> requires the stamp in the same write. A post without a stamp, a stamp with a client clock time, a stamp under another uid, and a delete of the stamp are all refused. The limit is on writes, not posts: one multi-path update can carry several posts and one stamp. For a cooldown on one record, combine <code>timing.cooldownElapsed(ms, 'lastMoveAt')</code> with <code>timing.isServerTimestamp('lastMoveAt')</code> in its <code>.write</code>.</p>
|
|
162
|
+
<p><code>now</code> is the server clock in milliseconds, and the server replaces a written <code>serverTimestamp()</code> with <code>now</code> before it evaluates the rules. The stamp’s <code>.validate</code> requires the server timestamp and refuses it until 60 seconds after the stored one; the post’s <code>.validate</code> requires the stamp in the same write. Neither reads the post itself, so the post’s <code>.validate</code> adds its own value check; without one, lint reports <code>RTDB-SEC-6</code>. A post without a stamp, a stamp with a client clock time, a stamp under another uid, and a delete of the stamp are all refused. The limit is on writes, not posts: one multi-path update can carry several posts and one stamp. For a cooldown on one record, combine <code>timing.cooldownElapsed(ms, 'lastMoveAt')</code> with <code>timing.isServerTimestamp('lastMoveAt')</code> in its <code>.write</code>.</p>
|
|
162
163
|
<h2 id="check-who-the-writer-is">Check who the writer is</h2>
|
|
163
164
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">const</span><span style="color:#ABB2BF"> { </span><span style="color:#E5C07B">auth</span><span style="color:#ABB2BF">, </span><span style="color:#E5C07B">membership</span><span style="color:#ABB2BF"> } </span><span style="color:#56B6C2">=</span><span style="color:#E06C75"> rtdbStdlib</span><span style="color:#ABB2BF">;</span></span>
|
|
164
165
|
<span class="line"></span>
|
|
@@ -185,7 +186,8 @@ html{scrollbar-gutter:stable}
|
|
|
185
186
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#E06C75">paths</span><span style="color:#ABB2BF">: {</span></span>
|
|
186
187
|
<span class="line"><span style="color:#98C379"> '/posts/$postId'</span><span style="color:#ABB2BF">: {</span></span>
|
|
187
188
|
<span class="line"><span style="color:#E06C75"> write</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">all</span><span style="color:#ABB2BF">(</span><span style="color:#61AFEF">authenticated</span><span style="color:#ABB2BF">(), </span><span style="color:#E5C07B">lifecycle</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">createOnly</span><span style="color:#ABB2BF">()),</span></span>
|
|
188
|
-
<span class="line"><span style="color:#
|
|
189
|
+
<span class="line"><span style="color:#7F848E;font-style:italic"> // The quota check reads quota, not the post, so the post's own value needs a check too.</span></span>
|
|
190
|
+
<span class="line"><span style="color:#E06C75"> validate</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">all</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">timing</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">countedInSameWrite</span><span style="color:#ABB2BF">(</span><span style="color:#D19A66">2</span><span style="color:#ABB2BF">, [</span><span style="color:#98C379">'quota'</span><span style="color:#ABB2BF">, { </span><span style="color:#E06C75">$</span><span style="color:#ABB2BF">: </span><span style="color:#98C379">'auth.uid'</span><span style="color:#ABB2BF"> }]), </span><span style="color:#98C379">'newData.isString() && newData.val().length < 1000'</span><span style="color:#ABB2BF">),</span></span>
|
|
189
191
|
<span class="line"><span style="color:#ABB2BF"> },</span></span>
|
|
190
192
|
<span class="line"><span style="color:#7F848E;font-style:italic"> // At most 10 posts per minute per user.</span></span>
|
|
191
193
|
<span class="line"><span style="color:#98C379"> '/quota/$uid'</span><span style="color:#ABB2BF">: {</span></span>
|
|
@@ -199,7 +201,18 @@ html{scrollbar-gutter:stable}
|
|
|
199
201
|
<span class="line"><span style="color:#C678DD">await</span><span style="color:#61AFEF"> update</span><span style="color:#ABB2BF">(</span><span style="color:#61AFEF">ref</span><span style="color:#ABB2BF">(</span><span style="color:#E06C75">db</span><span style="color:#ABB2BF">), { </span><span style="color:#98C379">'posts/p2'</span><span style="color:#ABB2BF">: </span><span style="color:#E06C75">post</span><span style="color:#ABB2BF">, [</span><span style="color:#98C379">`quota/</span><span style="color:#C678DD">${</span><span style="color:#E06C75">uid</span><span style="color:#C678DD">}</span><span style="color:#98C379">/count`</span><span style="color:#ABB2BF">]: </span><span style="color:#D19A66">2</span><span style="color:#ABB2BF"> });</span></span></code></pre>
|
|
200
202
|
<p>The quota node holds <code>{ windowStart, count }</code>. A write either opens a new window, with <code>windowStart</code> the server timestamp and <code>count</code> 1, once nothing is stored or the stored window has ended, or adds 1 to <code>count</code> in the open window, up to the maximum. A post must move the quota in the same write. Like the cooldown stamp, the quota limits writes, not posts: one multi-path update can carry several posts and one count.</p>
|
|
201
203
|
<h2 id="bound-a-collection">Bound a collection</h2>
|
|
202
|
-
<
|
|
204
|
+
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">const</span><span style="color:#ABB2BF"> { </span><span style="color:#E5C07B">collections</span><span style="color:#ABB2BF"> } </span><span style="color:#56B6C2">=</span><span style="color:#E06C75"> rtdbStdlib</span><span style="color:#ABB2BF">;</span></span>
|
|
205
|
+
<span class="line"></span>
|
|
206
|
+
<span class="line"><span style="color:#E06C75">paths</span><span style="color:#ABB2BF">: {</span></span>
|
|
207
|
+
<span class="line"><span style="color:#98C379"> '/tables/$tableId'</span><span style="color:#ABB2BF">: {</span></span>
|
|
208
|
+
<span class="line"><span style="color:#E06C75"> write</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">authenticated</span><span style="color:#ABB2BF">(),</span></span>
|
|
209
|
+
<span class="line"><span style="color:#E06C75"> children</span><span style="color:#ABB2BF">: {</span></span>
|
|
210
|
+
<span class="line"><span style="color:#98C379"> '/seats/$slot'</span><span style="color:#ABB2BF">: { </span><span style="color:#E06C75">validate</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">all</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">collections</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">slotKey</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">'$slot'</span><span style="color:#ABB2BF">, </span><span style="color:#D19A66">4</span><span style="color:#ABB2BF">), </span><span style="color:#98C379">'newData.isString()'</span><span style="color:#ABB2BF">) },</span></span>
|
|
211
|
+
<span class="line"><span style="color:#98C379"> '/flags/$flag'</span><span style="color:#ABB2BF">: { </span><span style="color:#E06C75">validate</span><span style="color:#ABB2BF">: </span><span style="color:#61AFEF">all</span><span style="color:#ABB2BF">(</span><span style="color:#E5C07B">collections</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">keyIn</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">'$flag'</span><span style="color:#ABB2BF">, [</span><span style="color:#98C379">'red'</span><span style="color:#ABB2BF">, </span><span style="color:#98C379">'blue'</span><span style="color:#ABB2BF">]), </span><span style="color:#98C379">'newData.isBoolean()'</span><span style="color:#ABB2BF">) },</span></span>
|
|
212
|
+
<span class="line"><span style="color:#ABB2BF"> },</span></span>
|
|
213
|
+
<span class="line"><span style="color:#ABB2BF"> },</span></span>
|
|
214
|
+
<span class="line"><span style="color:#ABB2BF">}</span></span></code></pre>
|
|
215
|
+
<p>RTDB rules cannot count a node’s children. Bound the keys instead: <code>collections.slotKey('$slot', 4)</code> on <code>'/seats/$slot'</code> allows the keys <code>'0'</code> to <code>'3'</code>, which is how a client stores an array, so a fifth seat is refused. <code>collections.keyIn('$flag', ['red', 'blue'])</code> allows a fixed list. Both check the key, not the value, so each recipe adds a value check; without one, lint reports <code>RTDB-SEC-6</code>. The library has no builder for a collection with free keys, such as push IDs.</p>
|
|
203
216
|
<h2 id="ask-an-agent">Ask an agent</h2>
|
|
204
217
|
<p>An agent reads the same catalog through the rules tools:</p>
|
|
205
218
|
<ul>
|
|
@@ -113,7 +113,8 @@ const { timing, lifecycle } = rtdbStdlib;
|
|
|
113
113
|
paths: {
|
|
114
114
|
'/posts/$postId': {
|
|
115
115
|
write: authenticated(),
|
|
116
|
-
|
|
116
|
+
// The stamp check reads lastPost, not the post, so the post's own value needs a check too.
|
|
117
|
+
validate: all(timing.stampedInSameWrite(2, ['lastPost', { $: 'auth.uid' }]), 'newData.isString() && newData.val().length < 1000'),
|
|
117
118
|
},
|
|
118
119
|
// A deleted stamp would reset the cooldown, so the owner may not delete it.
|
|
119
120
|
'/lastPost/$uid': { write: all(ownPath('$uid'), lifecycle.noDelete()), validate: timing.throttled(60_000) },
|
|
@@ -122,7 +123,7 @@ paths: {
|
|
|
122
123
|
// Client: the post and the stamp in one multi-path update.
|
|
123
124
|
await update(ref(db), { 'posts/p1': post, [`lastPost/${uid}`]: serverTimestamp() });
|
|
124
125
|
```
|
|
125
|
-
`now` is the server clock in milliseconds, and the server replaces a written `serverTimestamp()` with `now` before it evaluates the rules. The stamp's `.validate` requires the server timestamp and refuses it until 60 seconds after the stored one; the post's `.validate` requires the stamp in the same write. A post without a stamp, a stamp with a client clock time, a stamp under another uid, and a delete of the stamp are all refused. The limit is on writes, not posts: one multi-path update can carry several posts and one stamp. For a cooldown on one record, combine `timing.cooldownElapsed(ms, 'lastMoveAt')` with `timing.isServerTimestamp('lastMoveAt')` in its `.write`.
|
|
126
|
+
`now` is the server clock in milliseconds, and the server replaces a written `serverTimestamp()` with `now` before it evaluates the rules. The stamp's `.validate` requires the server timestamp and refuses it until 60 seconds after the stored one; the post's `.validate` requires the stamp in the same write. Neither reads the post itself, so the post's `.validate` adds its own value check; without one, lint reports `RTDB-SEC-6`. A post without a stamp, a stamp with a client clock time, a stamp under another uid, and a delete of the stamp are all refused. The limit is on writes, not posts: one multi-path update can carry several posts and one stamp. For a cooldown on one record, combine `timing.cooldownElapsed(ms, 'lastMoveAt')` with `timing.isServerTimestamp('lastMoveAt')` in its `.write`.
|
|
126
127
|
|
|
127
128
|
## Check who the writer is
|
|
128
129
|
|
|
@@ -159,7 +160,8 @@ A member is a key whose value is `true`. `memberOf` reads the stored data, so a
|
|
|
159
160
|
paths: {
|
|
160
161
|
'/posts/$postId': {
|
|
161
162
|
write: all(authenticated(), lifecycle.createOnly()),
|
|
162
|
-
|
|
163
|
+
// The quota check reads quota, not the post, so the post's own value needs a check too.
|
|
164
|
+
validate: all(timing.countedInSameWrite(2, ['quota', { $: 'auth.uid' }]), 'newData.isString() && newData.val().length < 1000'),
|
|
163
165
|
},
|
|
164
166
|
// At most 10 posts per minute per user.
|
|
165
167
|
'/quota/$uid': {
|
|
@@ -176,7 +178,20 @@ The quota node holds `{ windowStart, count }`. A write either opens a new window
|
|
|
176
178
|
|
|
177
179
|
## Bound a collection
|
|
178
180
|
|
|
179
|
-
|
|
181
|
+
```ts
|
|
182
|
+
const { collections } = rtdbStdlib;
|
|
183
|
+
|
|
184
|
+
paths: {
|
|
185
|
+
'/tables/$tableId': {
|
|
186
|
+
write: authenticated(),
|
|
187
|
+
children: {
|
|
188
|
+
'/seats/$slot': { validate: all(collections.slotKey('$slot', 4), 'newData.isString()') },
|
|
189
|
+
'/flags/$flag': { validate: all(collections.keyIn('$flag', ['red', 'blue']), 'newData.isBoolean()') },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
RTDB rules cannot count a node's children. Bound the keys instead: `collections.slotKey('$slot', 4)` on `'/seats/$slot'` allows the keys `'0'` to `'3'`, which is how a client stores an array, so a fifth seat is refused. `collections.keyIn('$flag', ['red', 'blue'])` allows a fixed list. Both check the key, not the value, so each recipe adds a value check; without one, lint reports `RTDB-SEC-6`. The library has no builder for a collection with free keys, such as push IDs.
|
|
180
195
|
|
|
181
196
|
## Ask an agent
|
|
182
197
|
|
|
@@ -132,7 +132,7 @@ The linter checks two different kinds of failure.</p>
|
|
|
132
132
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="plaintext"><code><span class="line"><span>matchedPath: /records/$recordId/title</span></span>
|
|
133
133
|
<span class="line"><span>matchedRule: newData.isString() && newData.val().length <= 80</span></span>
|
|
134
134
|
<span class="line"><span>reason: Validation rule evaluated to false</span></span></code></pre>
|
|
135
|
-
<p><code>matchedPath</code> is the rule’s location in the ruleset, so it shows the <code>$recordId</code> wildcard rather than the concrete path. <code>UNSUPPORTED</code> means the case reached an expression the simulator cannot evaluate
|
|
135
|
+
<p><code>matchedPath</code> is the rule’s location in the ruleset, so it shows the <code>$recordId</code> wildcard rather than the concrete path. <code>UNSUPPORTED</code> means the simulator did not evaluate the case, and <code>reason</code> says why. Either the case reached an expression the simulator cannot evaluate, and it abstains rather than guesses, or no client could send the request. The second covers a path with <code>.</code>, <code>#</code>, <code>$</code>, <code>[</code> or <code>]</code> in it, update paths that overlap, a write under <code>/.info</code>, a query the SDK refuses to build, <code>updates</code> that leave out the case’s <code>path</code> or give it a value other than <code>newData</code>, and a ruleset that does not compile. For a request the SDK refuses, <code>reason</code> is the SDK’s own error text.</p>
|
|
136
136
|
<p>The result also carries a <code>trace</code> with every rule the simulator ran, in order:</p>
|
|
137
137
|
<pre class="astro-code one-dark-pro" style="background-color:#282c34;color:#abb2bf; overflow-x: auto;" tabindex="0" data-language="ts"><code><span class="line"><span style="color:#C678DD">for</span><span style="color:#ABB2BF"> (</span><span style="color:#C678DD">const</span><span style="color:#E5C07B"> step</span><span style="color:#C678DD"> of</span><span style="color:#E06C75"> cases</span><span style="color:#ABB2BF">[</span><span style="color:#D19A66">0</span><span style="color:#ABB2BF">].</span><span style="color:#E06C75">trace</span><span style="color:#ABB2BF">) {</span></span>
|
|
138
138
|
<span class="line"><span style="color:#E5C07B"> console</span><span style="color:#ABB2BF">.</span><span style="color:#61AFEF">log</span><span style="color:#ABB2BF">(</span><span style="color:#98C379">`</span><span style="color:#C678DD">${</span><span style="color:#E5C07B">step</span><span style="color:#ABB2BF">.</span><span style="color:#E06C75">path</span><span style="color:#C678DD">}</span><span style="color:#98C379"> .</span><span style="color:#C678DD">${</span><span style="color:#E5C07B">step</span><span style="color:#ABB2BF">.</span><span style="color:#E06C75">kind</span><span style="color:#C678DD">}</span><span style="color:#98C379"> -> </span><span style="color:#C678DD">${</span><span style="color:#E5C07B">step</span><span style="color:#ABB2BF">.</span><span style="color:#E06C75">verdict</span><span style="color:#C678DD">}</span><span style="color:#98C379">: </span><span style="color:#C678DD">${</span><span style="color:#E5C07B">step</span><span style="color:#ABB2BF">.</span><span style="color:#E06C75">conditionText</span><span style="color:#C678DD">}</span><span style="color:#98C379">`</span><span style="color:#ABB2BF">);</span></span>
|
|
@@ -253,8 +253,8 @@ The linter checks two different kinds of failure.</p>
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
|
|
256
|
-
<table><thead><tr><th>Code</th><th>What it reports</th></tr></thead><tbody><tr><td><code>PARSE_ERROR</code></td><td>The expression does not parse.</td></tr><tr><td><code>NOT_BOOLEAN</code></td><td>The rule does not evaluate to a boolean, such as a <code>.read</code> of <code>auth.uid</code>.</td></tr><tr><td><code>INVALID_OPERAND</code></td><td>An operator gets an operand type it does not take, such as <code>data.val() > true</code>, or <code>!</code> on a string.</td></tr><tr><td><code>INVALID_ARGUMENT</code></td><td>A method gets the wrong number or type of arguments, such as <code>child(1)</code>, or <code>matches()</code> with anything other than a regular expression literal.</td></tr><tr><td><code>INVALID_REGEX</code></td><td>A regular expression literal
|
|
257
|
-
<p><code>INDEX_ON_SHAPE</code> and <code>INVALID_KEY</code> are warnings, not errors, for related forms: any other <code>.indexOn</code> that is not a string or an array of strings,
|
|
256
|
+
<table><thead><tr><th>Code</th><th>What it reports</th></tr></thead><tbody><tr><td><code>PARSE_ERROR</code></td><td>The expression does not parse.</td></tr><tr><td><code>NOT_BOOLEAN</code></td><td>The rule does not evaluate to a boolean, such as a <code>.read</code> of <code>auth.uid</code>.</td></tr><tr><td><code>INVALID_OPERAND</code></td><td>An operator gets an operand type it does not take, such as <code>data.val() > true</code>, or <code>!</code> on a string.</td></tr><tr><td><code>INVALID_ARGUMENT</code></td><td>A method gets the wrong number or type of arguments, such as <code>child(1)</code>, or <code>matches()</code> with anything other than a regular expression literal.</td></tr><tr><td><code>INVALID_REGEX</code></td><td>A regular expression literal production refuses: a flag other than <code>i</code>, a <code>^</code> that is not the first character or a <code>$</code> that is not the last, or an unclosed group, repeat count or character set. A repeat count above 2,147,483,647 and a pattern too large to determinize, such as <code>a{100000}</code>, fail with production’s texts. A pattern past pyric’s own limits (more than 200,000 matcher states, or nesting deeper than 1,000 levels) reports here too, with a message that names the limit.</td></tr><tr><td><code>NO_SUCH_MEMBER</code></td><td>The value has no such method or property, such as <code>data.nope()</code>.</td></tr><tr><td><code>NOT_AN_OBJECT</code></td><td>A property access on a number, boolean, null, or regular expression.</td></tr><tr><td><code>NOT_A_FUNCTION</code></td><td>A call on something that is not a method, such as <code>auth.uid()</code>.</td></tr><tr><td><code>INVALID_PROPERTY_ACCESS</code></td><td>An index with a computed key, <code>x[expr]</code>, on anything other than <code>auth</code>.</td></tr><tr><td><code>UNEXPECTED_ARRAY</code></td><td>An array literal anywhere other than the argument of <code>hasChildren()</code>.</td></tr><tr><td><code>UNKNOWN_IDENTIFIER</code></td><td>The variable is not available, such as a <code>$name</code> that no enclosing key declares.</td></tr><tr><td><code>NEWDATA_IN_READ</code></td><td>A <code>.read</code> rule uses <code>newData</code>, which exists only for writes.</td></tr><tr><td><code>MULTIPLE_WILDCARDS</code></td><td>One location has two <code>$</code> children.</td></tr><tr><td><code>RULE_NOT_EXPRESSION</code></td><td>A <code>.read</code>, <code>.write</code>, or <code>.validate</code> value is a number or a plain object rather than a boolean or a string.</td></tr><tr><td><code>INDEX_ON_SHAPE</code></td><td><code>.indexOn</code> is a number.</td></tr><tr><td><code>INVALID_KEY</code></td><td>A key contains <code>#</code>, an unknown key that starts with <code>.</code> holds an object, or an <code>.indexOn</code> entry other than <code>.value</code> contains <code>.</code>.</td></tr><tr><td><code>EXPECTED_OBJECT</code></td><td>An unknown key that starts with <code>.</code>, or a child key, holds a string.</td></tr></tbody></table></div>
|
|
257
|
+
<p><code>INDEX_ON_SHAPE</code> and <code>INVALID_KEY</code> are warnings, not errors, for related forms: any other <code>.indexOn</code> that is not a string or an array of strings, a key that contains <code>.</code>, <code>/</code>, <code>[</code>, <code>]</code>, or an inner <code>$</code> but no <code>#</code>, and an <code>.indexOn</code> entry that contains <code>#</code>, <code>$</code>, <code>[</code> or <code>]</code>. Their text comes from the deploy’s message for the related form, and no deploy of these forms has been recorded.</p>
|
|
258
258
|
<p>The second group is the rest of the per-rule checks:</p>
|
|
259
259
|
<div class="table-scroll">
|
|
260
260
|
|
|
@@ -330,7 +330,7 @@ The linter checks two different kinds of failure.</p>
|
|
|
330
330
|
|
|
331
331
|
|
|
332
332
|
|
|
333
|
-
<table><thead><tr><th>Code</th><th>What it reports</th></tr></thead><tbody><tr><td><code>RTDB-SEC-1</code></td><td>A <code>.write</code> of <code>true</code>,
|
|
333
|
+
<table><thead><tr><th>Code</th><th>What it reports</th></tr></thead><tbody><tr><td><code>RTDB-SEC-1</code></td><td>A <code>.write</code> of <code>true</code>, or one true for every request signed in or not (such as <code>auth != null || true</code>), so anyone can write, replace, or delete values under that path.</td></tr><tr><td><code>RTDB-SEC-2</code></td><td>A <code>.read</code> of <code>true</code>, or one true for every request, so anyone can read everything under that path. At the root, this exposes the entire database.</td></tr><tr><td><code>RTDB-SEC-3</code></td><td>A conditional <code>.write</code> that never reads <code>auth</code>, so a signed-out client can write whenever the condition holds.</td></tr><tr><td><code>RTDB-SEC-4</code></td><td>A rule that tries to restrict access that an ancestor already grants, such as <code>auth.uid == $uid</code> under an ancestor’s <code>auth != null</code>. A deeper rule cannot revoke an ancestor’s grant, so the deeper rule restricts nothing.</td></tr><tr><td><code>RTDB-SEC-5</code></td><td>A <code>.validate</code> that a delete skips. <code>.validate</code> runs only on a non-null new value, so a client that can write <code>null</code> at an ancestor bypasses it.</td></tr><tr><td><code>RTDB-SEC-6</code></td><td>A <code>.write</code> that accepts any value: no <code>.validate</code> that reads its own new value sits at or below it, and the write rule does not check <code>newData</code>.</td></tr><tr><td><code>RTDB-SEC-7</code></td><td>A node that names children in <code>.validate</code> but has no <code>$other</code> rule, so a write can add any other key under it.</td></tr></tbody></table></div>
|
|
334
334
|
<p>For a ruleset that names children in <code>.validate</code>, <code>"$other": { ".validate": false }</code> rejects the keys it does not name.</p>
|
|
335
335
|
<p>Lint a ruleset before you simulate it. <code>simulate</code> does not run the deploy check. A case that reaches an expression that does not parse comes back <code>UNSUPPORTED</code>, and a rule the deploy refuses can still evaluate: a <code>.read</code> of <code>auth.uid</code> grants any signed-in reader in the simulator, and production never loads that ruleset at all.</p>
|
|
336
336
|
<p><code>pyric database rules validate database.rules.json</code> prints the errors in a file and exits with status 2 when it finds any. A finding about one rule includes the <code>line</code> and <code>column</code> of its rule key in the file, and comments in the file do not shift them.</p>
|
|
@@ -126,7 +126,7 @@ matchedPath: /records/$recordId/title
|
|
|
126
126
|
matchedRule: newData.isString() && newData.val().length <= 80
|
|
127
127
|
reason: Validation rule evaluated to false
|
|
128
128
|
```
|
|
129
|
-
`matchedPath` is the rule's location in the ruleset, so it shows the `$recordId` wildcard rather than the concrete path. `UNSUPPORTED` means the case reached an expression the simulator cannot evaluate
|
|
129
|
+
`matchedPath` is the rule's location in the ruleset, so it shows the `$recordId` wildcard rather than the concrete path. `UNSUPPORTED` means the simulator did not evaluate the case, and `reason` says why. Either the case reached an expression the simulator cannot evaluate, and it abstains rather than guesses, or no client could send the request. The second covers a path with `.`, `#`, `$`, `[` or `]` in it, update paths that overlap, a write under `/.info`, a query the SDK refuses to build, `updates` that leave out the case's `path` or give it a value other than `newData`, and a ruleset that does not compile. For a request the SDK refuses, `reason` is the SDK's own error text.
|
|
130
130
|
|
|
131
131
|
The result also carries a `trace` with every rule the simulator ran, in order:
|
|
132
132
|
```ts
|
|
@@ -197,7 +197,7 @@ The errors in the first group are the ones `firebase deploy` refuses, and each m
|
|
|
197
197
|
| `NOT_BOOLEAN` | The rule does not evaluate to a boolean, such as a `.read` of `auth.uid`. |
|
|
198
198
|
| `INVALID_OPERAND` | An operator gets an operand type it does not take, such as `data.val() > true`, or `!` on a string. |
|
|
199
199
|
| `INVALID_ARGUMENT` | A method gets the wrong number or type of arguments, such as `child(1)`, or `matches()` with anything other than a regular expression literal. |
|
|
200
|
-
| `INVALID_REGEX` | A regular expression literal
|
|
200
|
+
| `INVALID_REGEX` | A regular expression literal production refuses: a flag other than `i`, a `^` that is not the first character or a `$` that is not the last, or an unclosed group, repeat count or character set. A repeat count above 2,147,483,647 and a pattern too large to determinize, such as `a{100000}`, fail with production's texts. A pattern past pyric's own limits (more than 200,000 matcher states, or nesting deeper than 1,000 levels) reports here too, with a message that names the limit. |
|
|
201
201
|
| `NO_SUCH_MEMBER` | The value has no such method or property, such as `data.nope()`. |
|
|
202
202
|
| `NOT_AN_OBJECT` | A property access on a number, boolean, null, or regular expression. |
|
|
203
203
|
| `NOT_A_FUNCTION` | A call on something that is not a method, such as `auth.uid()`. |
|
|
@@ -208,10 +208,10 @@ The errors in the first group are the ones `firebase deploy` refuses, and each m
|
|
|
208
208
|
| `MULTIPLE_WILDCARDS` | One location has two `$` children. |
|
|
209
209
|
| `RULE_NOT_EXPRESSION` | A `.read`, `.write`, or `.validate` value is a number or a plain object rather than a boolean or a string. |
|
|
210
210
|
| `INDEX_ON_SHAPE` | `.indexOn` is a number. |
|
|
211
|
-
| `INVALID_KEY` | A key contains `#`,
|
|
211
|
+
| `INVALID_KEY` | A key contains `#`, an unknown key that starts with `.` holds an object, or an `.indexOn` entry other than `.value` contains `.`. |
|
|
212
212
|
| `EXPECTED_OBJECT` | An unknown key that starts with `.`, or a child key, holds a string. |
|
|
213
213
|
|
|
214
|
-
`INDEX_ON_SHAPE` and `INVALID_KEY` are warnings, not errors, for related forms: any other `.indexOn` that is not a string or an array of strings,
|
|
214
|
+
`INDEX_ON_SHAPE` and `INVALID_KEY` are warnings, not errors, for related forms: any other `.indexOn` that is not a string or an array of strings, a key that contains `.`, `/`, `[`, `]`, or an inner `$` but no `#`, and an `.indexOn` entry that contains `#`, `$`, `[` or `]`. Their text comes from the deploy's message for the related form, and no deploy of these forms has been recorded.
|
|
215
215
|
|
|
216
216
|
The second group is the rest of the per-rule checks:
|
|
217
217
|
|
|
@@ -227,12 +227,12 @@ The third group reads the whole ruleset, because the common RTDB mistakes come f
|
|
|
227
227
|
|
|
228
228
|
| Code | What it reports |
|
|
229
229
|
|---|---|
|
|
230
|
-
| `RTDB-SEC-1` | A `.write` of `true`,
|
|
231
|
-
| `RTDB-SEC-2` | A `.read` of `true`, so anyone can read everything under that path. At the root, this exposes the entire database. |
|
|
230
|
+
| `RTDB-SEC-1` | A `.write` of `true`, or one true for every request signed in or not (such as `auth != null \|\| true`), so anyone can write, replace, or delete values under that path. |
|
|
231
|
+
| `RTDB-SEC-2` | A `.read` of `true`, or one true for every request, so anyone can read everything under that path. At the root, this exposes the entire database. |
|
|
232
232
|
| `RTDB-SEC-3` | A conditional `.write` that never reads `auth`, so a signed-out client can write whenever the condition holds. |
|
|
233
|
-
| `RTDB-SEC-4` | A rule that tries to restrict access that an ancestor already grants. A deeper rule cannot revoke an ancestor's grant, so the deeper rule restricts nothing. |
|
|
233
|
+
| `RTDB-SEC-4` | A rule that tries to restrict access that an ancestor already grants, such as `auth.uid == $uid` under an ancestor's `auth != null`. A deeper rule cannot revoke an ancestor's grant, so the deeper rule restricts nothing. |
|
|
234
234
|
| `RTDB-SEC-5` | A `.validate` that a delete skips. `.validate` runs only on a non-null new value, so a client that can write `null` at an ancestor bypasses it. |
|
|
235
|
-
| `RTDB-SEC-6` | A `.write` that accepts any value: no `.validate` sits at or below it and the write rule does not check `newData`. |
|
|
235
|
+
| `RTDB-SEC-6` | A `.write` that accepts any value: no `.validate` that reads its own new value sits at or below it, and the write rule does not check `newData`. |
|
|
236
236
|
| `RTDB-SEC-7` | A node that names children in `.validate` but has no `$other` rule, so a write can add any other key under it. |
|
|
237
237
|
|
|
238
238
|
For a ruleset that names children in `.validate`, `"$other": { ".validate": false }` rejects the keys it does not name.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"><title>Play chess against Security Rules</title><link rel="icon" type="image/svg+xml" href="/__pyric/ui/pyric-logo.svg"><link rel="preconnect" href="https://use.typekit.net" crossorigin><link rel="stylesheet" href="https://use.typekit.net/jwb3ycu.css"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"><style>html{scrollbar-gutter:stable}
|
|
2
2
|
</style>
|
|
3
3
|
<link rel="stylesheet" href="/__pyric/ui/_astro/_example_.B9DG9HLJ.css">
|
|
4
|
-
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="
|
|
4
|
+
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="Z2mTpvA" component-url="/__pyric/ui/_astro/chess-showcase.CVhCNUi_.js" component-export="ChessShowcase" renderer-url="/__pyric/ui/_astro/client.D2ORs4B-.js" props="{}" ssr client="only" opts="{"name":"ChessShowcase","value":"react"}"></astro-island> </body></html>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
</script><link rel="preconnect" href="https://use.typekit.net" crossorigin><link rel="stylesheet" href="https://use.typekit.net/jwb3ycu.css"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"><link rel="stylesheet" href="/__pyric/ui/_astro/_studio_.CzyEuvVS.css">
|
|
7
7
|
<style>html{scrollbar-gutter:stable}
|
|
8
8
|
</style>
|
|
9
|
-
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <div id="root" data-astro-cid-w5sm6wlz> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="b1btC" component-url="/__pyric/ui/_astro/app.
|
|
9
|
+
<link rel="stylesheet" href="/__pyric/ui/_astro/app.8TtSeQQX.css"></head> <body> <div id="root" data-astro-cid-w5sm6wlz> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style><script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();</script><script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script><astro-island uid="b1btC" component-url="/__pyric/ui/_astro/app.DqyhRyrU.js" component-export="StudioApp" renderer-url="/__pyric/ui/_astro/client.D2ORs4B-.js" props="{"data-astro-cid-w5sm6wlz":[0,true]}" ssr client="only" opts="{"name":"StudioApp","value":"react"}" await-children><p class="studio-loading" aria-live="polite" data-astro-cid-w5sm6wlz>
|
|
10
10
|
Starting Pyric Studio…
|
|
11
11
|
</p><template data-astro-template="fallback"><p class="studio-loading" aria-live="polite" data-astro-cid-w5sm6wlz>
|
|
12
12
|
Starting Pyric Studio…
|