pyric 0.1.0-alpha.19 → 0.1.0-alpha.21

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.
Files changed (281) hide show
  1. package/README.md +64 -89
  2. package/dist/ai/blocked.d.ts +52 -0
  3. package/dist/ai/blocked.d.ts.map +1 -0
  4. package/dist/ai/blocked.js +83 -0
  5. package/dist/ai/blocked.js.map +1 -0
  6. package/dist/ai/broker/broker.d.ts +50 -0
  7. package/dist/ai/broker/broker.d.ts.map +1 -1
  8. package/dist/ai/broker/broker.js +157 -21
  9. package/dist/ai/broker/broker.js.map +1 -1
  10. package/dist/ai/broker/gemini-engine.d.ts +56 -0
  11. package/dist/ai/broker/gemini-engine.d.ts.map +1 -0
  12. package/dist/ai/broker/gemini-engine.js +235 -0
  13. package/dist/ai/broker/gemini-engine.js.map +1 -0
  14. package/dist/ai/broker/index.d.ts +2 -1
  15. package/dist/ai/broker/index.d.ts.map +1 -1
  16. package/dist/ai/broker/index.js +2 -1
  17. package/dist/ai/broker/index.js.map +1 -1
  18. package/dist/ai/broker/openai-engine.d.ts +10 -1
  19. package/dist/ai/broker/openai-engine.d.ts.map +1 -1
  20. package/dist/ai/broker/openai-engine.js +26 -4
  21. package/dist/ai/broker/openai-engine.js.map +1 -1
  22. package/dist/ai/broker/synthesizer.d.ts +14 -0
  23. package/dist/ai/broker/synthesizer.d.ts.map +1 -1
  24. package/dist/ai/broker/synthesizer.js +31 -0
  25. package/dist/ai/broker/synthesizer.js.map +1 -1
  26. package/dist/ai/broker/types.d.ts +29 -0
  27. package/dist/ai/broker/types.d.ts.map +1 -1
  28. package/dist/ai/instances.js +1 -1
  29. package/dist/ai/instances.js.map +1 -1
  30. package/dist/ai/internal.d.ts +5 -0
  31. package/dist/ai/internal.d.ts.map +1 -1
  32. package/dist/ai/internal.js +5 -0
  33. package/dist/ai/internal.js.map +1 -1
  34. package/dist/ai/response-helpers.d.ts.map +1 -1
  35. package/dist/ai/response-helpers.js +6 -24
  36. package/dist/ai/response-helpers.js.map +1 -1
  37. package/dist/analytics/index.d.ts +40 -0
  38. package/dist/analytics/index.d.ts.map +1 -0
  39. package/dist/analytics/index.js +25 -0
  40. package/dist/analytics/index.js.map +1 -0
  41. package/dist/app-check/index.d.ts +31 -0
  42. package/dist/app-check/index.d.ts.map +1 -0
  43. package/dist/app-check/index.js +20 -0
  44. package/dist/app-check/index.js.map +1 -0
  45. package/dist/auth/instances.js +1 -1
  46. package/dist/auth/instances.js.map +1 -1
  47. package/dist/database/controls.d.ts +1 -1
  48. package/dist/database/controls.js +1 -1
  49. package/dist/database/instances.js +1 -1
  50. package/dist/database/instances.js.map +1 -1
  51. package/dist/database/sandbox/backend.d.ts +1 -0
  52. package/dist/database/sandbox/backend.d.ts.map +1 -1
  53. package/dist/database/sandbox/backend.js +1 -0
  54. package/dist/database/sandbox/backend.js.map +1 -1
  55. package/dist/database/sandbox/rules-eval.d.ts +6 -2
  56. package/dist/database/sandbox/rules-eval.d.ts.map +1 -1
  57. package/dist/database/sandbox/rules-eval.js +15 -2
  58. package/dist/database/sandbox/rules-eval.js.map +1 -1
  59. package/dist/database/sandbox/write-plane.d.ts +1 -0
  60. package/dist/database/sandbox/write-plane.d.ts.map +1 -1
  61. package/dist/database/sandbox/write-plane.js +7 -2
  62. package/dist/database/sandbox/write-plane.js.map +1 -1
  63. package/dist/database/sandbox-controls.d.ts +12 -5
  64. package/dist/database/sandbox-controls.d.ts.map +1 -1
  65. package/dist/database/sandbox-controls.js +20 -9
  66. package/dist/database/sandbox-controls.js.map +1 -1
  67. package/dist/database/sandbox-namespace.d.ts +19 -1
  68. package/dist/database/sandbox-namespace.d.ts.map +1 -1
  69. package/dist/database/sandbox-namespace.js +24 -1
  70. package/dist/database/sandbox-namespace.js.map +1 -1
  71. package/dist/deferred/entry.d.ts +75 -0
  72. package/dist/deferred/entry.d.ts.map +1 -0
  73. package/dist/deferred/entry.js +143 -0
  74. package/dist/deferred/entry.js.map +1 -0
  75. package/dist/firestore/instances.js +1 -1
  76. package/dist/firestore/instances.js.map +1 -1
  77. package/dist/firestore/lite.d.ts +72 -0
  78. package/dist/firestore/lite.d.ts.map +1 -0
  79. package/dist/firestore/lite.js +22 -0
  80. package/dist/firestore/lite.js.map +1 -0
  81. package/dist/firestore/persistence.d.ts +2 -2
  82. package/dist/firestore/persistence.js +1 -1
  83. package/dist/firestore/sandbox-controls.d.ts +2 -0
  84. package/dist/firestore/sandbox-controls.d.ts.map +1 -1
  85. package/dist/firestore/sandbox-controls.js +8 -0
  86. package/dist/firestore/sandbox-controls.js.map +1 -1
  87. package/dist/functions/index.d.ts +29 -0
  88. package/dist/functions/index.d.ts.map +1 -0
  89. package/dist/functions/index.js +20 -0
  90. package/dist/functions/index.js.map +1 -0
  91. package/dist/messaging/client.d.ts +2 -1
  92. package/dist/messaging/client.d.ts.map +1 -1
  93. package/dist/messaging/client.js +2 -2
  94. package/dist/messaging/client.js.map +1 -1
  95. package/dist/messaging/instance.d.ts +2 -2
  96. package/dist/messaging/instance.d.ts.map +1 -1
  97. package/dist/messaging/instance.js +40 -6
  98. package/dist/messaging/instance.js.map +1 -1
  99. package/dist/messaging/sw.d.ts +2 -1
  100. package/dist/messaging/sw.d.ts.map +1 -1
  101. package/dist/messaging/sw.js +2 -2
  102. package/dist/messaging/sw.js.map +1 -1
  103. package/dist/performance/index.d.ts +22 -0
  104. package/dist/performance/index.d.ts.map +1 -0
  105. package/dist/performance/index.js +19 -0
  106. package/dist/performance/index.js.map +1 -0
  107. package/dist/remote-config/index.d.ts +40 -0
  108. package/dist/remote-config/index.d.ts.map +1 -0
  109. package/dist/remote-config/index.js +25 -0
  110. package/dist/remote-config/index.js.map +1 -0
  111. package/dist/rules/grammar/FirestoreValidator.d.ts.map +1 -1
  112. package/dist/rules/grammar/FirestoreValidator.js +43 -27
  113. package/dist/rules/grammar/FirestoreValidator.js.map +1 -1
  114. package/dist/rules/grammar/document-access-count.d.ts +24 -0
  115. package/dist/rules/grammar/document-access-count.d.ts.map +1 -0
  116. package/dist/rules/grammar/document-access-count.js +100 -0
  117. package/dist/rules/grammar/document-access-count.js.map +1 -0
  118. package/dist/rules/linter/ast-utils.d.ts +2 -6
  119. package/dist/rules/linter/ast-utils.d.ts.map +1 -1
  120. package/dist/rules/linter/ast-utils.js +2 -63
  121. package/dist/rules/linter/ast-utils.js.map +1 -1
  122. package/dist/rules/linter/hallucinations.d.ts +8 -0
  123. package/dist/rules/linter/hallucinations.d.ts.map +1 -1
  124. package/dist/rules/linter/hallucinations.js +121 -31
  125. package/dist/rules/linter/hallucinations.js.map +1 -1
  126. package/dist/rules/linter/linter.d.ts +10 -1
  127. package/dist/rules/linter/linter.d.ts.map +1 -1
  128. package/dist/rules/linter/linter.js +23 -9
  129. package/dist/rules/linter/linter.js.map +1 -1
  130. package/dist/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
  131. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts +1 -1
  132. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.d.ts.map +1 -1
  133. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js +5 -2
  134. package/dist/rules/rtdb/grammar/RtdbExpr.ohm.generated.js.map +1 -1
  135. package/dist/rules/rtdb/grammar/simulator.d.ts.map +1 -1
  136. package/dist/rules/rtdb/grammar/simulator.js +61 -15
  137. package/dist/rules/rtdb/grammar/simulator.js.map +1 -1
  138. package/dist/rules/rtdb/simulation/handler.d.ts.map +1 -1
  139. package/dist/rules/rtdb/simulation/handler.js +96 -35
  140. package/dist/rules/rtdb/simulation/handler.js.map +1 -1
  141. package/dist/rules/simulator/document-lookups.d.ts +4 -0
  142. package/dist/rules/simulator/document-lookups.d.ts.map +1 -1
  143. package/dist/rules/simulator/document-lookups.js +42 -3
  144. package/dist/rules/simulator/document-lookups.js.map +1 -1
  145. package/dist/rules/simulator/eval-error.d.ts +11 -0
  146. package/dist/rules/simulator/eval-error.d.ts.map +1 -1
  147. package/dist/rules/simulator/eval-error.js +14 -0
  148. package/dist/rules/simulator/eval-error.js.map +1 -1
  149. package/dist/rules/simulator/evaluation-builtins.d.ts.map +1 -1
  150. package/dist/rules/simulator/evaluation-builtins.js +33 -8
  151. package/dist/rules/simulator/evaluation-builtins.js.map +1 -1
  152. package/dist/rules/simulator/evaluation-context.d.ts +12 -0
  153. package/dist/rules/simulator/evaluation-context.d.ts.map +1 -1
  154. package/dist/rules/simulator/evaluator.d.ts +1 -1
  155. package/dist/rules/simulator/evaluator.d.ts.map +1 -1
  156. package/dist/rules/simulator/evaluator.js +16 -4
  157. package/dist/rules/simulator/evaluator.js.map +1 -1
  158. package/dist/rules/simulator/handler.d.ts.map +1 -1
  159. package/dist/rules/simulator/handler.js +52 -5
  160. package/dist/rules/simulator/handler.js.map +1 -1
  161. package/dist/rules/simulator/lookup-budget.d.ts +37 -0
  162. package/dist/rules/simulator/lookup-budget.d.ts.map +1 -0
  163. package/dist/rules/simulator/lookup-budget.js +96 -0
  164. package/dist/rules/simulator/lookup-budget.js.map +1 -0
  165. package/dist/rules/stdlib-modules.d.ts +7 -0
  166. package/dist/rules/stdlib-modules.d.ts.map +1 -1
  167. package/dist/rules/stdlib-modules.js +11 -11
  168. package/dist/rules/stdlib-modules.js.map +1 -1
  169. package/dist/sandbox/admin-firestore/get-firestore.d.ts +6 -0
  170. package/dist/sandbox/admin-firestore/get-firestore.d.ts.map +1 -1
  171. package/dist/sandbox/admin-firestore/get-firestore.js +22 -3
  172. package/dist/sandbox/admin-firestore/get-firestore.js.map +1 -1
  173. package/dist/sandbox/index.d.ts +1 -1
  174. package/dist/sandbox/index.d.ts.map +1 -1
  175. package/dist/sandbox/index.js +1 -1
  176. package/dist/sandbox/index.js.map +1 -1
  177. package/dist/sandbox/persistence/types.d.ts +1 -1
  178. package/dist/sandbox/remote.d.ts +2 -2
  179. package/dist/sandbox/sandbox-context.d.ts +8 -1
  180. package/dist/sandbox/sandbox-context.d.ts.map +1 -1
  181. package/dist/sandbox/sandbox-context.js +59 -8
  182. package/dist/sandbox/sandbox-context.js.map +1 -1
  183. package/dist/sandbox/types/auth-state.d.ts +1 -0
  184. package/dist/sandbox/types/auth-state.d.ts.map +1 -1
  185. package/dist/sandbox/types/events.d.ts +2 -1
  186. package/dist/sandbox/types/events.d.ts.map +1 -1
  187. package/dist/sandbox/types/operation.d.ts +1 -0
  188. package/dist/sandbox/types/operation.d.ts.map +1 -1
  189. package/dist/storage/enforce.d.ts +20 -1
  190. package/dist/storage/enforce.d.ts.map +1 -1
  191. package/dist/storage/enforce.js +47 -14
  192. package/dist/storage/enforce.js.map +1 -1
  193. package/dist/storage/index.d.ts +1 -1
  194. package/dist/storage/index.d.ts.map +1 -1
  195. package/dist/storage/list.js +1 -1
  196. package/dist/storage/list.js.map +1 -1
  197. package/dist/storage/persistence.d.ts.map +1 -1
  198. package/dist/storage/persistence.js +66 -14
  199. package/dist/storage/persistence.js.map +1 -1
  200. package/dist/storage/sandbox/rules-evaluation-error.d.ts +25 -0
  201. package/dist/storage/sandbox/rules-evaluation-error.d.ts.map +1 -1
  202. package/dist/storage/sandbox/rules-evaluation-error.js +29 -0
  203. package/dist/storage/sandbox/rules-evaluation-error.js.map +1 -1
  204. package/dist/storage/sandbox/rules-evaluator.d.ts +10 -5
  205. package/dist/storage/sandbox/rules-evaluator.d.ts.map +1 -1
  206. package/dist/storage/sandbox/rules-evaluator.js +105 -33
  207. package/dist/storage/sandbox/rules-evaluator.js.map +1 -1
  208. package/dist/storage/sandbox/rules-methods.d.ts.map +1 -1
  209. package/dist/storage/sandbox/rules-methods.js +17 -7
  210. package/dist/storage/sandbox/rules-methods.js.map +1 -1
  211. package/dist/storage/service.d.ts +23 -2
  212. package/dist/storage/service.d.ts.map +1 -1
  213. package/dist/storage/service.js +37 -26
  214. package/dist/storage/service.js.map +1 -1
  215. package/package.json +42 -2
  216. package/src/ai/blocked.ts +97 -0
  217. package/src/ai/broker/broker.ts +153 -22
  218. package/src/ai/broker/gemini-engine.ts +311 -0
  219. package/src/ai/broker/index.ts +2 -0
  220. package/src/ai/broker/openai-engine.ts +31 -3
  221. package/src/ai/broker/synthesizer.ts +34 -0
  222. package/src/ai/broker/types.ts +27 -1
  223. package/src/ai/instances.ts +1 -1
  224. package/src/ai/internal.ts +5 -0
  225. package/src/ai/response-helpers.ts +6 -25
  226. package/src/analytics/index.ts +53 -0
  227. package/src/app/dispatch.test.ts +3 -1
  228. package/src/app-check/index.ts +41 -0
  229. package/src/auth/instances.ts +1 -1
  230. package/src/database/controls.ts +1 -1
  231. package/src/database/instances.ts +1 -1
  232. package/src/database/sandbox/backend.ts +1 -0
  233. package/src/database/sandbox/rules-eval.ts +18 -2
  234. package/src/database/sandbox/write-plane.ts +8 -2
  235. package/src/database/sandbox-controls.ts +29 -9
  236. package/src/database/sandbox-namespace.ts +28 -1
  237. package/src/deferred/entry.ts +167 -0
  238. package/src/firestore/instances.ts +1 -1
  239. package/src/firestore/lite.ts +91 -0
  240. package/src/firestore/persistence.ts +2 -2
  241. package/src/firestore/sandbox-controls.ts +10 -0
  242. package/src/functions/index.ts +38 -0
  243. package/src/messaging/client.ts +3 -2
  244. package/src/messaging/instance.ts +44 -7
  245. package/src/messaging/sw.ts +3 -2
  246. package/src/performance/index.ts +31 -0
  247. package/src/remote-config/index.ts +53 -0
  248. package/src/rules/grammar/FirestoreValidator.ts +44 -28
  249. package/src/rules/grammar/document-access-count.ts +75 -0
  250. package/src/rules/linter/ast-utils.ts +2 -36
  251. package/src/rules/linter/hallucinations.ts +123 -25
  252. package/src/rules/linter/linter.ts +32 -10
  253. package/src/rules/rtdb/grammar/RtdbExpr.ohm +5 -2
  254. package/src/rules/rtdb/grammar/RtdbExpr.ohm.generated.ts +5 -2
  255. package/src/rules/rtdb/grammar/simulator.ts +41 -15
  256. package/src/rules/rtdb/simulation/handler.ts +149 -46
  257. package/src/rules/simulator/document-lookups.ts +46 -3
  258. package/src/rules/simulator/eval-error.ts +16 -0
  259. package/src/rules/simulator/evaluation-builtins.ts +35 -8
  260. package/src/rules/simulator/evaluation-context.ts +12 -0
  261. package/src/rules/simulator/evaluator.ts +14 -3
  262. package/src/rules/simulator/handler.ts +68 -4
  263. package/src/rules/simulator/lookup-budget.ts +101 -0
  264. package/src/rules/stdlib-modules.ts +19 -11
  265. package/src/sandbox/admin-firestore/get-firestore.ts +19 -3
  266. package/src/sandbox/index.ts +5 -1
  267. package/src/sandbox/persistence/types.ts +1 -1
  268. package/src/sandbox/remote.ts +2 -2
  269. package/src/sandbox/sandbox-context.ts +69 -9
  270. package/src/sandbox/types/auth-state.ts +1 -1
  271. package/src/sandbox/types/events.ts +2 -1
  272. package/src/sandbox/types/operation.ts +1 -1
  273. package/src/storage/enforce.ts +53 -14
  274. package/src/storage/index.ts +1 -1
  275. package/src/storage/list.ts +1 -1
  276. package/src/storage/persistence.ts +74 -16
  277. package/src/storage/sandbox/rules-evaluation-error.ts +32 -0
  278. package/src/storage/sandbox/rules-evaluator.ts +109 -32
  279. package/src/storage/sandbox/rules-methods.ts +21 -7
  280. package/src/storage/service.ts +93 -27
  281. package/README.md.orig +0 -53
