@lunora/advisor 0.0.0 → 1.0.0-alpha.10

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 (49) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +130 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/index.d.mts +1680 -0
  5. package/dist/index.d.ts +1680 -0
  6. package/dist/index.mjs +89 -0
  7. package/dist/packem_shared/AE_METRIC_EVENTS-DexctYv6.mjs +85 -0
  8. package/dist/packem_shared/adminRouteWithoutGuard-UUGBkAjU.mjs +33 -0
  9. package/dist/packem_shared/authApiCallWithoutHeaders-BeJhCZaf.mjs +38 -0
  10. package/dist/packem_shared/circularFk-B2freHrP.mjs +84 -0
  11. package/dist/packem_shared/constraintValidator-Dr9Py3FD.mjs +186 -0
  12. package/dist/packem_shared/containerOversizedInstance-5U1VKPRM.mjs +36 -0
  13. package/dist/packem_shared/containerPublicInternet-CuNerJE5.mjs +30 -0
  14. package/dist/packem_shared/duplicateIndex-BOublMSt.mjs +57 -0
  15. package/dist/packem_shared/emptyIndex-BX8EuEY7.mjs +32 -0
  16. package/dist/packem_shared/externalSourceOnGlobal-Bg-NfCX9.mjs +30 -0
  17. package/dist/packem_shared/externalSourceUnscoped-5vT-Bup3.mjs +44 -0
  18. package/dist/packem_shared/filterWithoutIndex-BYVeJaSs.mjs +31 -0
  19. package/dist/packem_shared/finding-Dm_zvzS1.mjs +16 -0
  20. package/dist/packem_shared/fk-index-IUK1ukgs.mjs +7 -0
  21. package/dist/packem_shared/fromServerSchema-WVRvXPy8.mjs +56 -0
  22. package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +35 -0
  23. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +4 -0
  24. package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +48 -0
  25. package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +30 -0
  26. package/dist/packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs +36 -0
  27. package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +45 -0
  28. package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +61 -0
  29. package/dist/packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs +26 -0
  30. package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +35 -0
  31. package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +38 -0
  32. package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +32 -0
  33. package/dist/packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs +36 -0
  34. package/dist/packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs +30 -0
  35. package/dist/packem_shared/relationReferencesUnknownField-YznyXt_7.mjs +54 -0
  36. package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +33 -0
  37. package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +56 -0
  38. package/dist/packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs +34 -0
  39. package/dist/packem_shared/shapeUnknownTable-C8aDWFoe.mjs +34 -0
  40. package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +26 -0
  41. package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +34 -0
  42. package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +32 -0
  43. package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +45 -0
  44. package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +53 -0
  45. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs +42 -0
  46. package/dist/packem_shared/workflowDuplicateStepName-ioBxPBCy.mjs +48 -0
  47. package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +34 -0
  48. package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +38 -0
  49. package/package.json +40 -17
@@ -0,0 +1,1680 @@
1
+ import { Schema } from '@lunora/server';
2
+ /**
3
+ * One `httpRoute.<verb>("/admin/…")` REST route on an admin/privileged-looking
4
+ * path, with whether its handler references an auth/admin guard — the input the
5
+ * `admin_route_without_guard` lint consumes. Produced by the codegen feeder;
6
+ * runtime callers don't supply it, so the lint finds nothing there.
7
+ */
8
+ interface AdvisorAdminRoute {
9
+ /** The exported binding name of the route handler. */
10
+ exportName: string;
11
+ /** Source file relative to the lunora dir, no extension. */
12
+ file: string;
13
+ /** HTTP verb the route binds to (uppercased), e.g. `"POST"`. */
14
+ method: string;
15
+ /** The route path, e.g. `/admin/users`. */
16
+ path: string;
17
+ /** `true` when the handler references an auth/session/admin guard. */
18
+ usesGuard: boolean;
19
+ }
20
+ /**
21
+ * One public procedure's argument validators reduced to the input-safety facts
22
+ * the `public_arg_uses_any` and `unbounded_string_arg` lints consume: which args
23
+ * are declared `v.any()` (unvalidated input) and which `v.string()` args carry no
24
+ * length bound (a DoS / storage-abuse vector). Produced by the codegen feeder for
25
+ * public procedures only; internal functions take server-trusted input. Runtime
26
+ * callers don't supply it, so the lints find nothing there.
27
+ */
28
+ interface AdvisorArgumentValidator {
29
+ /** Arg names declared as `v.any()`. */
30
+ anyArgs: ReadonlyArray<string>;
31
+ /** The exported binding name of the procedure (e.g. `updateProfile`). */
32
+ exportName: string;
33
+ /** Source file relative to the lunora dir, no extension. */
34
+ file: string;
35
+ /** 1-based line of the registration call, or `0` when unknown. */
36
+ line: number;
37
+ /** Arg names declared as `v.string()` with no statically-visible max-length bound. */
38
+ unboundedStringArgs: ReadonlyArray<string>;
39
+ }
40
+ /**
41
+ * One `ctx.authApi.&lt;method>(...)` call discovered in a function body — the input
42
+ * the `auth_api_call_without_headers` lint consumes. Produced by the codegen
43
+ * feeder; runtime callers don't supply it, so the lint finds nothing there.
44
+ */
45
+ interface AdvisorAuthApiCall {
46
+ /** The exported function performing the call (e.g. `createOrg`). */
47
+ exportName: string;
48
+ /** Source file the call appears in (relative to the lunora dir, no extension). */
49
+ file: string;
50
+ /** True when the call's argument object includes a `headers` property. */
51
+ hasHeaders: boolean;
52
+ /** 1-based line of the call, or `0` when unknown. */
53
+ line: number;
54
+ /** The better-auth method invoked (e.g. `banUser`); empty when not statically known. */
55
+ method: string;
56
+ }
57
+ /**
58
+ * One container declaration discovered in `lunora/containers.ts` — the input
59
+ * the `container_*` lints consume. Produced by the codegen feeder (which lifts
60
+ * the static fields of each `defineContainer({...})` export); runtime callers
61
+ * don't supply it, so the container lints simply find nothing there.
62
+ *
63
+ * A structural subset of codegen's `ContainerIR`, so the feeder can pass the
64
+ * IR array straight through without conversion (mirrors how `AdvisorQueryRead`
65
+ * tracks `QueryReadIR`).
66
+ */
67
+ interface AdvisorContainer {
68
+ /**
69
+ * Whether outbound internet was explicitly configured. `undefined` means
70
+ * the field was omitted (platform default `true`) or wasn't a static literal.
71
+ */
72
+ enableInternet?: boolean;
73
+ /** The `lunora/containers.ts` export name, e.g. `transcoder`. */
74
+ exportName: string;
75
+ /** Declared `instanceType`: a named size, or a custom `{ vcpu, memoryMib, diskMb }`. */
76
+ instanceType?: string | {
77
+ diskMb?: number;
78
+ memoryMib?: number;
79
+ vcpu?: number;
80
+ };
81
+ /** Declared `maxInstances` cap, when present. */
82
+ maxInstances?: number;
83
+ /** Declared `sleepAfter` value, when a static literal. */
84
+ sleepAfter?: number | string;
85
+ }
86
+ /**
87
+ * One `ctx.sql` access discovered lexically inside a `query(...)` or
88
+ * `mutation(...)` handler body — the input the `hyperdrive_outside_action` lint
89
+ * consumes. Produced by the codegen feeder, which walks each exported function's
90
+ * handler with ts-morph and records reads of the Hyperdrive `ctx.sql` surface
91
+ * (`ctx.sql(...)`, `ctx.sql.query(...)`).
92
+ *
93
+ * Hyperdrive points at an **external** database Lunora does not own: a `ctx.sql`
94
+ * call is a network round-trip with a mutable result (non-deterministic, like
95
+ * `fetch`) and its writes are invisible to Lunora live queries. It therefore
96
+ * belongs **only** in `action(...)` handlers. Calls inside `action(...)` are
97
+ * intentionally **not** recorded — actions are the escape hatch. Runtime callers
98
+ * don't supply this, so the lint finds nothing there.
99
+ */
100
+ interface AdvisorHyperdriveCall {
101
+ /** The accessed `ctx.sql` surface, e.g. `ctx.sql.query` / `ctx.sql`. */
102
+ callee: string;
103
+ /** The exported function performing the access (e.g. `listCustomers`). */
104
+ exportName: string;
105
+ /** Source file the access appears in (relative to the lunora dir, no extension). */
106
+ file: string;
107
+ /** Which procedure kind the access lives in — only `query`/`mutation` are flagged; actions are exempt. */
108
+ kind: "mutation" | "query";
109
+ /** 1-based line of the access, or `0` when unknown. */
110
+ line: number;
111
+ }
112
+ /**
113
+ * Observed read signal over a table — the input the `index_utilization` runtime
114
+ * lint consumes. Produced by the studio backend from each shard's recorded
115
+ * metrics.
116
+ *
117
+ * `AdvisorTableScan` comes straight from the per-`(function, table)` full-scan
118
+ * attribution the runtime already records (`__lunora_metrics_scans`, surfaced as
119
+ * `FunctionCallStat.scannedTables`). Each entry is a table the app read with no
120
+ * index — a hot one points at a missing index.
121
+ *
122
+ * `AdvisorIndexHit` is the per-declared-index hit count. The runtime now records
123
+ * this in the durable `__lunora_metrics_index` table (stamped on every index use
124
+ * via `onIndexUse`) and surfaces it through the `getMetrics` admin RPC; the
125
+ * studio sums the per-shard arrays and feeds them as `context.indexHits`, and the
126
+ * lint flags a declared index with zero recorded reads as dead. When the feed is
127
+ * absent (a static caller, or a shard that recorded nothing) the dead-index half
128
+ * is a no-op and only the hot-scan half runs off the scan attribution.
129
+ */
130
+ /**
131
+ * Per-table full-scan volume observed over the window — a read that hit no
132
+ * index. Sourced from `FunctionCallStat.scannedTables` aggregated across
133
+ * functions and shards. Runtime callers supply this; static callers don't, so
134
+ * the hot-scan half of the lint finds nothing there.
135
+ */
136
+ interface AdvisorTableScan {
137
+ /** Total full-scans of `table` over the observed window. */
138
+ scans: number;
139
+ /** The full-scanned table. */
140
+ table: string;
141
+ }
142
+ /**
143
+ * Per-declared-index hit count observed over the window — how many recorded
144
+ * reads used the index to narrow.
145
+ *
146
+ * Produced by the runtime: every index use (`onIndexUse` in the DO) bumps a
147
+ * per-`(table, index)` counter in the durable `__lunora_metrics_index` table, the
148
+ * complement of the full-*scan* attribution in `__lunora_metrics_scans`. The
149
+ * `getMetrics` admin RPC surfaces it per shard; the studio sums the arrays across
150
+ * shards and passes them as `context.indexHits`. A declared index that appears
151
+ * with `reads: 0` (or is absent entirely after the schema reconciliation) is dead
152
+ * for the window.
153
+ */
154
+ interface AdvisorIndexHit {
155
+ /** The declared index name. */
156
+ index: string;
157
+ /** Recorded reads that used this index to narrow over the observed window. */
158
+ reads: number;
159
+ /** The table the index is declared on. */
160
+ table: string;
161
+ }
162
+ /**
163
+ * One `ctx.db.insert("table", …)` write discovered in a function body — the
164
+ * write-side analog of `AdvisorQueryRead`, the input the
165
+ * `table_without_insert` lint consumes. Produced by the codegen feeder (which
166
+ * attributes each insert to the exported function performing it); runtime callers
167
+ * don't supply it, so the lint simply finds nothing there.
168
+ */
169
+ interface AdvisorInsertWrite {
170
+ /** The exported function performing the insert (e.g. `send`). */
171
+ exportName: string;
172
+ /** Source file the insert appears in (relative to the lunora dir, no extension). */
173
+ file: string;
174
+ /** 1-based line of the `insert(...)` call, or `0` when unknown. */
175
+ line: number;
176
+ /** The inserted table; empty when the `insert(...)` argument is not a string literal. */
177
+ table: string;
178
+ }
179
+ /**
180
+ * One procedure (query / mutation / action) discovered in the lunora source,
181
+ * reduced to the facts the `mask_uncovered_pii_column` lint needs: whether the
182
+ * procedure's builder chain includes `.use(mask(...))`, which `(table, column)`
183
+ * pairs that mask declares, and which tables the procedure reads or writes.
184
+ * Produced by the codegen feeder; runtime callers don't supply it, so the lint
185
+ * finds nothing there. The column-level twin of `AdvisorRlsProcedure`.
186
+ */
187
+ interface AdvisorMaskProcedure {
188
+ /** The exported binding name of the procedure (e.g. `listUsers`). */
189
+ exportName: string;
190
+ /** Source file relative to the lunora dir, no extension. */
191
+ file: string;
192
+ /**
193
+ * The `(table, column)` pairs declared by the `mask(policies)` object passed
194
+ * to `.use(mask(...))` in this procedure's builder chain. Empty when the
195
+ * policies argument is not a statically-readable object literal
196
+ * (conservative: `usesMask` is still `true`).
197
+ */
198
+ maskColumns: ReadonlyArray<{
199
+ column: string;
200
+ table: string;
201
+ }>;
202
+ /** Tables read by the procedure via `ctx.db.query("table")` / `ctx.db.findMany(...)` etc. */
203
+ tablesRead: ReadonlyArray<string>;
204
+ /** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
205
+ tablesWritten: ReadonlyArray<string>;
206
+ /**
207
+ * `true` when the procedure's builder chain includes `.use(mask(...))` — the
208
+ * `mask` callee is identified by name from `@lunora/server`. `false` when no
209
+ * `.use(mask(...))` is found in the chain (or the procedure uses the bare
210
+ * `query({...})` factory form, which never carries a builder chain at all).
211
+ */
212
+ usesMask: boolean;
213
+ /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
214
+ visibility: "internal" | "public";
215
+ }
216
+ /**
217
+ * One whole-row `ctx.db.replace(id, document)` write discovered inside a custom
218
+ * mutator's authoritative `server` impl (`lunora/mutators.ts`) — the input the
219
+ * `mutator_full_row_replace` lint consumes.
220
+ *
221
+ * In the local-first sync engine a `replace` overwrites the entire row, so a
222
+ * concurrent edit to a *different* column (committed between this mutator's read
223
+ * and its write) is silently clobbered. `patch(id, { onlyTheField })` merges at
224
+ * the column level instead, letting independent field edits coexist — the
225
+ * blessed pattern for mutators on a synced (poke-live) table. Produced by the
226
+ * codegen feeder, which attributes each `replace` to the mutator export
227
+ * performing it; runtime callers don't supply it, so the lint finds nothing
228
+ * there.
229
+ */
230
+ interface AdvisorMutatorWrite {
231
+ /** The mutator export whose `server` impl performs the replace (e.g. `renameChannel`). */
232
+ exportName: string;
233
+ /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
234
+ file: string;
235
+ /** 1-based line of the `replace(...)` call, or `0` when unknown. */
236
+ line: number;
237
+ }
238
+ /**
239
+ * One non-deterministic API call discovered lexically inside a `query(...)` or
240
+ * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
241
+ * lint consumes. Produced by the codegen feeder, which walks each exported
242
+ * function's handler with ts-morph and records calls to `Date.now`,
243
+ * `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and `fetch`.
244
+ * Calls inside `action(...)` handlers are intentionally **not** recorded — actions
245
+ * are the determinism escape hatch. Runtime callers don't supply this, so the
246
+ * lint finds nothing there.
247
+ */
248
+ interface AdvisorNondeterministicCall {
249
+ /** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
250
+ callee: string;
251
+ /** The exported function performing the call (e.g. `sendMessage`). */
252
+ exportName: string;
253
+ /** Source file the call appears in (relative to the lunora dir, no extension). */
254
+ file: string;
255
+ /** Which procedure kind the call lives in — only `query`/`mutation` handlers are non-deterministic; actions are exempt. */
256
+ kind: "mutation" | "query";
257
+ /** 1-based line of the call, or `0` when unknown. */
258
+ line: number;
259
+ }
260
+ /**
261
+ * One procedure (query / mutation / action) reduced to the protective middlewares
262
+ * its builder chain installs plus the behavioural facts that decide whether a
263
+ * guard is expected — the input the `public_mutation_without_ratelimit` and
264
+ * `user_creating_mutation_without_captcha` lints consume. A `protectPublic({...})`
265
+ * bundle is unwrapped by the feeder: its keys set `usesRateLimit`/`usesCaptcha`
266
+ * exactly as the standalone `.use(...)` steps would. Produced by the codegen
267
+ * feeder; runtime callers don't supply it, so the lints find nothing there.
268
+ */
269
+ interface AdvisorProcedureProtection {
270
+ /** `true` when the handler references `ctx.mail` / `ctx.email` (sends mail). */
271
+ callsMail: boolean;
272
+ /** The exported binding name of the procedure (e.g. `signUp`). */
273
+ exportName: string;
274
+ /** Source file relative to the lunora dir, no extension. */
275
+ file: string;
276
+ /** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
277
+ kind: "action" | "mutation" | "query";
278
+ /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
279
+ usesCaptcha: boolean;
280
+ /** `true` when the chain carries `.use(mask(...))`. */
281
+ usesMask: boolean;
282
+ /** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
283
+ usesRateLimit: boolean;
284
+ /** `true` when the chain carries `.use(rls(...))`. */
285
+ usesRls: boolean;
286
+ /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
287
+ visibility: "internal" | "public";
288
+ /** `true` when the handler inserts into a user/session/account-shaped table. */
289
+ writesUserTable: boolean;
290
+ }
291
+ /**
292
+ * One query read discovered in a function body — the input the
293
+ * `filter_without_index` lint consumes. Produced by the codegen feeder (which
294
+ * parses `ctx.db.query("table")…` chains from the AST); runtime callers don't
295
+ * supply it, so the lint simply finds nothing there.
296
+ */
297
+ interface AdvisorQueryRead {
298
+ /** Source file the read appears in (relative to the lunora dir, no extension). */
299
+ file: string;
300
+ /** True when the chain calls `.filter(...)`. */
301
+ hasFilter: boolean;
302
+ /** True when the chain narrows with `.withIndex(...)` or `.withSearchIndex(...)`. */
303
+ hasIndex: boolean;
304
+ /** 1-based line of the `query(...)` call, or `0` when unknown. */
305
+ line: number;
306
+ /** The queried table; empty when the `query(...)` argument is not a string literal. */
307
+ table: string;
308
+ }
309
+ /**
310
+ * One `ctx.r2sql` access discovered lexically inside a `query(...)` or
311
+ * `mutation(...)` handler body — the input the `r2sql_outside_action` lint
312
+ * consumes. Produced by the codegen feeder, which walks each exported function's
313
+ * handler with ts-morph and records reads of the R2 SQL `ctx.r2sql` surface
314
+ * (`ctx.r2sql.query(...)`, `ctx.r2sql.from(...)`, …).
315
+ *
316
+ * R2 SQL queries Apache Iceberg tables over an **external** REST endpoint Lunora
317
+ * does not own (there is no Workers binding): a `ctx.r2sql` call is a network
318
+ * round-trip with a mutable result (non-deterministic, like `fetch`) and its
319
+ * reads are invisible to Lunora live queries. It therefore belongs **only** in
320
+ * `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
321
+ * recorded — actions are the escape hatch. Runtime callers don't supply this, so
322
+ * the lint finds nothing there.
323
+ */
324
+ interface AdvisorR2sqlCall {
325
+ /** The accessed `ctx.r2sql` surface, e.g. `ctx.r2sql.query` / `ctx.r2sql.from`. */
326
+ callee: string;
327
+ /** The exported function performing the access (e.g. `topPerRegion`). */
328
+ exportName: string;
329
+ /** Source file the access appears in (relative to the lunora dir, no extension). */
330
+ file: string;
331
+ /** Which procedure kind the access lives in — only `query`/`mutation` are flagged; actions are exempt. */
332
+ kind: "mutation" | "query";
333
+ /** 1-based line of the access, or `0` when unknown. */
334
+ line: number;
335
+ }
336
+ /**
337
+ * One procedure (query / mutation / action) discovered in the lunora source,
338
+ * reduced to the facts the `rls_uncovered_table` lint needs: whether the
339
+ * procedure's builder chain includes `.use(rls(...))`, and which tables the
340
+ * procedure reads or writes. Produced by the codegen feeder; runtime callers
341
+ * don't supply it, so the lint finds nothing there.
342
+ */
343
+ interface AdvisorRlsProcedure {
344
+ /** The exported binding name of the procedure (e.g. `listDocuments`). */
345
+ exportName: string;
346
+ /** Source file relative to the lunora dir, no extension. */
347
+ file: string;
348
+ /**
349
+ * Tables explicitly named in the `rls(policies)` array passed to `.use(rls(...))`
350
+ * in this procedure's builder chain. Empty when the policies argument is not a
351
+ * statically-readable array literal (conservative: `usesRls` is still `true`).
352
+ */
353
+ rlsTables: ReadonlyArray<string>;
354
+ /** Tables read by the procedure via `ctx.db.query("table")` / `ctx.db.findMany(...)` etc. */
355
+ tablesRead: ReadonlyArray<string>;
356
+ /** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
357
+ tablesWritten: ReadonlyArray<string>;
358
+ /**
359
+ * `true` when the procedure's builder chain includes `.use(rls(...))` — the
360
+ * `rls` callee is identified by name from `@lunora/server`. `false` when no
361
+ * `.use(rls(...))` is found in the chain (or the procedure uses the bare
362
+ * `query({...})` factory form, which never carries a builder chain at all).
363
+ */
364
+ usesRls: boolean;
365
+ /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
366
+ visibility: "internal" | "public";
367
+ }
368
+ /**
369
+ * Normalized, feeder-agnostic view of a schema that lints run against. Both the
370
+ * runtime `@lunora/server` {@link Schema} (record-shaped) and `@lunora/codegen`'s
371
+ * `SchemaIR` (array-shaped, AST-derived) collapse to this same shape, so a lint
372
+ * is written once and runs in either place. It carries only what the lints
373
+ * read — tables, their columns, indexes, and relations.
374
+ */
375
+ interface AdvisorSchema {
376
+ tables: ReadonlyArray<AdvisorTable>;
377
+ }
378
+ /** A table plus the column/index/relation metadata lints inspect. */
379
+ interface AdvisorTable {
380
+ /**
381
+ * `true` when the table is written outside Lunora's discoverable insert path
382
+ * — declared via `.externallyManaged()` (e.g. `@lunora/auth`'s better-auth
383
+ * tables, `@lunora/ratelimit`'s store). Insert-path lints
384
+ * (`table_without_insert`) skip such tables. Defaults to `false`.
385
+ */
386
+ externallyManaged?: boolean;
387
+ /**
388
+ * Set when the table was declared with `.source(...)` (plan 077) —
389
+ * materialized from an external Hyperdrive-backed database. Read by the
390
+ * `external_source_*` lints to enforce the tenant-scope boundary (mandatory
391
+ * `tenantBy` under `.shardBy()`) and reject sourcing a `.global()` table.
392
+ * Optional — feeders that don't know about sourced tables omit it.
393
+ */
394
+ externalSource?: AdvisorExternalSource;
395
+ /**
396
+ * Declared column names (the `defineTable({...})` keys). Excludes the
397
+ * framework-managed system fields `_id` / `_creationTime`, which every table
398
+ * has implicitly — lints that resolve a column treat those as always valid.
399
+ */
400
+ fields: ReadonlyArray<string>;
401
+ /** Every declared index, across all kinds (secondary / search / rank / vector). */
402
+ indexes: ReadonlyArray<AdvisorIndex>;
403
+ /** Table name. */
404
+ name: string;
405
+ /**
406
+ * Column names that are optional or nullable and therefore may legally hold
407
+ * `null` / `undefined` in stored rows. Populated by {@link fromServerSchema}
408
+ * from the runtime validator graph (`v.optional(...)` → kind `"optional"`;
409
+ * `.nullable()` → `column.notNull === false`). When absent (e.g. from the
410
+ * codegen feeder, which does not supply this field), constraint lints that
411
+ * check NOT NULL should skip the check entirely or treat every field as
412
+ * required (the codegen feeder never runs runtime lints anyway).
413
+ */
414
+ optionalFields?: ReadonlySet<string>;
415
+ /** Declared relations (`.relations((r) => …)`). */
416
+ relations: ReadonlyArray<AdvisorRelation>;
417
+ /**
418
+ * Storage tier the table is declared in: `"global"` (a `.global()` table,
419
+ * lives in D1 — the cross-shard tier), `"shardBy"` (partitioned across
420
+ * shard DOs by a key), or `"root"` (the default single-DO table). Read by
421
+ * the `shape_*` lints to flag replication shapes targeting a `.global()`
422
+ * table (poll-refreshed/latency-tiered, not poke-live). Optional — the
423
+ * codegen feeder always supplies it, the runtime feeder derives it; a feeder
424
+ * that omits it leaves tier-sensitive lints to treat the table as local.
425
+ */
426
+ shardKind?: "global" | "root" | "shardBy";
427
+ }
428
+ /**
429
+ * One declared index, flattened across Lunora's index kinds so a single lint can
430
+ * reason about every column an index touches. `kind` distinguishes the DSL that
431
+ * declared it — only `index` (a btree secondary index) covers a foreign-key
432
+ * equality lookup, so the FK lint filters on it. `fields` is every column the
433
+ * index references (a secondary index's columns; a search index's text +
434
+ * filter fields; a rank index's sort + partition fields; a vector index's
435
+ * source field). `unique` is set only for unique secondary indexes.
436
+ */
437
+ interface AdvisorIndex {
438
+ fields: ReadonlyArray<string>;
439
+ kind: "index" | "rank" | "search" | "vector";
440
+ name: string;
441
+ unique?: boolean;
442
+ }
443
+ /** The statically-knowable `.source(...)` bits the `external_source_*` lints read. */
444
+ interface AdvisorExternalSource {
445
+ /** `true` when a `reconcileEveryMs` was given (the incremental-mode delete-visibility companion). */
446
+ hasReconcile?: boolean;
447
+ /** `true` when a `tenantBy` mapper was given — the tenant-isolation boundary. */
448
+ hasTenantBy: boolean;
449
+ /** Delete-detection mode literal, when given (`"full-pull"` | `"incremental"`). */
450
+ mode?: string;
451
+ /**
452
+ * `true` when `.source(...)` was declared but its config wasn't a static object
453
+ * literal, so `hasTenantBy` (and the rest) couldn't be read. Only the codegen
454
+ * feeder can hit this; the runtime feeder always holds the real config.
455
+ */
456
+ unanalyzable?: boolean;
457
+ }
458
+ /**
459
+ * One declared relation. For a `one` relation the FK column `field` lives on
460
+ * the holding table; for `many` it lives on the target. `name` is the accessor
461
+ * the relation is loaded under.
462
+ */
463
+ interface AdvisorRelation {
464
+ field: string;
465
+ kind: "many" | "one";
466
+ name: string;
467
+ onDelete?: "cascade" | "restrict" | "set null";
468
+ references: string;
469
+ table: string;
470
+ }
471
+ /**
472
+ * Adapt the runtime `@lunora/server` {@link Schema} into an {@link AdvisorSchema}.
473
+ * Runtime callers (the studio backend, a live shard) hold the real schema
474
+ * object; this collapses its record-keyed `tables`/`relationMap` into the array
475
+ * form lints consume and flattens the per-kind index arrays into one list. The
476
+ * codegen feeder builds the same shape from its AST IR independently (it never
477
+ * imports `@lunora/server`).
478
+ */
479
+ declare const fromServerSchema: (schema: Schema) => AdvisorSchema;
480
+ /**
481
+ * One secret-shaped string literal discovered in the lunora source — the input
482
+ * the `hardcoded_secret` lint consumes. The full value is never carried; only a
483
+ * redacted {@link AdvisorSecretLiteral.preview}. Produced by the codegen feeder
484
+ * (complementing the pre-commit `vis secrets` scan); runtime callers don't supply
485
+ * it, so the lint finds nothing there.
486
+ */
487
+ interface AdvisorSecretLiteral {
488
+ /** Source file relative to the lunora dir, no extension. */
489
+ file: string;
490
+ /** Heuristic that matched, e.g. `stripe_live_key` / `aws_access_key` / `private_key` / `high_entropy`. */
491
+ kind: string;
492
+ /** 1-based line of the literal, or `0` when unknown. */
493
+ line: number;
494
+ /** Redacted preview (first few chars + length) — never the full secret. */
495
+ preview: string;
496
+ }
497
+ /**
498
+ * A replication shape declared via `defineShape({ table, where, columns? })` in
499
+ * `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
500
+ * The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
501
+ * against the declared schema to flag a shape targeting an unknown table or a
502
+ * `.global()` table (which replicates through the latency-tiered D1 poll path,
503
+ * not the poke-live op-log). Supplied by the codegen feeder, which lifts only
504
+ * the export name + the static `table` literal; absent for runtime callers,
505
+ * where the shape lints find nothing.
506
+ */
507
+ interface AdvisorShape {
508
+ /** Export binding name — the shape's registry key (e.g. `channelMessages`). */
509
+ exportName: string;
510
+ /** File the shape is declared in (relative, for the operator to open). */
511
+ file: string;
512
+ /**
513
+ * The `table` string literal the shape replicates from, or `undefined` when
514
+ * the feeder could not read it as a plain string literal — tier-sensitive
515
+ * lints skip a shape with no resolvable table rather than guessing.
516
+ */
517
+ table?: string;
518
+ }
519
+ /**
520
+ * One shard's observed traffic share — the input the `hot_shard` runtime lint
521
+ * consumes. Produced by the studio backend, which fans out over a sharded
522
+ * function's shards and reads each shard's recorded request volume from the
523
+ * durable `__lunora_metrics` accumulator (`SUM(calls)`) — or, equivalently, the
524
+ * per-shard request-log count. Codegen and other static callers don't supply
525
+ * it, so the lint simply finds nothing there.
526
+ *
527
+ * The lint is a pure function over its context, so it can't fan out over shards
528
+ * itself; the caller does the cross-shard read and hands the aggregated
529
+ * distribution here, exactly as the codegen feeder hands `AdvisorQueryRead`s for
530
+ * the static query lints.
531
+ */
532
+ interface AdvisorShardTraffic {
533
+ /**
534
+ * The sharded function group these shards belong to, when the caller scopes
535
+ * the distribution to one `.shardBy(...)` function. Used only to name the
536
+ * finding; empty when the traffic is the whole deployment's shard set.
537
+ */
538
+ group?: string;
539
+ /** Total requests (function dispatches) recorded against this shard over the observed window. */
540
+ requests: number;
541
+ /**
542
+ * The shard key (the Durable Object id name) traffic was attributed to —
543
+ * a user / tenant / room id, depending on the `.shardBy(...)` key. Empty for
544
+ * the unnamed root DO.
545
+ */
546
+ shardKey: string;
547
+ }
548
+ /**
549
+ * One `ctx.sql` tagged-template interpolation that splices an unparameterized
550
+ * string-building expression into the query — the input the `sql_injection_risk`
551
+ * lint consumes. A `${…}` placeholder that simply names a value is bound as a
552
+ * parameter by the Hyperdrive driver and is *not* recorded; only in-place string
553
+ * construction (`"… " + raw`, a nested template literal) reaches here. Produced by
554
+ * the codegen feeder; runtime callers don't supply it, so the lint finds nothing
555
+ * there.
556
+ */
557
+ interface AdvisorSqlInterpolation {
558
+ /** The exported binding name of the procedure performing the `ctx.sql` call. */
559
+ exportName: string;
560
+ /** Source file relative to the lunora dir, no extension. */
561
+ file: string;
562
+ /** 1-based line of the interpolation, or `0` when unknown. */
563
+ line: number;
564
+ }
565
+ /**
566
+ * A bounded sample of rows from one table, fed into the constraint-validator
567
+ * lint by the studio backend (via `readTablePage`). The cap prevents unbounded
568
+ * scans while still catching obvious violations on small-to-medium tables.
569
+ *
570
+ * The studio notes the cap to the operator when the row count exceeds it
571
+ * (`truncated: true`), so violations on rows beyond the sample window are not
572
+ * silently missed — the finding description mentions the cap.
573
+ */
574
+ interface AdvisorTableSample {
575
+ /** The cap applied; equals `rows.length` when not truncated. */
576
+ readonly cap: number;
577
+ /**
578
+ * The row ids of every existing row in this table (bounded to `cap`), used
579
+ * for FK referential-integrity checks: if a FK value does not appear in the
580
+ * target table's `existingIds`, it is a dangling reference.
581
+ */
582
+ readonly existingIds: ReadonlySet<string>;
583
+ /** Sampled rows (up to `cap`). Each row includes `_id` and all declared columns. */
584
+ readonly rows: ReadonlyArray<Record<string, unknown>>;
585
+ /** The table's name. */
586
+ readonly table: string;
587
+ /** Whether more rows exist beyond the cap. */
588
+ readonly truncated: boolean;
589
+ }
590
+ /**
591
+ * The two workflow-shaped inputs the `workflow_*` lints consume, produced by the
592
+ * codegen feeder. {@link AdvisorWorkflow} is the declaration side (one per
593
+ * `defineWorkflow` export in `lunora/workflows.ts`); {@link AdvisorWorkflowCall}
594
+ * is the use side (one per `ctx.workflows.get("name")` call discovered in a
595
+ * function body). Runtime callers don't supply either, so the workflow lints
596
+ * simply find nothing there.
597
+ *
598
+ * Both are structural subsets of codegen's `WorkflowIR` / `WorkflowCallIR`, so
599
+ * the feeder passes the IR arrays straight through without conversion (mirrors
600
+ * how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
601
+ * `InsertWriteIR`).
602
+ */
603
+ /** One durable step call lifted from a workflow handler body — the input the duplicate-step-name lint compares. Structural subset of codegen's `WorkflowStepIR`. */
604
+ interface AdvisorWorkflowStep {
605
+ /** 1-based line of the durable step call, or `0` when unknown. */
606
+ line: number;
607
+ /** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
608
+ method: string;
609
+ /** The step's static label (the first string-literal argument). */
610
+ name: string;
611
+ }
612
+ /** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
613
+ interface AdvisorWorkflow {
614
+ /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
615
+ exportName: string;
616
+ /**
617
+ * The durable step labels discovered in the handler body, in source order —
618
+ * the duplicate-step-name input. Cloudflare memoizes a step by its name, so a
619
+ * name used twice makes the second call silently return the first's cached
620
+ * result. Supplied by the codegen feeder; `undefined` for runtime callers,
621
+ * where the lint finds nothing.
622
+ */
623
+ steps?: ReadonlyArray<AdvisorWorkflowStep>;
624
+ }
625
+ /** One `ctx.workflows.get("name")` call discovered in a function body. */
626
+ interface AdvisorWorkflowCall {
627
+ /** The exported function performing the call (e.g. `create`). */
628
+ exportName: string;
629
+ /** Source file the call appears in (relative to the lunora dir, no extension). */
630
+ file: string;
631
+ /** 1-based line of the `get(...)` call, or `0` when unknown. */
632
+ line: number;
633
+ /** The referenced workflow export name; empty when the `get(...)` argument is not a string literal. */
634
+ workflow: string;
635
+ }
636
+ /**
637
+ * Severity of a finding, mirroring splinter's `level`. `ERROR` is a definite
638
+ * problem, `WARN` a likely one, `INFO` an advisory nudge.
639
+ */
640
+ type Level = "ERROR" | "INFO" | "WARN";
641
+ /**
642
+ * Who the finding concerns, mirroring splinter's `facing`. `EXTERNAL` findings
643
+ * affect clients of the app (performance/security a user can feel); `INTERNAL`
644
+ * ones are operator-only hygiene.
645
+ */
646
+ type Facing = "EXTERNAL" | "INTERNAL";
647
+ /**
648
+ * Concern bucket a lint belongs to. `SCHEMA` covers shape/correctness nits that
649
+ * are neither a perf nor a security issue (missing primary key, duplicate
650
+ * index). `PERFORMANCE` and `SECURITY` match splinter's two categories.
651
+ */
652
+ type Category = "PERFORMANCE" | "SCHEMA" | "SECURITY";
653
+ /**
654
+ * Where a lint draws its evidence from.
655
+ *
656
+ * `static` runs against the declared {@link AdvisorSchema} alone (tables,
657
+ * indexes, relations) — deterministic, runnable at codegen/build time, and
658
+ * catches a problem _before_ it ships. This is the edge Lunora has over a
659
+ * live-DB-only advisor like Supabase's.
660
+ *
661
+ * `runtime` needs observed signal from a running shard (full-scan attribution,
662
+ * function call stats). Added in a later slice; the context grows optional
663
+ * fields the runtime lints read.
664
+ */
665
+ type LintSource = "runtime" | "static";
666
+ /**
667
+ * One emitted advisory, shaped after splinter's lint-view row so the studio
668
+ * Advisors table can render any lint uniformly. `cacheKey` is a stable,
669
+ * content-derived id used to dedup across runs and to let an operator dismiss a
670
+ * specific finding without silencing the whole lint.
671
+ */
672
+ interface Finding {
673
+ /** Stable identifier for dedup/dismissal across runs. */
674
+ cacheKey: string;
675
+ /** The lint's concern buckets (usually one). */
676
+ categories: Category[];
677
+ /** Human-readable explanation of the rule in general terms. */
678
+ description: string;
679
+ /** The specific violation message for _this_ occurrence. */
680
+ detail: string;
681
+ /** Who the finding concerns. */
682
+ facing: Facing;
683
+ /** Severity. */
684
+ level: Level;
685
+ /** Structured context (table, field, index, …) for the UI and deep links. */
686
+ metadata: Record<string, unknown>;
687
+ /** The lint id that produced this finding, e.g. `unindexed_foreign_key`. */
688
+ name: string;
689
+ /** How to fix it — a doc URL or short imperative guidance. */
690
+ remediation: string;
691
+ /** Short headline for the finding. */
692
+ title: string;
693
+ }
694
+ /**
695
+ * Everything a lint may inspect. Static lints read only {@link LintContext.schema};
696
+ * runtime lints will additionally read observed-signal fields added here later.
697
+ */
698
+ interface LintContext {
699
+ /**
700
+ * `httpRoute.&lt;verb>("/admin/…")` routes on admin/privileged-looking paths and
701
+ * whether each references an auth/admin guard — the `admin_route_without_guard`
702
+ * input. Supplied by the codegen feeder; absent for runtime callers, where the
703
+ * lint finds nothing.
704
+ */
705
+ adminRoutes?: ReadonlyArray<AdvisorAdminRoute>;
706
+ /**
707
+ * Per-public-procedure argument validators that weaken input safety — the
708
+ * `public_arg_uses_any` (`v.any()` args) and `unbounded_string_arg` (length-less
709
+ * `v.string()` args) input. Supplied by the codegen feeder for public procedures
710
+ * only; absent for runtime callers, where the lints find nothing.
711
+ */
712
+ argValidators?: ReadonlyArray<AdvisorArgumentValidator>;
713
+ /**
714
+ * `ctx.authApi.&lt;method>(...)` calls discovered in function bodies (the
715
+ * `auth_api_call_without_headers` input). Supplied by the codegen feeder; absent
716
+ * for runtime callers, where the lint finds nothing.
717
+ */
718
+ authApiCalls?: ReadonlyArray<AdvisorAuthApiCall>;
719
+ /**
720
+ * Containers declared in `lunora/containers.ts` — the `container_*` lint
721
+ * input. Supplied by the codegen feeder; absent for runtime callers, where
722
+ * the container lints find nothing.
723
+ */
724
+ containers?: ReadonlyArray<AdvisorContainer>;
725
+ /**
726
+ * Hyperdrive `ctx.sql` accesses discovered lexically inside `query`/`mutation`
727
+ * handler bodies — the `hyperdrive_outside_action` input. Supplied by the
728
+ * codegen feeder, which omits `action` handlers (where `ctx.sql` is the typed,
729
+ * intended surface); absent for runtime callers, where the lint finds nothing.
730
+ */
731
+ hyperdriveCalls?: ReadonlyArray<AdvisorHyperdriveCall>;
732
+ /**
733
+ * Per-declared-index hit counts observed at runtime (the dead-index half of
734
+ * the `index_utilization` lint input). Supplied by the studio backend, which
735
+ * sums the per-`(table, index)` reads each shard records in the durable
736
+ * `__lunora_metrics_index` table and surfaces through the `getMetrics` admin
737
+ * RPC (see {@link AdvisorIndexHit}). Absent for static callers, where the
738
+ * dead-index check finds nothing.
739
+ */
740
+ indexHits?: ReadonlyArray<AdvisorIndexHit>;
741
+ /**
742
+ * Insert writes discovered in function bodies (the `table_without_insert`
743
+ * input). Supplied by the codegen feeder; absent for runtime callers, where
744
+ * the write-shaped lints simply find nothing.
745
+ */
746
+ inserts?: ReadonlyArray<AdvisorInsertWrite>;
747
+ /**
748
+ * Per-procedure column-masking usage discovered in function bodies (the
749
+ * `mask_uncovered_pii_column` input). Carries whether each procedure's builder
750
+ * chain includes `.use(mask(...))`, which `(table, column)` pairs its mask
751
+ * policy declares, and which tables the procedure reads/writes. Supplied by
752
+ * the codegen feeder; absent for runtime callers, where the lint finds
753
+ * nothing.
754
+ */
755
+ maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
756
+ /**
757
+ * Whole-row `ctx.db.replace(id, document)` writes lifted from custom
758
+ * mutators' authoritative `server` impls (the `mutator_full_row_replace`
759
+ * input). Each `replace` overwrites the entire row, clobbering a concurrent
760
+ * edit to a different column on a synced table. Supplied by the codegen
761
+ * feeder; absent for runtime callers, where the lint finds nothing.
762
+ */
763
+ mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
764
+ /**
765
+ * Non-deterministic API calls (`Date.now`, `Math.random`,
766
+ * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
767
+ * inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
768
+ * input. Supplied by the codegen feeder, which omits `action` handlers (their
769
+ * non-determinism is intentional); absent for runtime callers, where the lint
770
+ * finds nothing.
771
+ */
772
+ nondeterministicCalls?: ReadonlyArray<AdvisorNondeterministicCall>;
773
+ /**
774
+ * Per-procedure protective-middleware snapshots — the
775
+ * `public_mutation_without_ratelimit` and `user_creating_mutation_without_captcha`
776
+ * input. Records which `.use(...)` guards (`rateLimit`, captcha, `rls`, `mask`,
777
+ * the `protectPublic` bundle) each procedure carries and whether it writes a
778
+ * user table or sends mail. Supplied by the codegen feeder; absent for runtime
779
+ * callers, where the lints find nothing.
780
+ */
781
+ procedureProtections?: ReadonlyArray<AdvisorProcedureProtection>;
782
+ /**
783
+ * Query reads discovered in function bodies (the `filter_without_index`
784
+ * input). Supplied by the codegen feeder; absent for runtime callers, where
785
+ * the query-shaped lints simply find nothing.
786
+ */
787
+ queries?: ReadonlyArray<AdvisorQueryRead>;
788
+ /**
789
+ * R2 SQL `ctx.r2sql` accesses discovered lexically inside `query`/`mutation`
790
+ * handler bodies — the `r2sql_outside_action` input. Supplied by the codegen
791
+ * feeder, which omits `action` handlers (where `ctx.r2sql` is the typed,
792
+ * intended surface); absent for runtime callers, where the lint finds nothing.
793
+ */
794
+ r2sqlCalls?: ReadonlyArray<AdvisorR2sqlCall>;
795
+ /**
796
+ * Per-procedure RLS usage discovered in function bodies (the
797
+ * `rls_uncovered_table` input). Carries whether each procedure's builder chain
798
+ * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
799
+ * tables its RLS policy array names. Supplied by the codegen feeder; absent for
800
+ * runtime callers, where the lint finds nothing.
801
+ */
802
+ rlsProcedures?: ReadonlyArray<AdvisorRlsProcedure>;
803
+ /** The declared schema under audit, normalized to the feeder-agnostic {@link AdvisorSchema}. */
804
+ schema: AdvisorSchema;
805
+ /**
806
+ * Secret-shaped string literals discovered in the lunora source — the
807
+ * `hardcoded_secret` input. Each carries only a redacted preview, never the
808
+ * full value. Supplied by the codegen feeder; absent for runtime callers,
809
+ * where the lint finds nothing.
810
+ */
811
+ secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
812
+ /**
813
+ * Replication shapes declared via `defineShape` in `lunora/shapes.ts` — the
814
+ * `shape_unknown_table` and `shape_targets_global_table` lint input. Each
815
+ * carries the export name and its static `table` literal, cross-referenced
816
+ * against {@link LintContext.schema}. Supplied by the codegen feeder; absent
817
+ * for runtime callers, where the shape lints find nothing.
818
+ */
819
+ shapes?: ReadonlyArray<AdvisorShape>;
820
+ /**
821
+ * Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
822
+ * studio backend, which fans out over a sharded function's shards and reads
823
+ * each shard's recorded request volume from the durable `__lunora_metrics`
824
+ * accumulator. Absent for static callers, where the lint finds nothing.
825
+ */
826
+ shardTraffic?: ReadonlyArray<AdvisorShardTraffic>;
827
+ /**
828
+ * `ctx.sql` tagged-template interpolations that splice an unparameterized
829
+ * string-building expression into the query — the `sql_injection_risk` input.
830
+ * Supplied by the codegen feeder; absent for runtime callers, where the lint
831
+ * finds nothing.
832
+ */
833
+ sqlInterpolations?: ReadonlyArray<AdvisorSqlInterpolation>;
834
+ /**
835
+ * Bounded row samples per table — the `constraint_validator` lint input.
836
+ * Supplied by the studio backend, which reads up to the configured row cap
837
+ * from each table via `readTablePage` and assembles the existing-id set for
838
+ * FK referential-integrity checks. Absent for static callers or codegen
839
+ * feeders, where the constraint lint simply finds nothing.
840
+ *
841
+ * Each entry carries `existingIds` (every `_id` in the sample window) so
842
+ * FK columns can be cross-checked across tables in O(1) per value. When
843
+ * `truncated` is `true`, violations on rows beyond the cap are not reported
844
+ * — the finding description notes the sample cap so the operator understands
845
+ * the bounded window.
846
+ */
847
+ tableSamples?: ReadonlyArray<AdvisorTableSample>;
848
+ /**
849
+ * Per-table full-scan volume observed at runtime (the hot-scan half of the
850
+ * `index_utilization` lint input). Sourced from the per-`(function, table)`
851
+ * full-scan attribution the runtime records (`__lunora_metrics_scans`,
852
+ * surfaced as `FunctionCallStat.scannedTables`), aggregated across functions
853
+ * and shards. Absent for static callers, where the lint finds nothing.
854
+ */
855
+ tableScans?: ReadonlyArray<AdvisorTableScan>;
856
+ /**
857
+ * `ctx.workflows.get("name")` call sites discovered in function bodies — the
858
+ * use-side input the `workflow_unused` and `workflow_unknown_target` lints
859
+ * cross-reference against {@link LintContext.workflows}. Supplied by the
860
+ * codegen feeder; absent for runtime callers, where the workflow lints find
861
+ * nothing.
862
+ */
863
+ workflowCalls?: ReadonlyArray<AdvisorWorkflowCall>;
864
+ /**
865
+ * Workflows declared via `defineWorkflow` exports in `lunora/workflows.ts` —
866
+ * the declaration-side input for the `workflow_*` lints. Supplied by the
867
+ * codegen feeder; absent for runtime callers, where the workflow lints find
868
+ * nothing.
869
+ */
870
+ workflows?: ReadonlyArray<AdvisorWorkflow>;
871
+ }
872
+ /**
873
+ * A single advisory rule. `run` is pure over its {@link LintContext} so lints are
874
+ * trivially testable and order-independent. Each rule owns the static metadata
875
+ * (`name`/`title`/…) that its findings inherit, keeping individual `Finding`
876
+ * construction to just the per-occurrence `detail`/`metadata`/`cacheKey`.
877
+ */
878
+ interface Lint {
879
+ /** Concern buckets every finding from this lint carries. */
880
+ categories: Category[];
881
+ /** General-purpose description shared by every finding. */
882
+ description: string;
883
+ /** Default audience for this lint's findings. */
884
+ facing: Facing;
885
+ /** Default severity for this lint's findings. */
886
+ level: Level;
887
+ /** Unique lint id, snake_case (e.g. `unindexed_foreign_key`). */
888
+ name: string;
889
+ /** Fix guidance shared by every finding. */
890
+ remediation: string;
891
+ /** Produce zero or more findings for the given context. */
892
+ run: (context: LintContext) => Finding[];
893
+ /** Evidence source — see {@link LintSource}. */
894
+ source: LintSource;
895
+ /** Short headline shared by every finding. */
896
+ title: string;
897
+ }
898
+ /**
899
+ * Minimal structural view of the `@lunora/bindings/analytics` SQL client — just its
900
+ * `query(sql)` method. Kept structural (not an `import type` from
901
+ * `@lunora/bindings/analytics`) so the advisor needn't depend on the analytics package;
902
+ * the real `AnalyticsSqlClient` satisfies it, as does a plain test double.
903
+ */
904
+ interface AnalyticsMetricsSource {
905
+ query: (sql: string) => Promise<{
906
+ rows: ReadonlyArray<Record<string, unknown>>;
907
+ }>;
908
+ }
909
+ /**
910
+ * The AE event-name + dimension-column contract the runtime writes and this
911
+ * reader reads. `blob1` is the event name; dimensions start at `blob2`.
912
+ */
913
+ declare const AE_METRIC_EVENTS: {
914
+ /** `lunora.index.hit` — one row per `(table, index)` use. `blob2`=table, `blob3`=index. */
915
+ readonly indexHit: {
916
+ readonly event: "lunora.index.hit";
917
+ readonly index: "blob3";
918
+ readonly table: "blob2";
919
+ };
920
+ /** `lunora.shard.request` — one row per shard dispatch. `blob2`=shardKey, `blob3`=group. */
921
+ readonly shardRequest: {
922
+ readonly event: "lunora.shard.request";
923
+ readonly group: "blob3";
924
+ readonly shardKey: "blob2";
925
+ };
926
+ /** `lunora.table.scan` — one row per full-scan. `blob2`=table. */
927
+ readonly tableScan: {
928
+ readonly event: "lunora.table.scan";
929
+ readonly table: "blob2";
930
+ };
931
+ };
932
+ /** Options for the AE-backed runtime-metrics feeder. */
933
+ interface AnalyticsMetricsOptions {
934
+ /** The AE dataset (the wrangler `analytics_engine_datasets[].dataset`) to read from. */
935
+ dataset: string;
936
+ /**
937
+ * Declared index names per table, used to synthesise the `reads: 0` rows the
938
+ * `index_utilization` dead-index half needs. AE only stores rows for indexes
939
+ * that were *used*, so a never-hit index has no AE row at all; supplying the
940
+ * declared set lets the reader emit an explicit `reads: 0` entry for any
941
+ * declared index absent from the AE hit feed. Omit it to report only the
942
+ * positive hit counts AE returns.
943
+ */
944
+ declaredIndexes?: ReadonlyArray<{
945
+ index: string;
946
+ table: string;
947
+ }>;
948
+ /**
949
+ * Restrict the shard-traffic read to one sharded-function group (`blob3`).
950
+ * Omit to read the whole deployment's shard set.
951
+ */
952
+ group?: string;
953
+ }
954
+ /** The runtime-lint input arrays this module reconstructs from AE. */
955
+ interface AnalyticsRuntimeMetrics {
956
+ indexHits: AdvisorIndexHit[];
957
+ shardTraffic: AdvisorShardTraffic[];
958
+ tableScans: AdvisorTableScan[];
959
+ }
960
+ /**
961
+ * Reconstruct the runtime-lint input arrays (`shardTraffic` / `tableScans` /
962
+ * `indexHits`) from the Analytics Engine SQL API. The three reads run
963
+ * concurrently; each degrades to an empty array on a query failure, so a
964
+ * partially-misconfigured read path still returns what it can.
965
+ *
966
+ * Feed the result into a {@link LintContext} alongside the declared schema:
967
+ *
968
+ * ```ts
969
+ * const metrics = await loadAnalyticsRuntimeMetrics(client, { dataset: "ANALYTICS" });
970
+ * runAdvisor({ schema, ...metrics }, { source: "runtime" });
971
+ * ```
972
+ */
973
+ declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
974
+ /**
975
+ * Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
976
+ * constraints by cross-checking sampled row data against the schema.
977
+ *
978
+ * This lint reads the `context.tableSamples` feed (bounded row samples supplied
979
+ * by the studio backend via `readTablePage`) and the declared schema. Three
980
+ * families of check run over each sample:
981
+ *
982
+ * FK referential integrity: for every `one` relation the holding table declares,
983
+ * check that each sampled row's FK column value appears in the target table's
984
+ * sampled id set. A dangling value means no target row exists for the reference.
985
+ *
986
+ * NOT NULL / non-optional columns: the lint surfaces rows with null/undefined in
987
+ * declared fields — inserted before a column was added or via raw import.
988
+ *
989
+ * UNIQUE index violations: for each declared unique secondary index, check the
990
+ * sampled rows for duplicate values across the index's columns.
991
+ *
992
+ * All checks are bounded by the cap in each sample; the lint never triggers an
993
+ * additional read. When a sample is truncated, findings note the caveat.
994
+ */
995
+ declare const constraintValidator: Lint;
996
+ /**
997
+ * `hot_shard` — flag a shard whose request share is disproportionately high.
998
+ *
999
+ * Sharding (`.shardBy(key)`) spreads state and load across many Durable Objects
1000
+ * by user / tenant / room. Its whole value is *even* distribution: when one
1001
+ * shard absorbs a dominant fraction of traffic, that single DO becomes the
1002
+ * bottleneck (one request stream, one SQLite, one WS fan-out) while its siblings
1003
+ * idle — the hot-key skew sharding is meant to avoid. That usually means the
1004
+ * shard key has too little cardinality, or one entity is unusually busy and
1005
+ * needs its own split.
1006
+ *
1007
+ * The per-shard request volume comes from the runtime feeder
1008
+ * (`context.shardTraffic`): the studio backend fans out over the function's
1009
+ * shards and reads each shard's recorded `__lunora_metrics` call total. The lint
1010
+ * is pure over that distribution, so it only fires once the window has more than
1011
+ * one shard and enough total requests (`MIN_TOTAL_REQUESTS`) for the proportion
1012
+ * to be trustworthy.
1013
+ */
1014
+ declare const hotShard: Lint;
1015
+ /**
1016
+ * `index_utilization` — flag indexes the workload doesn't pay for. Two
1017
+ * complementary checks over recorded reads.
1018
+ *
1019
+ * Dead index — a declared index that recorded reads never used. An unused index
1020
+ * is pure overhead: every write maintains it, every byte of storage holds it,
1021
+ * and nothing reads through it. Fired off the per-index hit feed
1022
+ * (`context.indexHits`); a declared index whose recorded `reads` is `0` is dead.
1023
+ * The runtime records this in the durable `__lunora_metrics_index` table (every
1024
+ * index use stamps a per-`(table, index)` counter via `onIndexUse`) and surfaces
1025
+ * it through the `getMetrics` admin RPC; the studio sums the per-shard arrays
1026
+ * into `context.indexHits` (see `AdvisorIndexHit`). The counter is cumulative and
1027
+ * never decays, so a non-zero index never reverts to "dead" — `reads: 0` means
1028
+ * the index has not been used once since the counter was created.
1029
+ *
1030
+ * Hot unindexed scan — a table read hot with no index at all. Fired off the
1031
+ * full-scan attribution the runtime does record (`context.tableScans`, sourced
1032
+ * from `__lunora_metrics_scans` / `FunctionCallStat.scannedTables`): a table
1033
+ * whose scan count clears `HOT_SCAN_THRESHOLD` is one the app keeps
1034
+ * full-scanning, the runtime-confirmed counterpart to the static
1035
+ * `filter_without_index` advisory.
1036
+ */
1037
+ declare const indexUtilization: Lint;
1038
+ /**
1039
+ * Flags an `httpRoute` on an admin/privileged-looking path whose handler shows no
1040
+ * auth/admin guard.
1041
+ *
1042
+ * REST routes (unlike queries/mutations) aren't covered by RLS — they run whatever
1043
+ * the handler does, so an `/admin/*` (or `/internal/*`, `/_*`) route with no
1044
+ * session/admin check is an open privilege door: anyone who can reach the URL can
1045
+ * invoke it. The handler must assert an authenticated, authorized caller
1046
+ * (`ctx.auth` / `getSession` / a `requireAdmin`-style guard) before doing
1047
+ * privileged work.
1048
+ *
1049
+ * Detection is heuristic: the feeder records whether the handler body references
1050
+ * any known guard token. Runs only when the codegen feeder supplies route evidence
1051
+ * (`context.adminRoutes`); a runtime caller flags nothing.
1052
+ */
1053
+ declare const adminRouteWithoutGuard: Lint;
1054
+ /**
1055
+ * Flags a `ctx.authApi.&lt;method>(...)` call whose argument object omits `headers`.
1056
+ *
1057
+ * `@lunora/auth`'s `withAuthPlugins` middleware attaches the full privileged
1058
+ * better-auth API to `ctx.authApi` — `banUser`, `setRole`, impersonation,
1059
+ * `createOrganization`, `removeMember`, etc. better-auth authorizes these calls
1060
+ * from the caller's session carried in the `headers` you pass. Called
1061
+ * **without** `headers`, better-auth treats the invocation as a trusted
1062
+ * server-to-server call and **skips session authorization entirely**. So a
1063
+ * header-less `ctx.authApi.banUser({ body })` runs with full privileges
1064
+ * regardless of who the caller is — an authorization bypass.
1065
+ *
1066
+ * This lint runs when the codegen feeder has supplied call evidence
1067
+ * (`context.authApiCalls` present); a runtime caller with no evidence flags
1068
+ * nothing rather than raising false alarms.
1069
+ */
1070
+ declare const authApiCallWithoutHeaders: Lint;
1071
+ /**
1072
+ * Detect FK cycles in the declared relation graph via a DFS.
1073
+ *
1074
+ * A "circular FK" exists when a chain of `one` relations forms a loop — for
1075
+ * example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
1076
+ * cause unexpected behavior during DELETE operations: a CASCADE chain may loop
1077
+ * forever (or deadlock), and even a RESTRICT cycle prevents deletion of any row
1078
+ * in the loop without temporarily disabling constraints.
1079
+ *
1080
+ * Only `one` relations are followed because they are the side that owns the FK
1081
+ * column (the `field` lives on the holding table). `many` relations point back
1082
+ * to the same edge from the opposite side and would cause every edge to be
1083
+ * double-counted; skipping them gives the correct directed graph.
1084
+ *
1085
+ * A single-table self-reference (`A.parentId → A`) is **not** reported: a
1086
+ * self-referential FK is the canonical, intentional shape for trees/hierarchies
1087
+ * (categories, org charts, threaded comments), so flagging every such schema
1088
+ * would be noise. Only multi-table cycles — the ones the description illustrates
1089
+ * — are surfaced.
1090
+ *
1091
+ * Each unique cycle is reported once: the cycle path is canonicalized to its
1092
+ * lexicographically smallest rotation so two DFS traversals that enter the same
1093
+ * ring at different nodes emit the same cacheKey and detail. A representative
1094
+ * cycle is reported for each distinct simple cycle in the graph; overlapping or
1095
+ * chord cycles that share interior nodes are each detected independently.
1096
+ */
1097
+ declare const circularFk: Lint;
1098
+ /**
1099
+ * Flags a container declared on a large instance type. The big `standard-3` /
1100
+ * `standard-4` sizes (and large custom shapes) are billed on their provisioned
1101
+ * memory + disk for the whole time an instance runs, so an over-provisioned
1102
+ * container is a standing cost. Informational — a real workload may need it —
1103
+ * but worth surfacing so the choice is deliberate.
1104
+ */
1105
+ declare const containerOversizedInstance: Lint;
1106
+ /**
1107
+ * Flags a container that leaves outbound internet access at the platform
1108
+ * default (`enableInternet: true`). Egress is billed per GB and an open
1109
+ * outbound path widens the attack surface, so a container that doesn't call
1110
+ * external services should set `enableInternet: false` explicitly. We can't
1111
+ * tell from config whether egress is actually used, so this is an INFO nudge to
1112
+ * make the choice deliberate, not an error.
1113
+ *
1114
+ * Only fires when the field was omitted (or a non-literal we couldn't read) —
1115
+ * an explicit `enableInternet: true` is treated as a deliberate opt-in and left
1116
+ * alone.
1117
+ */
1118
+ declare const containerPublicInternet: Lint;
1119
+ /**
1120
+ * Lunora port of splinter's `0009_duplicate_index`.
1121
+ *
1122
+ * A btree secondary index is redundant when another index already serves every
1123
+ * lookup it does — i.e. its columns are a leading prefix of the other's
1124
+ * (SQLite's leftmost-prefix rule means `["a", "b"]` already covers `["a"]`).
1125
+ * Exact duplicates are the degenerate case. A redundant index is pure overhead:
1126
+ * extra storage and a write amplified on every insert/update/delete.
1127
+ *
1128
+ * Only `kind: "index"` participates — search/rank/vector indexes are distinct
1129
+ * structures, never redundant with a btree. A `unique` index is never reported
1130
+ * as redundant even when its columns are a prefix: it enforces a constraint the
1131
+ * covering index does not, so dropping it would change behavior.
1132
+ */
1133
+ declare const duplicateIndex: Lint;
1134
+ /**
1135
+ * Flags a secondary index declared with no columns (`.index("x", [])`). The
1136
+ * `.index(name, fields)` builder types `fields` as `string[]`, not
1137
+ * `keyof Shape[]`, so an empty array slips past the compiler — but an index over
1138
+ * zero columns indexes nothing and can never narrow a read. Almost always a
1139
+ * leftover from a refactor. (Search / rank / vector indexes always carry at
1140
+ * least one field by construction, so only `kind: "index"` is checked.)
1141
+ */
1142
+ declare const emptyIndex: Lint;
1143
+ /**
1144
+ * Flags a table that is both `.source(...)` and `.global()`.
1145
+ *
1146
+ * The two are contradictory. `.global()` already places a table in an external
1147
+ * store (D1, or a Hyperdrive-fronted Postgres/MySQL) that Lunora owns the schema
1148
+ * for and reads through the global backend. `.source(...)` declares the table as
1149
+ * **materialized from** an external database into a shard DO's SQLite by the
1150
+ * ingest poll loop. A table cannot simultaneously live in the global tier and be
1151
+ * polled into per-shard SQLite — the ingest loop has no DO-local table to write,
1152
+ * and the global backend has no poll loop. This is a definite misconfiguration,
1153
+ * so it is an `ERROR`.
1154
+ *
1155
+ * **Evidence supply**: reads `table.externalSource` + `table.shardKind`. Skipped
1156
+ * unless both a sourced declaration and the `global` tier are present.
1157
+ */
1158
+ declare const externalSourceOnGlobal: Lint;
1159
+ /**
1160
+ * Flags a `.source(...)` + `.shardBy(...)` table that has no `tenantBy` mapper.
1161
+ *
1162
+ * Per-shard SQLite isolation only controls *where* materialized rows land — not
1163
+ * what* the ingest query pulls. A sourced + sharded table whose `tenantBy` is
1164
+ * absent runs the same unscoped membership query on every tenant's Durable
1165
+ * Object, so each agent replicates the **entire** multitenant table into its own
1166
+ * SQLite (and then to its clients via `defineShape`). That is a cross-tenant data
1167
+ * leak, not a performance nit — so it is an `ERROR` that fails the build.
1168
+ *
1169
+ * `tenantBy(shardKey)` is the boundary: it binds this DO's shard key into the
1170
+ * query's parameters so the tenant can only ever pull its own rows.
1171
+ *
1172
+ * **Evidence supply**: reads `table.externalSource` (the codegen feeder captures
1173
+ * it from `.source({...})`; the runtime feeder derives it). A table without a
1174
+ * sourced declaration, or one not sharded, is skipped.
1175
+ */
1176
+ declare const externalSourceUnscoped: Lint;
1177
+ /**
1178
+ * Flags a query read that calls `.filter()` without first narrowing with
1179
+ * `.withIndex()` / `.withSearchIndex()`. Such a read loads *every* row of the
1180
+ * table and applies the predicate in memory — a full table scan that degrades
1181
+ * linearly as the table grows. The healthy pattern is `.withIndex(...)` to
1182
+ * narrow, then `.filter(...)` only for predicates the index can't express
1183
+ * (which is why an indexed read with a trailing `.filter()` is NOT flagged).
1184
+ *
1185
+ * The query reads come from the codegen feeder, which parses
1186
+ * `ctx.db.query("table")…` chains out of function bodies. Runtime callers supply
1187
+ * no `queries`, so this lint is a no-op there.
1188
+ */
1189
+ declare const filterWithoutIndex: Lint;
1190
+ /**
1191
+ * Flags a secret-shaped string literal checked into the lunora source.
1192
+ *
1193
+ * A live API key, access key, private key, or high-entropy token committed to the
1194
+ * codebase leaks the moment the repo is cloned, forked, or its history is read —
1195
+ * and rotating it means a redeploy. Secrets belong in `.dev.vars` locally and
1196
+ * `wrangler secret put` in production, read at runtime via `env`. This lint
1197
+ * surfaces the same class of finding the pre-commit `vis secrets` gate catches,
1198
+ * inside the studio Advisors table.
1199
+ *
1200
+ * Runs only when the codegen feeder supplies secret evidence
1201
+ * (`context.secretLiterals`); a runtime caller flags nothing. One finding per
1202
+ * literal.
1203
+ */
1204
+ declare const hardcodedSecret: Lint;
1205
+ /**
1206
+ * Flags a Hyperdrive `ctx.sql` access inside a `query(...)` or `mutation(...)`
1207
+ * handler body.
1208
+ *
1209
+ * Hyperdrive (`@lunora/hyperdrive`) points at an **external** Postgres/MySQL
1210
+ * database Lunora does not own. A `ctx.sql` query is a network round-trip with a
1211
+ * mutable result — non-deterministic, exactly like `fetch` — so it breaks the
1212
+ * determinism the coordinator relies on when it re-runs a query on subscription
1213
+ * re-evaluation or a mutation on OCC retry. Worse, external writes are invisible
1214
+ * to the DO/SQLite change-feed, so a subscription will never re-fire on them.
1215
+ * `ctx.sql` is therefore wired onto `ActionCtx` **only** and belongs exclusively
1216
+ * in `action(...)` handlers; using it in a query/mutation is the same class of
1217
+ * bug as `fetch`/`Date.now`.
1218
+ *
1219
+ * This is the enforcement teeth behind the action-only rule — runtime
1220
+ * enforcement is still absent (see `MEMORY.md` "Query/mutation determinism not
1221
+ * enforced"), so the lint is the guardrail.
1222
+ *
1223
+ * This lint runs when the codegen feeder has supplied access evidence
1224
+ * (`context.hyperdriveCalls` present); a runtime caller with no evidence flags
1225
+ * nothing rather than raising false alarms. The feeder records accesses only
1226
+ * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1227
+ */
1228
+ declare const hyperdriveOutsideAction: Lint;
1229
+ /**
1230
+ * A correctness lint with no splinter analogue — it exploits Lunora's static
1231
+ * edge: the schema is fully declared, so a typo'd index column is catchable at
1232
+ * codegen time rather than surfacing as a runtime error or a silently
1233
+ * never-matching index.
1234
+ *
1235
+ * Every index (secondary / search / rank / vector) names the columns it covers;
1236
+ * each must be a declared column of the table (or a system field). A reference
1237
+ * to an unknown column is almost always a typo or a column that was renamed
1238
+ * without updating the index.
1239
+ */
1240
+ declare const indexReferencesUnknownField: Lint;
1241
+ /**
1242
+ * Flags a public procedure that reads a table for which at least one other
1243
+ * procedure declares a column mask (evidence the developer decided that table
1244
+ * carries sensitive columns), but whose own builder chain does NOT include
1245
+ * `.use(mask(...))`.
1246
+ *
1247
+ * Lunora masking is **opt-in per procedure**: a `mask(policies)` object only
1248
+ * redacts columns inside procedures whose builder chain includes
1249
+ * `.use(mask(policies))`. A procedure without it returns the raw column value —
1250
+ * even when another procedure in the same app declares that column maskable.
1251
+ * This is the "one procedure masks `users.email`, another leaks it" failure
1252
+ * mode, the column-level sibling of `rls_uncovered_table`.
1253
+ *
1254
+ * **Granularity**: the lint is table-granular, not column-precise. Statically
1255
+ * proving that a procedure *returns* a specific masked column would need
1256
+ * return-shape analysis that is infeasible over the IR; instead the lint flags a
1257
+ * public procedure that *reads* a mask-covered table without any
1258
+ * `.use(mask(...))` of its own, and the finding lists the masked columns the
1259
+ * developer flagged elsewhere. Only reads are considered — masking is a
1260
+ * read/return-path concern, so writes never trigger it.
1261
+ *
1262
+ * **Scope**: only `public` procedures are flagged. `internal*` procedures
1263
+ * (e.g. `internalQuery`, `internalMutation`) intentionally bypass masking, so
1264
+ * flagging them would produce only noise. Remediation text notes this exemption.
1265
+ *
1266
+ * **Evidence supply**: this lint runs only when the codegen feeder has supplied
1267
+ * `context.maskProcedures`; a runtime caller with no evidence flags nothing
1268
+ * rather than raising false alarms.
1269
+ *
1270
+ * **Conservative policy detection**: when a procedure calls `mask(policies)`
1271
+ * with a non-literal object (a variable reference), the feeder cannot statically
1272
+ * enumerate the masked `(table, column)` pairs. In that case the procedure is
1273
+ * still marked `usesMask: true` (so it is NOT itself flagged), but its pairs
1274
+ * contribute nothing to the masked-column map. The lint may under-report (false
1275
+ * negatives) when policies are extracted into named constants, but never
1276
+ * over-reports (no false positives).
1277
+ */
1278
+ declare const maskUncoveredPiiColumn: Lint;
1279
+ /**
1280
+ * Flags a custom mutator whose authoritative `server` impl writes a row with
1281
+ * `ctx.db.replace(id, document)` — a whole-document overwrite.
1282
+ *
1283
+ * The local-first sync engine serializes mutators in the shard DO, so two
1284
+ * mutators that touch the *same row* but *different columns* both run to
1285
+ * completion — but only if each writes its own column. A `replace` overwrites
1286
+ * the entire row from the document the mutator assembled, so a concurrent edit
1287
+ * to another column (committed between this mutator's read and its write, or
1288
+ * carried as a pending optimistic overlay on a client) is silently clobbered:
1289
+ * the kind of "two offline edits to different fields fight each other" data loss
1290
+ * a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
1291
+ * merges at the column level instead, so independent field edits coexist.
1292
+ *
1293
+ * `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
1294
+ * the whole row (a full-form save, a state-machine transition that rewrites
1295
+ * every field). The lint just surfaces the column-clobber risk so a developer
1296
+ * reaches for `patch` by default on a synced table.
1297
+ *
1298
+ * **Evidence supply**: runs only when the codegen feeder supplies
1299
+ * `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
1300
+ * `server` body); absent for runtime callers, where the lint finds nothing.
1301
+ */
1302
+ declare const mutatorFullRowReplace: Lint;
1303
+ /**
1304
+ * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
1305
+ * handler body.
1306
+ *
1307
+ * Lunora queries and mutations must be **deterministic**: the coordinator may
1308
+ * re-run a mutation on optimistic-concurrency (OCC) retry and a query on
1309
+ * subscription re-evaluation, so a handler that reads wall-clock time, draws
1310
+ * randomness, or hits the network can produce different results on each run —
1311
+ * breaking read-your-writes, cache invalidation, and replayable history.
1312
+ * `Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and
1313
+ * `fetch` are therefore disallowed in query/mutation handlers and belong in an
1314
+ * `action(...)`, which runs exactly once and may use ambient/non-deterministic
1315
+ * APIs freely (pass the result into a mutation as an argument).
1316
+ *
1317
+ * This lint runs when the codegen feeder has supplied call evidence
1318
+ * (`context.nondeterministicCalls` present); a runtime caller with no evidence
1319
+ * flags nothing rather than raising false alarms. The feeder records calls only
1320
+ * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1321
+ */
1322
+ declare const nondeterministicQueryMutation: Lint;
1323
+ /**
1324
+ * Flags an RLS policy whose `table` names a table that does not exist in the
1325
+ * schema.
1326
+ *
1327
+ * A policy is bound to a table by a plain string (`definePolicy({ table:
1328
+ * "documents", … })`). The `rls()` middleware only applies a policy to reads and
1329
+ * writes of that exact table name — so a typo, a stale name after a rename, or a
1330
+ * copy-paste mistake produces a policy that silently matches **nothing**. The
1331
+ * table the developer believes is gated is left completely ungated, which is a
1332
+ * security gap, not a mere dead-code wart: every read of the real table returns
1333
+ * unrestricted rows and every write is allowed.
1334
+ *
1335
+ * This is strictly worse than `rls_uncovered_table` (a procedure forgetting the
1336
+ * middleware): here the middleware *is* wired up, the policy *is* in the list,
1337
+ * and it still does nothing — the failure is invisible at every call site.
1338
+ *
1339
+ * **Evidence supply**: like `rls_uncovered_table`, this runs only when the
1340
+ * codegen feeder supplies `context.rlsProcedures`. The covered-table names come
1341
+ * from each procedure's statically-read `rls(policies)` array (`rlsTables`); a
1342
+ * policies argument that isn't a literal array contributes no names, so the lint
1343
+ * under-reports rather than raising false alarms.
1344
+ */
1345
+ declare const policyReferencesUnknownTable: Lint;
1346
+ /**
1347
+ * Flags a `v.any()` argument on a public procedure.
1348
+ *
1349
+ * `v.any()` disables validation: the field accepts arbitrary, untyped,
1350
+ * arbitrarily-large input straight from an untrusted client. That defeats the
1351
+ * end-to-end type safety Lunora exists to provide and opens the door to injection,
1352
+ * prototype pollution, and oversized-payload abuse. Public input should be a
1353
+ * precise validator (`v.object`, `v.string`, `v.union`, …).
1354
+ *
1355
+ * Runs only when the codegen feeder supplies arg evidence
1356
+ * (`context.argValidators`, public procedures only); a runtime caller flags
1357
+ * nothing. One finding per offending arg.
1358
+ */
1359
+ declare const publicArgumentUsesAny: Lint;
1360
+ /**
1361
+ * Flags a public `mutation`/`action` whose builder chain installs no rate limit.
1362
+ *
1363
+ * Every publicly-callable write is a flood target: without a `rateLimit`
1364
+ * middleware a single client can hammer it to exhaust D1 writes, send-mail quota,
1365
+ * or paid credits, and brute-force auth-shaped endpoints (login / reset / OTP).
1366
+ * Lunora ships `rateLimit()` (`@lunora/ratelimit`) and the `protectPublic({...})`
1367
+ * bundle for exactly this; this lint fires when neither is present on a public
1368
+ * write.
1369
+ *
1370
+ * Runs only when the codegen feeder supplies protection evidence
1371
+ * (`context.procedureProtections`); a runtime caller with no evidence flags
1372
+ * nothing. `query` is read-only and excluded; internal functions are
1373
+ * server-called and excluded.
1374
+ */
1375
+ declare const publicMutationWithoutRatelimit: Lint;
1376
+ /**
1377
+ * Flags an R2 SQL `ctx.r2sql` access inside a `query(...)` or `mutation(...)`
1378
+ * handler body.
1379
+ *
1380
+ * R2 SQL (`@lunora/bindings/r2sql`) queries Apache Iceberg tables over an **external**
1381
+ * REST endpoint Lunora does not own — there is no Workers binding, every query
1382
+ * is an HTTPS round-trip. A `ctx.r2sql` query is therefore non-deterministic
1383
+ * (exactly like `fetch`), which breaks the determinism the coordinator relies on
1384
+ * when it re-runs a query on subscription re-evaluation or a mutation on OCC
1385
+ * retry. And R2 SQL reads are invisible to the DO/SQLite change-feed, so a
1386
+ * subscription will never re-fire on them. `ctx.r2sql` is therefore wired onto
1387
+ * `ActionCtx` **only** and belongs exclusively in `action(...)` handlers; using
1388
+ * it in a query/mutation is the same class of bug as `fetch`/`Date.now`.
1389
+ *
1390
+ * This mirrors `hyperdrive_outside_action` — the action-only enforcement teeth
1391
+ * for external, non-reactive I/O. Runtime enforcement is still absent (see
1392
+ * `MEMORY.md` "Query/mutation determinism not enforced"), so the lint is the
1393
+ * guardrail.
1394
+ *
1395
+ * This lint runs when the codegen feeder has supplied access evidence
1396
+ * (`context.r2sqlCalls` present); a runtime caller with no evidence flags nothing
1397
+ * rather than raising false alarms. The feeder records accesses only inside
1398
+ * `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1399
+ */
1400
+ declare const r2sqlOutsideAction: Lint;
1401
+ /**
1402
+ * A correctness lint covering the columns a relation wires together: the FK
1403
+ * `field` and the `references` column must each exist on their respective
1404
+ * tables, or the join can never resolve. Caught here at codegen time rather
1405
+ * than as a runtime failure.
1406
+ */
1407
+ declare const relationReferencesUnknownField: Lint;
1408
+ /**
1409
+ * A correctness lint: every relation declared via `.relations((r) => …)` names a
1410
+ * target table, which must exist in the schema. A target that resolves to no
1411
+ * table is a typo or a reference to a table that was removed/renamed — the
1412
+ * relation can never load. Caught here at codegen time rather than at runtime.
1413
+ *
1414
+ * (Extension tables are already namespaced and their relation targets rewritten
1415
+ * by the time the schema reaches a lint, so a surviving unknown target is a real
1416
+ * miss, not an unresolved cross-package reference.)
1417
+ */
1418
+ declare const relationReferencesUnknownTable: Lint;
1419
+ /**
1420
+ * Flags a public procedure that reads or writes a table named in at least one
1421
+ * other procedure's `rls(policies)` list, but whose own builder chain does NOT
1422
+ * include `.use(rls(...))`.
1423
+ *
1424
+ * Lunora RLS is **opt-in per procedure**: a policy list only takes effect
1425
+ * inside procedures whose builder chain includes `.use(rls(policies))`. A
1426
+ * procedure without it sees the raw, unwrapped `ctx.db` and silently bypasses
1427
+ * every policy in the list — even when another procedure in the same app
1428
+ * declares that table as policy-gated.
1429
+ *
1430
+ * The lint surfaces the most dangerous subclass of this failure mode: a table
1431
+ * that the developer explicitly decided to gate with RLS (evidenced by naming
1432
+ * it in at least one procedure's policy list) is nonetheless accessible
1433
+ * without restriction from a procedure that forgot the `.use(rls(...))` call.
1434
+ *
1435
+ * **Scope**: only `public` procedures are flagged. `internal*` procedures
1436
+ * (e.g. `internalQuery`, `internalMutation`) are intentional server-side
1437
+ * helpers that legitimately bypass the user-facing RLS gate, so flagging them
1438
+ * would produce only noise. Remediation text notes this exemption so authors
1439
+ * know to use `internalQuery`/`internalMutation`/`internalAction` when they
1440
+ * truly need unwrapped access.
1441
+ *
1442
+ * **Evidence supply**: this lint runs only when the codegen feeder has supplied
1443
+ * `context.rlsProcedures`; a runtime caller with no evidence flags nothing
1444
+ * rather than raising false alarms.
1445
+ *
1446
+ * **Conservative policy-table detection**: when a procedure calls
1447
+ * `rls(policies)` with a non-literal array (a variable reference), the feeder
1448
+ * cannot statically enumerate the covered tables. In that case the procedure is
1449
+ * still marked `usesRls: true` (so it is NOT itself flagged), but its tables
1450
+ * contribute nothing to `policyCoveredTables`. This means the lint may
1451
+ * under-report (false negatives) when policies are extracted into named
1452
+ * constants, but it never over-reports (no false positives).
1453
+ */
1454
+ declare const rlsUncoveredTable: Lint;
1455
+ /**
1456
+ * Flags a replication shape whose `table` is a `.global()` table.
1457
+ *
1458
+ * Poke-live replication is a per-shard-DO property: the shard owns its SQLite
1459
+ * and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
1460
+ * every subscriber at the next flush. A `.global()` table lives outside the
1461
+ * shard DO's SQLite op-log (in a global backend — D1, or Hyperdrive-fronted
1462
+ * Postgres/MySQL) — so a shape over a global table cannot be poke-live. It is
1463
+ * served through the cross-shard tier: **coordinator/poll-refreshed, latency-
1464
+ * tiered**, not live. That is a real and supported tier (it is the recommended
1465
+ * answer for cross-shard reads — denormalize, or move the joined table to
1466
+ * `.global()` and read through the global backend), but its freshness semantics
1467
+ * differ from a sharded shape's, so the boundary is surfaced rather than hidden.
1468
+ *
1469
+ * `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
1470
+ * accept the poll-refresh latency; the lint just makes the tier explicit so a
1471
+ * developer does not assume poke-live freshness.
1472
+ *
1473
+ * **Evidence supply**: runs only when the codegen feeder supplies
1474
+ * `context.shapes`; the table's tier comes from the schema's `shardKind`. A
1475
+ * shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
1476
+ * the feeder didn't supply is skipped.
1477
+ */
1478
+ declare const shapeTargetsGlobalTable: Lint;
1479
+ /**
1480
+ * Flags a replication shape whose `table` names a table that does not exist in
1481
+ * the schema.
1482
+ *
1483
+ * `defineShape({ table: "messages", … })` binds a shape to a table by a plain
1484
+ * string. A live `subscribeShape("…")` resolves that shape server-side and runs
1485
+ * its membership query against the named table — so a typo, a stale name after a
1486
+ * rename, or a copy-paste mistake produces a shape that can never resolve a
1487
+ * rowset: the subscription seeds empty and then errors at the first flush
1488
+ * (`no such table`). This is a definite, build-time-detectable break, so it is
1489
+ * an `ERROR` — surfaced before the broken shape ever ships.
1490
+ *
1491
+ * **Evidence supply**: runs only when the codegen feeder supplies
1492
+ * `context.shapes`. A shape whose `table` wasn't a static string literal (no
1493
+ * resolvable name) is skipped rather than guessed at, so the lint under-reports
1494
+ * rather than raising false alarms.
1495
+ */
1496
+ declare const shapeUnknownTable: Lint;
1497
+ /**
1498
+ * Flags a `ctx.sql` tagged-template that splices an unparameterized
1499
+ * string-building expression into the query.
1500
+ *
1501
+ * The Hyperdrive `ctx.sql\`…\`` driver binds each `${value}` placeholder as a
1502
+ * query parameter — safe by construction. But a placeholder that *builds* a string
1503
+ * in place (`ctx.sql\`… ${"WHERE name='" + name + "'"}\``, or a nested template
1504
+ * literal) splices raw, attacker-controlled text into the SQL, reopening classic
1505
+ * SQL injection. The fix is always to pass the value through a placeholder so the
1506
+ * driver parameterizes it.
1507
+ *
1508
+ * Runs only when the codegen feeder supplies interpolation evidence
1509
+ * (`context.sqlInterpolations`); a runtime caller flags nothing. One finding per
1510
+ * interpolation.
1511
+ */
1512
+ declare const sqlInjectionRisk: Lint;
1513
+ /**
1514
+ * Flags a declared table that no function inserts into.
1515
+ *
1516
+ * Using `@lunora/codegen`'s write-side discovery (the analog of the read
1517
+ * discovery that feeds `filter_without_index`), this lint cross-references every
1518
+ * schema table against the set of tables some exported function writes via
1519
+ * `ctx.db.insert("&lt;table>", …)`. A table with no such write either is dead schema
1520
+ * or is populated through a path the static analysis can't see — a migration/seed,
1521
+ * cross-region replication, the `ctx.orm.insert(...)` builder, or a trusted
1522
+ * snapshot import. Hence `INFO`/`INTERNAL`: a nudge to confirm intent, not an error.
1523
+ *
1524
+ * A table declared `.externallyManaged()` is skipped — that flag is the explicit
1525
+ * acknowledgement that its rows are written outside Lunora (an adapter/migration/
1526
+ * middleware), so `@lunora/auth`'s better-auth tables and `@lunora/ratelimit`'s
1527
+ * store never flag here.
1528
+ *
1529
+ * Only runs when the write feeder supplied evidence (`context.inserts` present);
1530
+ * a runtime caller with no insert signal flags nothing rather than every table.
1531
+ */
1532
+ declare const tableWithoutInsert: Lint;
1533
+ /**
1534
+ * Flags a public `v.string()` argument with no length bound.
1535
+ *
1536
+ * A string field that accepts an unbounded value lets a client send megabytes of
1537
+ * text per request — inflating storage, blowing the row/document size budget, and
1538
+ * driving CPU/memory on every handler that processes it. A `.check()`/`.meta()`
1539
+ * max-length bound caps the blast radius. Advisory (INFO): a deliberately-open
1540
+ * free-text field is sometimes legitimate, so this nudges rather than blocks.
1541
+ *
1542
+ * Runs only when the codegen feeder supplies arg evidence
1543
+ * (`context.argValidators`, public procedures only); a runtime caller flags
1544
+ * nothing. One finding per offending arg.
1545
+ */
1546
+ declare const unboundedStringArgument: Lint;
1547
+ /**
1548
+ * Lunora port of splinter's `0001_unindexed_foreign_keys`.
1549
+ *
1550
+ * A `one` (many-to-one) relation declares a foreign-key column (`relation.field`)
1551
+ * on the holder table pointing at the target's `references` column. If no index
1552
+ * leads with that column, every read that filters or joins on the FK degrades to
1553
+ * a full table scan — the canonical silent performance cliff as a table grows.
1554
+ *
1555
+ * Coverage follows SQLite's leftmost-prefix rule: a composite index
1556
+ * `["authorId", "createdAt"]` covers lookups on `authorId`, so the FK is
1557
+ * satisfied when it is the *leading* column of any declared index. `many`
1558
+ * relations are skipped here — their FK lives on the opposite table and is
1559
+ * caught when that table's own `one` side is audited.
1560
+ */
1561
+ declare const unindexedForeignKey: Lint;
1562
+ /**
1563
+ * The to-many counterpart of `unindexed_foreign_key`.
1564
+ *
1565
+ * A `many` relation declares its foreign-key column (`relation.field`) on the
1566
+ * target table — `users.posts = r.many("posts", { field: "authorId" })` puts
1567
+ * `authorId` on `posts`. A relation predicate over that relation (`{ posts: {
1568
+ * some|none|every: W } }` in a `where`/RLS policy) and a `with:` child load both
1569
+ * resolve by querying the target table on that FK column, so an unindexed FK
1570
+ * there is the same silent full-scan cliff `unindexed_foreign_key` warns about —
1571
+ * just on the other side of the relation.
1572
+ *
1573
+ * `unindexed_foreign_key` only audits a table's own `one` relations, so it
1574
+ * catches this column **only when the target table declares the inverse `one`
1575
+ * relation** (`posts.author = r.one("users", { field: "authorId" })`). A
1576
+ * one-directional `many` (declared on the parent, with no inverse `one` on the
1577
+ * child) slips through — that exact gap is this lint's job. To stay strictly
1578
+ * complementary it skips any FK the target already covers via its own `one`
1579
+ * relation (reported there) and only fires on the otherwise-unaudited column.
1580
+ */
1581
+ declare const unindexedRelationTarget: Lint;
1582
+ /**
1583
+ * Flags a public `mutation`/`action` that creates a user/session or sends mail but
1584
+ * installs no CAPTCHA / bot check.
1585
+ *
1586
+ * Endpoints that mint accounts or trigger emails are the classic automated-abuse
1587
+ * surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
1588
+ * and disposable-account farming. A server-verified human check (Turnstile) in
1589
+ * front of them is the defense. Lunora ships `verifyTurnstile()` (`@lunora/auth`)
1590
+ * and the `protectPublic({ captcha })` bundle; this lint fires when a public
1591
+ * procedure writes a user/session/account-shaped table (or references `ctx.mail`)
1592
+ * with no captcha middleware.
1593
+ *
1594
+ * Runs only when the codegen feeder supplies protection evidence
1595
+ * (`context.procedureProtections`); a runtime caller with no evidence flags
1596
+ * nothing.
1597
+ */
1598
+ declare const userCreatingMutationWithoutCaptcha: Lint;
1599
+ /**
1600
+ * Flags a durable step name reused within one workflow.
1601
+ *
1602
+ * Cloudflare Workflows memoizes every `step.do` / `step.sleep` / `step.sleepUntil`
1603
+ * / `step.waitForEvent` call by its name: on replay the runtime returns the cached
1604
+ * result for a name it has already seen. Two distinct steps that share a name are
1605
+ * therefore a silent bug — the second call never runs its body and instead yields
1606
+ * the first's result, skipping the work (a charge, a write, an external wait)
1607
+ * without error. Hence `ERROR`/`INTERNAL`: it is a developer-facing correctness
1608
+ * defect in the workflow's own code, not a runtime-data nit.
1609
+ *
1610
+ * Only the first string-literal argument of each step call is compared; a step
1611
+ * named dynamically (`step.do(\`load-${id}\`, …)`) is omitted by the feeder, so a
1612
+ * deliberately-parameterized fan-out is never flagged. `ctx.runStep(stepDef, …)`
1613
+ * names (which come from `defineStep` in another file) are out of scope here.
1614
+ * Only runs when the declaration feeder supplied step evidence
1615
+ * (`workflow.steps` present); a runtime caller flags nothing.
1616
+ */
1617
+ declare const workflowDuplicateStepName: Lint;
1618
+ /**
1619
+ * A correctness lint: every `ctx.workflows.get("name")` call must reference a
1620
+ * workflow that exists — i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
1621
+ * A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
1622
+ * reference to a workflow that was removed/renamed; codegen wires the typed
1623
+ * `ctx.workflows` accessor off the declared set, so the call throws at runtime.
1624
+ * Caught here at codegen time instead.
1625
+ *
1626
+ * Calls with a non-literal name (`workflow === ""`) are skipped — they can't be
1627
+ * statically resolved, so they're neither confirmed-unknown here nor counted as
1628
+ * a typo. Only runs when both feeders supplied evidence (declared workflows and
1629
+ * discovered calls); a runtime caller flags nothing.
1630
+ */
1631
+ declare const workflowUnknownTarget: Lint;
1632
+ /**
1633
+ * Flags a declared workflow that nothing starts.
1634
+ *
1635
+ * Cross-references every `defineWorkflow` export against the set of workflow
1636
+ * names some function references via `ctx.workflows.get("&lt;name>")`. A workflow
1637
+ * with no such call is either dead code (declared, deployed as a billable
1638
+ * `WorkflowEntrypoint`, never triggered) or is started through a path the static
1639
+ * analysis can't see — the Cloudflare REST API, a `wrangler` invocation, or a
1640
+ * cross-service binding. Hence `INFO`/`INTERNAL`: a nudge to confirm intent.
1641
+ *
1642
+ * Suppressed entirely when any call uses a non-literal name
1643
+ * (`ctx.workflows.get(someVariable)`), because a dynamic dispatch could target
1644
+ * any declared workflow — flagging "unused" workflows then would be a false
1645
+ * positive. Only runs when the declaration feeder supplied evidence
1646
+ * (`context.workflows` present); a runtime caller flags nothing.
1647
+ */
1648
+ declare const workflowUnused: Lint;
1649
+ /**
1650
+ * Every lint that runs against the declared schema (and, for
1651
+ * `filter_without_index`, the discovered query reads) — no running shard
1652
+ * required. Correctness lints (`*_unknown_*`, `empty_index`) come first so a
1653
+ * broken schema's errors surface above the performance advisories.
1654
+ */
1655
+ declare const STATIC_LINTS: ReadonlyArray<Lint>;
1656
+ /**
1657
+ * Every lint that needs observed runtime signal (recorded metrics) rather than
1658
+ * just the declared schema. They read the feeder-supplied
1659
+ * {@link LintContext.shardTraffic} / {@link LintContext.tableScans} /
1660
+ * {@link LintContext.indexHits}; absent that signal (a static caller) each is a
1661
+ * no-op. Run them with `runAdvisor(ctx, { source: "runtime" })` against a live
1662
+ * deployment's aggregated metrics.
1663
+ */
1664
+ declare const RUNTIME_LINTS: ReadonlyArray<Lint>;
1665
+ /** The default lint set: the static lints, then the runtime lints. A caller filters by `source` to run one tier. */
1666
+ declare const ALL_LINTS: ReadonlyArray<Lint>;
1667
+ /** Options for {@link runAdvisor}. */
1668
+ interface RunAdvisorOptions {
1669
+ /** Lints to run (default: {@link ALL_LINTS}). */
1670
+ lints?: ReadonlyArray<Lint>;
1671
+ /** Restrict to a single evidence source — e.g. `"static"` at codegen time. */
1672
+ source?: LintSource;
1673
+ }
1674
+ /**
1675
+ * Run lints against a context and return their findings in lint-declaration
1676
+ * order. Filtering by {@link RunAdvisorOptions.source} lets a caller run only
1677
+ * `static` lints at build time and defer `runtime` lints to a live shard.
1678
+ */
1679
+ declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
1680
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, externalSourceOnGlobal, externalSourceUnscoped, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowDuplicateStepName, workflowUnknownTarget, workflowUnused };