@@ -12,6 +12,7 @@
12
12
  * routing between the two planes is a single visibility decision.
13
13
  */
14
14
  import type { FirebaseApp } from '../app/types.js';
15
+ import type { Sandbox, SandboxContext } from '../sandbox/index.js';
15
16
  import {
16
17
  defaultRegistration,
17
18
  deliverToMessaging,
@@ -50,8 +51,8 @@ export type {
50
51
  * service-worker plane (upstream: `getMessagingInSw`, component name
51
52
  * `messaging-sw`).
52
53
  */
53
- export function getMessaging(app?: FirebaseApp): Messaging {
54
- return resolveMessaging('sw', app);
54
+ export function getMessaging(target?: FirebaseApp | Sandbox | SandboxContext): Messaging {
55
+ return resolveMessaging('sw', target);
55
56
  }
56
57
 
57
58
  /**
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `pyric/performance` — a DEFERRED mirror of `firebase/performance`.
3
+ *
4
+ * Performance Monitoring instruments real network and render timings and
5
+ * ships them to Google. Nothing about it is locally observable, so it is
6
+ * deferred.
7
+ *
8
+ * Every symbol below resolves and links so an app that swaps `firebase` for
9
+ * `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
10
+ * subpath. See `../deferred/entry.ts` for the full rationale.
11
+ *
12
+ * The value list is the exact public runtime surface of `firebase/performance`
13
+ * (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
14
+ * real mirror.
15
+ */
16
+ import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
17
+
18
+ export { PyricDeferredApiError } from '../deferred/entry.js';
19
+
20
+ export const {
21
+ getPerformance, initializePerformance, trace,
22
+ } = deferredEntry('performance');
23
+
24
+ // Type declarations. Aliased to the deferred placeholder so a consumer's own
25
+ // annotations keep type-checking: every deferred call returns `never`, which is
26
+ // assignable to any of these. Names that Firebase exports as a CLASS appear both
27
+ // here and above — a class is a value and a type, and both meanings must survive
28
+ // the swap.
29
+ export type FirebasePerformance = DeferredApi;
30
+ export type PerformanceSettings = DeferredApi;
31
+ export type PerformanceTrace = DeferredApi;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `pyric/remote-config` — a DEFERRED mirror of `firebase/remote-config`.
3
+ *
4
+ * Remote Config serves server-side parameter values with fetch/activate
5
+ * caching semantics. Mirroring it needs a sandbox-side config store and a
6
+ * model of the fetch throttle — buildable, not yet built.
7
+ *
8
+ * Every symbol below resolves and links so an app that swaps `firebase` for
9
+ * `pyric` still loads; touching one throws `PyricDeferredApiError` naming this
10
+ * subpath. See `../deferred/entry.ts` for the full rationale.
11
+ *
12
+ * The value list is the exact public runtime surface of `firebase/remote-config`
13
+ * (Firebase Web SDK 12.13.0). Keep it in sync when this entry graduates to a
14
+ * real mirror.
15
+ */
16
+ import { deferredEntry, type DeferredApi } from '../deferred/entry.js';
17
+
18
+ export { PyricDeferredApiError } from '../deferred/entry.js';
19
+
20
+ export const {
21
+ activate, ensureInitialized, fetchAndActivate, fetchConfig, getAll, getBoolean, getNumber,
22
+ getRemoteConfig, getString, getValue, onConfigUpdate, setCustomSignals,
23
+ setLogLevel,
24
+ } = deferredEntry('remote-config');
25
+
26
+ /**
27
+ * The one deferred symbol that answers instead of throwing: the real SDK
28
+ * resolves a boolean, and a deferred entry IS unsupported, so the standard
29
+ * `isSupported().then(ok => ok && get…(app))` guard must run — not crash.
30
+ */
31
+ export const isSupported = async (): Promise<boolean> => false;
32
+
33
+
34
+ // Type declarations. Aliased to the deferred placeholder so a consumer's own
35
+ // annotations keep type-checking: every deferred call returns `never`, which is
36
+ // assignable to any of these. Names that Firebase exports as a CLASS appear both
37
+ // here and above — a class is a value and a type, and both meanings must survive
38
+ // the swap.
39
+ export type ConfigUpdate = DeferredApi;
40
+ export type ConfigUpdateObserver = DeferredApi;
41
+ export type CustomSignals = DeferredApi;
42
+ export type FetchResponse = DeferredApi;
43
+ export type FetchStatus = DeferredApi;
44
+ export type FetchType = DeferredApi;
45
+ export type FirebaseExperimentDescription = DeferredApi;
46
+ export type FirebaseRemoteConfigObject = DeferredApi;
47
+ export type LogLevel = DeferredApi;
48
+ export type RemoteConfig = DeferredApi;
49
+ export type RemoteConfigOptions = DeferredApi;
50
+ export type RemoteConfigSettings = DeferredApi;
51
+ export type Unsubscribe = DeferredApi;
52
+ export type Value = DeferredApi;
53
+ export type ValueSource = DeferredApi;
@@ -2,6 +2,7 @@ import type {
2
2
  FirestoreRules, MatchBlock, AllowRule, FunctionDef, Expression, Operation,
3
3
  } from './FirestoreAST.js';
4
4
  import { RULES_BUILTIN_FUNCTIONS } from './builtin-functions.js';
5
+ import { countDocumentAccessCalls } from './document-access-count.js';
5
6
 
6
7
  export interface ValidationFinding {
7
8
  code: string;
@@ -26,8 +27,9 @@ export function validateFirestoreRules(ast: FirestoreRules): ValidationFinding[]
26
27
  // SEC-4: Check for default deny
27
28
  checkDefaultDeny(rootMatch, findings);
28
29
 
29
- // QUA-3: Duplicate function names
30
- checkDuplicateFunctions(rootMatch, findings);
30
+ // QUA-3: Duplicate function names in one scope (production compile
31
+ // rejection, so critical). Nested shadowing is legal and is not flagged.
32
+ checkDuplicateFunctions(ast, findings);
31
33
 
32
34
  // QUA-4: Unused functions
33
35
  checkUnusedFunctions(rootMatch, allCalls, findings);
@@ -124,12 +126,15 @@ function walkMatch(
124
126
  });
125
127
  }
126
128
 
127
- // SEM-3: get()/exists() budget exceeded
128
- const docReads = countDocReads(cond, new Map(localScope.map(f => [f.name, f])));
129
+ // SEM-3: document access budget exceeded. Production allows exactly 10
130
+ // get/exists/getAfter/existsAfter reads per request evaluation; the
131
+ // 11th fails, so the finding fires only ABOVE 10 (same boundary as the
132
+ // linter's GET_COUNT error and the simulator's runtime LookupBudget).
133
+ const docReads = countDocumentAccessCalls(cond, fnMap);
129
134
  if (docReads > 10) {
130
135
  findings.push({
131
136
  code: 'SEM-3', severity: 'high', path: pathStr, operation: opStr,
132
- message: `Rule at ${pathStr} has ${docReads} document reads (get/exists) — exceeds the 10-call budget`,
137
+ message: `Rule at ${pathStr} may perform ${docReads} document access calls (get/exists/getAfter/existsAfter), which exceeds the 10-call budget`,
133
138
  });
134
139
  }
135
140
 
@@ -323,21 +328,6 @@ function referencesResourceData(expr: Expression): boolean {
323
328
  );
324
329
  }
325
330
 
326
- function countDocReads(expr: Expression, functions: Map<string, FunctionDef>, visited = new Set<string>()): number {
327
- let count = 0;
328
- walkExpr(expr, e => {
329
- if (e.type === 'functionCall' && (e.name === 'get' || e.name === 'exists' || e.name === 'getAfter')) {
330
- count++;
331
- }
332
- // Follow user-defined function calls
333
- if (e.type === 'functionCall' && functions.has(e.name) && !visited.has(e.name)) {
334
- visited.add(e.name);
335
- count += countDocReads(functions.get(e.name)!.body, functions, visited);
336
- }
337
- });
338
- return count;
339
- }
340
-
341
331
  function collectFunctionCalls(expr: Expression): string[] {
342
332
  const calls: string[] = [];
343
333
  walkExpr(expr, e => {
@@ -357,22 +347,48 @@ function collectAllFunctions(match: MatchBlock): Set<string> {
357
347
  }
358
348
 
359
349
  // ---- QUA-3: Duplicate functions ----
350
+ //
351
+ // Production REJECTS two declarations of one function name in the SAME
352
+ // scope at compile time, so this is severity 'critical': the write gate
353
+ // (`write/handler.ts`) blocks only critical validator findings, and
354
+ // 'medium' folded to a mere warning that deployed anyway.
355
+ //
356
+ // A scope is one declaration list: the global list above `service`, the
357
+ // list directly inside `service`, or one match block's own list. A nested
358
+ // match block that redeclares a name from an enclosing scope SHADOWS it and
359
+ // is legal: the simulator implements shadowing in
360
+ // `simulator/match-resolution.ts`, and the production capture in
361
+ // `conformance/rules-corpus/storage/function-scopes-and-shadowing.ts` pins
362
+ // inner-shadows-outer as accepted. Flagging that would block a ruleset
363
+ // production deploys.
364
+ //
365
+ // The code stays QUA-3 (already registered and asserted by consumers);
366
+ // `DUPLICATE_FUNCTION` was considered and rejected, because the module
367
+ // resolver already uses that name for its own error code
368
+ // (`modules/resolver-core.ts`), and colliding would make mixed issue
369
+ // lists ambiguous.
370
+
371
+ function checkDuplicateFunctions(ast: FirestoreRules, findings: ValidationFinding[]) {
372
+ checkDupsInScope(ast.functions ?? [], 'global scope', findings);
373
+ checkDupsInScope(ast.service.functions ?? [], `service ${ast.service.name}`, findings);
374
+ checkDupsInMatchScopes(ast.service.match, findings);
375
+ }
360
376
 
361
- function checkDuplicateFunctions(match: MatchBlock, findings: ValidationFinding[]) {
377
+ function checkDupsInMatchScopes(match: MatchBlock, findings: ValidationFinding[]) {
362
378
  checkDupsInScope(match.functions, match.path.raw, findings);
363
- for (const child of match.children) checkDuplicateFunctions(child, findings);
379
+ for (const child of match.children) checkDupsInMatchScopes(child, findings);
364
380
  }
365
381
 
366
- function checkDupsInScope(fns: FunctionDef[], path: string, findings: ValidationFinding[]) {
367
- const seen = new Set<string>();
382
+ function checkDupsInScope(fns: readonly FunctionDef[], scope: string, findings: ValidationFinding[]) {
383
+ const declared = new Set<string>();
368
384
  for (const fn of fns) {
369
- if (seen.has(fn.name)) {
385
+ if (declared.has(fn.name)) {
370
386
  findings.push({
371
- code: 'QUA-3', severity: 'medium', path,
372
- message: `Duplicate function '${fn.name}' in scope at ${path}`,
387
+ code: 'QUA-3', severity: 'critical', path: scope,
388
+ message: `Duplicate function '${fn.name}' declared twice in ${scope}. Production rejects duplicate function declarations in one scope at compile time`,
373
389
  });
374
390
  }
375
- seen.add(fn.name);
391
+ declared.add(fn.name);
376
392
  }
377
393
  }
378
394
 
@@ -0,0 +1,75 @@
1
+ import type { Expression, FunctionDef } from './FirestoreAST.js';
2
+
3
+ /**
4
+ * The four built-in document access calls production counts against a
5
+ * request's 10-read budget.
6
+ */
7
+ const DOCUMENT_ACCESS_BUILTINS = new Set(['get', 'exists', 'getAfter', 'existsAfter']);
8
+
9
+ /**
10
+ * Count the document access calls (get/exists/getAfter/existsAfter)
11
+ * reachable from a rule condition, expanding each user-defined function
12
+ * once PER CALL SITE. `isOwner(a) && isOwner(b) && isOwner(c)` with three
13
+ * gets inside `isOwner` costs nine, matching production where each call
14
+ * performs its own reads (different arguments mean different paths).
15
+ * A function's `let` bindings evaluate on every call, so their reads count
16
+ * too. The call stack guards recursion only, and entries unwind on return,
17
+ * so sibling call sites each pay full price.
18
+ *
19
+ * This is a static over-approximation: production caches repeated reads of
20
+ * the SAME path within a request (see site-docs
21
+ * secure/firestore-rules-limits.md), but path identity is not decidable
22
+ * statically, so every call site is charged. The cache-aware, distinct-path
23
+ * count is the runtime concern of LookupBudget in
24
+ * simulator/lookup-budget.ts, which counts what an evaluation actually
25
+ * reads rather than what a condition could reach.
26
+ *
27
+ * This is the single walker behind both static consumers: the validator's
28
+ * SEM-3 finding and the linter's GET_COUNT warning.
29
+ */
30
+ export function countDocumentAccessCalls(
31
+ expr: Expression,
32
+ functions: Map<string, FunctionDef>,
33
+ callStack: Set<string> = new Set<string>(),
34
+ ): number {
35
+ let count = 0;
36
+
37
+ const visit = (e: Expression): void => {
38
+ switch (e.type) {
39
+ case 'functionCall':
40
+ if (DOCUMENT_ACCESS_BUILTINS.has(e.name)) {
41
+ count++;
42
+ } else {
43
+ const fn = functions.get(e.name);
44
+ const isExpandable = fn !== undefined && !callStack.has(e.name);
45
+ if (isExpandable) {
46
+ callStack.add(e.name);
47
+ for (const binding of fn!.lets) visit(binding.value);
48
+ visit(fn!.body);
49
+ callStack.delete(e.name); // unwind: count once per call site, not once per rule
50
+ }
51
+ }
52
+ e.args.forEach(visit);
53
+ break;
54
+ case 'binaryOp': visit(e.left); visit(e.right); break;
55
+ case 'unaryOp': visit(e.operand); break;
56
+ case 'ternary': visit(e.condition); visit(e.consequent); visit(e.alternate); break;
57
+ case 'methodCall': visit(e.object); e.args.forEach(visit); break;
58
+ case 'memberAccess': visit(e.object); break;
59
+ case 'bracketAccess': visit(e.object); visit(e.index); break;
60
+ case 'sliceAccess': visit(e.object); visit(e.start); visit(e.end); break;
61
+ case 'inExpr': visit(e.element); visit(e.collection); break;
62
+ case 'isExpr': visit(e.value); break;
63
+ case 'listLiteral': e.elements.forEach(visit); break;
64
+ case 'mapLiteral': e.entries.forEach((entry) => { visit(entry.key); visit(entry.value); }); break;
65
+ case 'pathLiteral':
66
+ for (const segment of e.segments) {
67
+ if (typeof segment !== 'string') visit(segment);
68
+ }
69
+ break;
70
+ }
71
+ };
72
+
73
+ visit(expr);
74
+ return count;
75
+ }
@@ -218,44 +218,10 @@ export function maxCallDepth(start: string, graph: Map<string, string[]>, visite
218
218
  return 1 + max;
219
219
  }
220
220
 
221
- /**
222
- * Count get() and exists() calls reachable from an expression,
223
- * following function calls transitively.
224
- */
225
- export function countGetCalls(expr: Expression, functions: Map<string, FunctionDef>, visited = new Set<string>()): number {
226
- let count = 0;
227
- const walk = (e: Expression) => {
228
- switch (e.type) {
229
- case 'functionCall':
230
- if (e.name === 'get' || e.name === 'exists') {
231
- count++;
232
- } else if (functions.has(e.name) && !visited.has(e.name)) {
233
- visited.add(e.name);
234
- const fn = functions.get(e.name)!;
235
- walk(fn.body);
236
- for (const b of fn.lets) walk(b.value);
237
- }
238
- e.args.forEach(walk);
239
- break;
240
- case 'binaryOp': walk(e.left); walk(e.right); break;
241
- case 'unaryOp': walk(e.operand); break;
242
- case 'methodCall': walk(e.object); e.args.forEach(walk); break;
243
- case 'memberAccess': walk(e.object); break;
244
- case 'bracketAccess': walk(e.object); walk(e.index); break;
245
- case 'ternary': walk(e.condition); walk(e.consequent); walk(e.alternate); break;
246
- case 'inExpr': walk(e.element); walk(e.collection); break;
247
- case 'isExpr': walk(e.value); break;
248
- case 'listLiteral': e.elements.forEach(walk); break;
249
- case 'mapLiteral': e.entries.forEach(en => { walk(en.key); walk(en.value); }); break;
250
- }
251
- };
252
- walk(expr);
253
- return count;
254
- }
255
-
256
221
  /**
257
222
  * Count how many times each user-defined function is called in an expression.
258
- * Unlike countGetCalls (which deduplicates), this counts raw call-site occurrences.
223
+ * Counts raw call-site occurrences at the top level only: it does not expand
224
+ * nested user-function bodies the way countDocumentAccessCalls does.
259
225
  */
260
226
  export function countFunctionCallSites(expr: Expression, fnNames: Set<string>): Map<string, number> {
261
227
  const counts = new Map<string, number>();
@@ -95,7 +95,14 @@ const HALLUCINATED_GLOBALS: Record<string, string> = {
95
95
  undefined: '`undefined` does not exist in rules — use `null`',
96
96
  };
97
97
 
98
- const VALID_MATH_METHODS = new Set(['abs', 'ceil', 'floor', 'round', 'sqrt', 'pow', 'isNaN']);
98
+ /**
99
+ * The math-namespace functions production Firestore actually compiles.
100
+ * Notably ABSENT: `isInfinite`. Reference docs list it, but production
101
+ * rejects it at compile (`Function not found error: Name: [math.isInfinite]`).
102
+ * Exported so the stdlib-modules drift test can assert the documented
103
+ * catalog never re-grows a name this validator rejects.
104
+ */
105
+ export const VALID_MATH_METHODS = new Set(['abs', 'ceil', 'floor', 'round', 'sqrt', 'pow', 'isNaN']);
99
106
 
100
107
 
101
108
  /**
@@ -122,6 +129,21 @@ const WRONG_CONTEXT_PATHS: WrongPath[] = [
122
129
  suggestion: '`resource.id` is not available — capture the document id with `/{docId}` in the match path' },
123
130
  ];
124
131
 
132
+ /**
133
+ * Auth-token claims that are typed BOOL in production. Comparing one
134
+ * against a STRING literal compiles fine (CEL is dynamically typed) but
135
+ * the comparison is cross-type, so `== "true"` is always false and
136
+ * `!= "true"` is always true, and the latter silently opens the rule.
137
+ * WRONG_CONTEXT_PATHS can't express this: it matches `receiver.property`
138
+ * at depth 2, while token claims live at `request.auth.token.<claim>`
139
+ * and the bug is in the *comparison*, not the path itself.
140
+ *
141
+ * Only claims verifiably typed bool belong here. `email_verified` is the
142
+ * documented one (`request.auth.token.email_verified: bool`).
143
+ * `firebase.sign_in_provider` is a string, so it is deliberately NOT listed.
144
+ */
145
+ const BOOL_TOKEN_CLAIMS = new Set(['email_verified']);
146
+
125
147
  /**
126
148
  * Built-in CEL methods that, when accessed without parentheses, are
127
149
  * almost certainly a missing-call mistake. Restricted to a small
@@ -186,40 +208,51 @@ interface Loc {
186
208
  ruleIndex?: number;
187
209
  }
188
210
 
211
+ /**
212
+ * Visit every expression in the match tree, carrying the set of
213
+ * user-defined function names visible where the expression sits. Scope
214
+ * accumulates outward-in: a block sees its own declarations plus every
215
+ * enclosing scope's, which is the resolution order the simulator
216
+ * implements in `simulator/match-resolution.ts`.
217
+ */
189
218
  function walkAllExpressions(
190
219
  match: MatchBlock,
191
- visit: (expr: Expression, loc: Loc) => void,
220
+ inheritedScope: ReadonlySet<string>,
221
+ visit: (expr: Expression, loc: Loc, scope: ReadonlySet<string>) => void,
192
222
  ) {
193
223
  const path = match.path.raw;
224
+ const scope = new Set(inheritedScope);
225
+ for (const fn of match.functions) scope.add(fn.name);
194
226
  for (const fn of match.functions) {
195
227
  const loc: Loc = { matchPath: path, functionName: fn.name };
196
- walkExpr(fn.body, loc, visit);
197
- for (const b of fn.lets) walkExpr(b.value, loc, visit);
228
+ walkExpr(fn.body, loc, scope, visit);
229
+ for (const b of fn.lets) walkExpr(b.value, loc, scope, visit);
198
230
  }
199
231
  for (let i = 0; i < match.allows.length; i++) {
200
- walkExpr(match.allows[i].condition, { matchPath: path, ruleIndex: i }, visit);
232
+ walkExpr(match.allows[i].condition, { matchPath: path, ruleIndex: i }, scope, visit);
201
233
  }
202
- for (const child of match.children) walkAllExpressions(child, visit);
234
+ for (const child of match.children) walkAllExpressions(child, scope, visit);
203
235
  }
204
236
 
205
237
  function walkExpr(
206
238
  expr: Expression,
207
239
  loc: Loc,
208
- visit: (expr: Expression, loc: Loc) => void,
240
+ scope: ReadonlySet<string>,
241
+ visit: (expr: Expression, loc: Loc, scope: ReadonlySet<string>) => void,
209
242
  ) {
210
- visit(expr, loc);
243
+ visit(expr, loc, scope);
211
244
  switch (expr.type) {
212
- case 'binaryOp': walkExpr(expr.left, loc, visit); walkExpr(expr.right, loc, visit); break;
213
- case 'unaryOp': walkExpr(expr.operand, loc, visit); break;
214
- case 'methodCall': walkExpr(expr.object, loc, visit); expr.args.forEach(a => walkExpr(a, loc, visit)); break;
215
- case 'memberAccess': walkExpr(expr.object, loc, visit); break;
216
- case 'bracketAccess': walkExpr(expr.object, loc, visit); walkExpr(expr.index, loc, visit); break;
217
- case 'ternary': walkExpr(expr.condition, loc, visit); walkExpr(expr.consequent, loc, visit); walkExpr(expr.alternate, loc, visit); break;
218
- case 'inExpr': walkExpr(expr.element, loc, visit); walkExpr(expr.collection, loc, visit); break;
219
- case 'isExpr': walkExpr(expr.value, loc, visit); break;
220
- case 'listLiteral': expr.elements.forEach(e => walkExpr(e, loc, visit)); break;
221
- case 'mapLiteral': expr.entries.forEach(en => { walkExpr(en.key, loc, visit); walkExpr(en.value, loc, visit); }); break;
222
- case 'functionCall': expr.args.forEach(a => walkExpr(a, loc, visit)); break;
245
+ case 'binaryOp': walkExpr(expr.left, loc, scope, visit); walkExpr(expr.right, loc, scope, visit); break;
246
+ case 'unaryOp': walkExpr(expr.operand, loc, scope, visit); break;
247
+ case 'methodCall': walkExpr(expr.object, loc, scope, visit); expr.args.forEach(a => walkExpr(a, loc, scope, visit)); break;
248
+ case 'memberAccess': walkExpr(expr.object, loc, scope, visit); break;
249
+ case 'bracketAccess': walkExpr(expr.object, loc, scope, visit); walkExpr(expr.index, loc, scope, visit); break;
250
+ case 'ternary': walkExpr(expr.condition, loc, scope, visit); walkExpr(expr.consequent, loc, scope, visit); walkExpr(expr.alternate, loc, scope, visit); break;
251
+ case 'inExpr': walkExpr(expr.element, loc, scope, visit); walkExpr(expr.collection, loc, scope, visit); break;
252
+ case 'isExpr': walkExpr(expr.value, loc, scope, visit); break;
253
+ case 'listLiteral': expr.elements.forEach(e => walkExpr(e, loc, scope, visit)); break;
254
+ case 'mapLiteral': expr.entries.forEach(en => { walkExpr(en.key, loc, scope, visit); walkExpr(en.value, loc, scope, visit); }); break;
255
+ case 'functionCall': expr.args.forEach(a => walkExpr(a, loc, scope, visit)); break;
223
256
  }
224
257
  }
225
258
 
@@ -231,9 +264,19 @@ function walkExpr(
231
264
  * distinct (rule, key, location) tuple — duplicate identical patterns
232
265
  * within the same rule are de-duplicated.
233
266
  */
234
- function isUnsupportedDebugCall(expr: Expression, allowDebug?: boolean): boolean {
267
+ /**
268
+ * A `debug(...)` call that resolves to NOTHING. Production has no built-in
269
+ * `debug`, but a ruleset is free to declare `function debug(v) { ... }`, and
270
+ * that call resolves and evaluates like any other user function, so only an
271
+ * unresolved call is a finding.
272
+ */
273
+ function isUnresolvedDebugCall(
274
+ expr: Expression,
275
+ scope: ReadonlySet<string>,
276
+ allowDebug?: boolean,
277
+ ): boolean {
235
278
  if (allowDebug) return false;
236
- return expr.type === 'functionCall' && expr.name === 'debug';
279
+ return expr.type === 'functionCall' && expr.name === 'debug' && !scope.has('debug');
237
280
  }
238
281
 
239
282
  function isHallucinatedMethodCall(expr: Expression): boolean {
@@ -275,6 +318,37 @@ function isWrongContextPath(expr: Expression): { suggestion: string } | undefine
275
318
  return WRONG_CONTEXT_PATHS.find(p => p.receiver === recv && p.property === prop);
276
319
  }
277
320
 
321
+ /** Is `expr` a member access spelling exactly `request.auth.token.<claim>`
322
+ * for a claim in BOOL_TOKEN_CLAIMS? */
323
+ function boolTokenClaimName(expr: Expression): string | undefined {
324
+ if (expr.type !== 'memberAccess' || !BOOL_TOKEN_CLAIMS.has(expr.property)) return undefined;
325
+ const token = expr.object;
326
+ if (token.type !== 'memberAccess' || token.property !== 'token') return undefined;
327
+ const auth = token.object;
328
+ if (auth.type !== 'memberAccess' || auth.property !== 'auth') return undefined;
329
+ return auth.object.type === 'identifier' && auth.object.name === 'request'
330
+ ? expr.property
331
+ : undefined;
332
+ }
333
+
334
+ /**
335
+ * `request.auth.token.email_verified == "true"` (or `!=`, either operand
336
+ * order). The claim is a bool; a string literal can never equal it, so the
337
+ * comparison is a constant: `==` always denies, `!=` always allows.
338
+ */
339
+ function boolTokenClaimStringComparison(
340
+ expr: Expression,
341
+ ): { claim: string; op: string; literal: string } | undefined {
342
+ if (expr.type !== 'binaryOp' || (expr.op !== '==' && expr.op !== '!=')) return undefined;
343
+ for (const [side, other] of [[expr.left, expr.right], [expr.right, expr.left]] as const) {
344
+ const claim = boolTokenClaimName(side);
345
+ if (claim && other.type === 'literal' && typeof other.value === 'string') {
346
+ return { claim, op: expr.op, literal: other.value };
347
+ }
348
+ }
349
+ return undefined;
350
+ }
351
+
278
352
  function isLengthPropertyAccessOnMethod(expr: Expression): boolean {
279
353
  return expr.type === 'memberAccess' && expr.property === 'length' && expr.object.type === 'methodCall';
280
354
  }
@@ -301,14 +375,23 @@ export function checkHallucinations(ast: FirestoreRules, options: { allowDebug?:
301
375
  warnings.push(w);
302
376
  }
303
377
 
304
- walkAllExpressions(ast.service.match, (expr, loc) => {
305
- if (isUnsupportedDebugCall(expr, options.allowDebug)) {
378
+ // Functions declared above `service` and directly inside it are visible
379
+ // everywhere below, so they seed the scope the walk carries down.
380
+ const outerScope = new Set<string>();
381
+ for (const fn of ast.functions ?? []) outerScope.add(fn.name);
382
+ for (const fn of ast.service.functions ?? []) outerScope.add(fn.name);
383
+
384
+ walkAllExpressions(ast.service.match, outerScope, (expr, loc, scope) => {
385
+ if (isUnresolvedDebugCall(expr, scope, options.allowDebug)) {
306
386
  emit('HALLUCINATED_GLOBAL', 'debug', loc, {
307
387
  rule: 'HALLUCINATED_GLOBAL',
308
388
  severity: 'error',
309
- message: '`debug()` helper is only permitted in local testing environments; remove it before deployment.',
389
+ message:
390
+ '`debug()` is not a Firestore rules function and no function named `debug` is declared in scope. '
391
+ + 'Production rejects the whole ruleset at compile time with '
392
+ + '`Function not found error: Name: [debug]`, so nothing in the file deploys.',
310
393
  location: loc,
311
- fix: 'Remove debug() wrapper around the expression.',
394
+ fix: 'Remove the debug() call and evaluate the inner expression directly.',
312
395
  });
313
396
  }
314
397
 
@@ -400,6 +483,21 @@ export function checkHallucinations(ast: FirestoreRules, options: { allowDebug?:
400
483
  }
401
484
  }
402
485
 
486
+ const boolClaim = boolTokenClaimStringComparison(expr);
487
+ if (boolClaim) {
488
+ const { claim, op, literal } = boolClaim;
489
+ emit('BOOL_TOKEN_CLAIM', `${claim}|${op}|${literal}`, loc, {
490
+ rule: 'BOOL_TOKEN_CLAIM',
491
+ severity: 'error',
492
+ message:
493
+ `\`request.auth.token.${claim}\` is a bool, but it is compared against the string ` +
494
+ `"${literal}", a cross-type comparison that is always ${op === '==' ? 'false (rule always denies)' : 'true (rule silently allows)'}. ` +
495
+ `Compare against the boolean literal instead: \`request.auth.token.${claim} ${op} true\`.`,
496
+ location: loc,
497
+ fix: `Drop the quotes: \`request.auth.token.${claim} ${op} ${literal === 'false' ? 'false' : 'true'}\`.`,
498
+ });
499
+ }
500
+
403
501
  if (isLengthPropertyAccessOnMethod(expr)) {
404
502
  emit('LENGTH_PROPERTY', '', loc, {
405
503
  rule: 'LENGTH_PROPERTY',
@@ -18,7 +18,6 @@ import {
18
18
  extractFirstExpression,
19
19
  buildCallGraph,
20
20
  maxCallDepth,
21
- countGetCalls,
22
21
  countFunctionCallSites,
23
22
  functionContainsGet,
24
23
  referencesRequestTime,
@@ -26,6 +25,7 @@ import {
26
25
  collectAllRules,
27
26
  } from './ast-utils.js';
28
27
  import { checkSyntaxHints, checkHallucinations } from './hallucinations.js';
28
+ import { countDocumentAccessCalls } from '../grammar/document-access-count.js';
29
29
 
30
30
  // ═══ Types ═══
31
31
 
@@ -93,7 +93,11 @@ const THRESHOLDS = {
93
93
  CALL_DEPTH_WARN: 6,
94
94
  CALL_DEPTH_ERROR: 10,
95
95
  GET_COUNT_WARN: 5,
96
- GET_COUNT_ERROR: 10, // documented by Google
96
+ // Production allows EXACTLY 10 document access calls per request
97
+ // evaluation; the 11th fails (site-docs secure/firestore-rules-limits.md).
98
+ // Error fires strictly ABOVE this value, the same boundary as SEM-3 and
99
+ // the simulator's runtime LookupBudget.
100
+ GET_COUNT_ERROR: 10,
97
101
  };
98
102
 
99
103
  // ═══ Lint Rules ═══
@@ -350,12 +354,14 @@ function checkGetCount(
350
354
  for (const fn of allFunctions) fnMap.set(fn.name, fn);
351
355
 
352
356
  for (let i = 0; i < rules.length; i++) {
353
- const count = countGetCalls(rules[i].rule.condition, fnMap);
354
- if (count >= THRESHOLDS.GET_COUNT_ERROR) {
357
+ const count = countDocumentAccessCalls(rules[i].rule.condition, fnMap);
358
+ // Error only ABOVE the limit: production allows exactly 10 and fails
359
+ // the 11th, so a rule at exactly 10 is legal (still worth the WARN).
360
+ if (count > THRESHOLDS.GET_COUNT_ERROR) {
355
361
  warnings.push({
356
362
  rule: 'GET_COUNT',
357
363
  severity: 'error',
358
- message: `Rule #${i} may invoke ${count} get()/exists() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
364
+ message: `Rule #${i} may invoke ${count} get()/exists()/getAfter()/existsAfter() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
359
365
  location: { ruleIndex: i },
360
366
  fix: 'Cache get() results via a config() wrapper function. Same-path calls are cached by Firestore.',
361
367
  });
@@ -363,7 +369,7 @@ function checkGetCount(
363
369
  warnings.push({
364
370
  rule: 'GET_COUNT',
365
371
  severity: 'warning',
366
- message: `Rule #${i} invokes ${count} get()/exists() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
372
+ message: `Rule #${i} invokes ${count} get()/exists()/getAfter()/existsAfter() calls. Limit is ${THRESHOLDS.GET_COUNT_ERROR}.`,
367
373
  location: { ruleIndex: i },
368
374
  });
369
375
  }
@@ -767,7 +773,16 @@ export interface LintOptions {
767
773
  * block linting of a valid current ruleset.
768
774
  */
769
775
  previousSource?: string;
770
- /** Set to true when validating in a local emulator or testing environment where debug() is permitted. */
776
+ /**
777
+ * Set to true ONLY when linting a scratch ruleset that will never be
778
+ * deployed and an unresolved `debug()` call should be tolerated.
779
+ * Production Firestore rejects a ruleset that calls `debug()` at compile
780
+ * time (`Function not found error: Name: [debug]`), so the linter rejects
781
+ * it by default, including when `testCases` is supplied. A ruleset that
782
+ * declares its own `function debug(...)` is never flagged, with or
783
+ * without this flag. This flag is an explicit caller choice; it is never
784
+ * inferred from other options.
785
+ */
771
786
  allowDebug?: boolean;
772
787
  }
773
788
 
@@ -836,8 +851,15 @@ export function lintFirestoreRules(source: string, options: LintOptions = {}): L
836
851
  // Rule 8: Get duplication (same get()-containing function called multiple times)
837
852
  checkGetDuplication(allRules, allFunctions, warnings);
838
853
 
839
- // Rule 9: Hallucinations — JS-style code that parses but fails at runtime
840
- warnings.push(...checkHallucinations(ast, { allowDebug: options.allowDebug || Boolean(options.testCases && options.testCases.length > 0) }));
854
+ // Rule 9: Hallucinations, JS-style code that parses but fails at runtime.
855
+ // `allowDebug` is an EXPLICIT caller opt-in only. It used to be implied by
856
+ // a non-empty `testCases` array, which silently disabled the debug()
857
+ // rejection in any lint run that also carried a test suite, exactly the
858
+ // authoring path that feeds the write gate. Production rejects debug() at
859
+ // compile time (`Function not found error: Name: [debug]`), so the default
860
+ // must reject; a caller linting a ruleset that will never deploy can still
861
+ // pass `allowDebug: true`.
862
+ warnings.push(...checkHallucinations(ast, { allowDebug: options.allowDebug }));
841
863
 
842
864
  // Rule 9.5: Always-true predicates and recursive-wildcard open rules.
843
865
  // Severity: error so deployRules refuses to swap. The agent's #1
@@ -884,7 +906,7 @@ export function lintFirestoreRules(source: string, options: LintOptions = {}): L
884
906
  let maxExprs = 0;
885
907
  let maxGets = 0;
886
908
  for (const r of allRules) {
887
- const gets = countGetCalls(r.rule.condition, fnMap);
909
+ const gets = countDocumentAccessCalls(r.rule.condition, fnMap);
888
910
  if (gets > maxGets) maxGets = gets;
889
911
  }
890
912