@lunora/advisor 0.0.0 → 1.0.0-alpha.1

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 (42) 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 +1392 -0
  5. package/dist/index.d.ts +1392 -0
  6. package/dist/index.mjs +77 -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/filterWithoutIndex-BYVeJaSs.mjs +31 -0
  17. package/dist/packem_shared/finding-Dm_zvzS1.mjs +16 -0
  18. package/dist/packem_shared/fk-index-IUK1ukgs.mjs +7 -0
  19. package/dist/packem_shared/fromServerSchema-DinF1nph.mjs +50 -0
  20. package/dist/packem_shared/hardcodedSecret-W2pz1UZB.mjs +35 -0
  21. package/dist/packem_shared/helpers-DNCkMWZQ.mjs +4 -0
  22. package/dist/packem_shared/hotShard-Ir5D0B6J.mjs +48 -0
  23. package/dist/packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs +30 -0
  24. package/dist/packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs +36 -0
  25. package/dist/packem_shared/indexUtilization-B5DMQ3bI.mjs +45 -0
  26. package/dist/packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs +61 -0
  27. package/dist/packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs +35 -0
  28. package/dist/packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs +38 -0
  29. package/dist/packem_shared/publicArgumentUsesAny-C71b2NCf.mjs +32 -0
  30. package/dist/packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs +36 -0
  31. package/dist/packem_shared/relationReferencesUnknownField-YznyXt_7.mjs +54 -0
  32. package/dist/packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs +33 -0
  33. package/dist/packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs +56 -0
  34. package/dist/packem_shared/sqlInjectionRisk-zwytYGLt.mjs +26 -0
  35. package/dist/packem_shared/tableWithoutInsert-CbbaYIP4.mjs +34 -0
  36. package/dist/packem_shared/unboundedStringArgument-DThg2-wt.mjs +32 -0
  37. package/dist/packem_shared/unindexedForeignKey-BgJbKyqK.mjs +45 -0
  38. package/dist/packem_shared/unindexedRelationTarget-D6eyj6Xx.mjs +53 -0
  39. package/dist/packem_shared/userCreatingMutationWithoutCaptcha-CH31YsUZ.mjs +42 -0
  40. package/dist/packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs +34 -0
  41. package/dist/packem_shared/workflowUnused-D0jHxdz9.mjs +38 -0
  42. package/package.json +40 -17
@@ -0,0 +1,1392 @@
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 non-deterministic API call discovered lexically inside a `query(...)` or
218
+ * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
219
+ * lint consumes. Produced by the codegen feeder, which walks each exported
220
+ * function's handler with ts-morph and records calls to `Date.now`,
221
+ * `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and `fetch`.
222
+ * Calls inside `action(...)` handlers are intentionally **not** recorded — actions
223
+ * are the determinism escape hatch. Runtime callers don't supply this, so the
224
+ * lint finds nothing there.
225
+ */
226
+ interface AdvisorNondeterministicCall {
227
+ /** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
228
+ callee: string;
229
+ /** The exported function performing the call (e.g. `sendMessage`). */
230
+ exportName: string;
231
+ /** Source file the call appears in (relative to the lunora dir, no extension). */
232
+ file: string;
233
+ /** Which procedure kind the call lives in — only `query`/`mutation` handlers are non-deterministic; actions are exempt. */
234
+ kind: "mutation" | "query";
235
+ /** 1-based line of the call, or `0` when unknown. */
236
+ line: number;
237
+ }
238
+ /**
239
+ * One procedure (query / mutation / action) reduced to the protective middlewares
240
+ * its builder chain installs plus the behavioural facts that decide whether a
241
+ * guard is expected — the input the `public_mutation_without_ratelimit` and
242
+ * `user_creating_mutation_without_captcha` lints consume. A `protectPublic({...})`
243
+ * bundle is unwrapped by the feeder: its keys set `usesRateLimit`/`usesCaptcha`
244
+ * exactly as the standalone `.use(...)` steps would. Produced by the codegen
245
+ * feeder; runtime callers don't supply it, so the lints find nothing there.
246
+ */
247
+ interface AdvisorProcedureProtection {
248
+ /** `true` when the handler references `ctx.mail` / `ctx.email` (sends mail). */
249
+ callsMail: boolean;
250
+ /** The exported binding name of the procedure (e.g. `signUp`). */
251
+ exportName: string;
252
+ /** Source file relative to the lunora dir, no extension. */
253
+ file: string;
254
+ /** Registration kind — `query` is read-only; `mutation`/`action` are write-shaped. */
255
+ kind: "action" | "mutation" | "query";
256
+ /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
257
+ usesCaptcha: boolean;
258
+ /** `true` when the chain carries `.use(mask(...))`. */
259
+ usesMask: boolean;
260
+ /** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
261
+ usesRateLimit: boolean;
262
+ /** `true` when the chain carries `.use(rls(...))`. */
263
+ usesRls: boolean;
264
+ /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
265
+ visibility: "internal" | "public";
266
+ /** `true` when the handler inserts into a user/session/account-shaped table. */
267
+ writesUserTable: boolean;
268
+ }
269
+ /**
270
+ * One query read discovered in a function body — the input the
271
+ * `filter_without_index` lint consumes. Produced by the codegen feeder (which
272
+ * parses `ctx.db.query("table")…` chains from the AST); runtime callers don't
273
+ * supply it, so the lint simply finds nothing there.
274
+ */
275
+ interface AdvisorQueryRead {
276
+ /** Source file the read appears in (relative to the lunora dir, no extension). */
277
+ file: string;
278
+ /** True when the chain calls `.filter(...)`. */
279
+ hasFilter: boolean;
280
+ /** True when the chain narrows with `.withIndex(...)` or `.withSearchIndex(...)`. */
281
+ hasIndex: boolean;
282
+ /** 1-based line of the `query(...)` call, or `0` when unknown. */
283
+ line: number;
284
+ /** The queried table; empty when the `query(...)` argument is not a string literal. */
285
+ table: string;
286
+ }
287
+ /**
288
+ * One procedure (query / mutation / action) discovered in the lunora source,
289
+ * reduced to the facts the `rls_uncovered_table` lint needs: whether the
290
+ * procedure's builder chain includes `.use(rls(...))`, and which tables the
291
+ * procedure reads or writes. Produced by the codegen feeder; runtime callers
292
+ * don't supply it, so the lint finds nothing there.
293
+ */
294
+ interface AdvisorRlsProcedure {
295
+ /** The exported binding name of the procedure (e.g. `listDocuments`). */
296
+ exportName: string;
297
+ /** Source file relative to the lunora dir, no extension. */
298
+ file: string;
299
+ /**
300
+ * Tables explicitly named in the `rls(policies)` array passed to `.use(rls(...))`
301
+ * in this procedure's builder chain. Empty when the policies argument is not a
302
+ * statically-readable array literal (conservative: `usesRls` is still `true`).
303
+ */
304
+ rlsTables: ReadonlyArray<string>;
305
+ /** Tables read by the procedure via `ctx.db.query("table")` / `ctx.db.findMany(...)` etc. */
306
+ tablesRead: ReadonlyArray<string>;
307
+ /** Tables written by the procedure via `ctx.db.insert("table", …)` / `ctx.db.patch(...)` etc. */
308
+ tablesWritten: ReadonlyArray<string>;
309
+ /**
310
+ * `true` when the procedure's builder chain includes `.use(rls(...))` — the
311
+ * `rls` callee is identified by name from `@lunora/server`. `false` when no
312
+ * `.use(rls(...))` is found in the chain (or the procedure uses the bare
313
+ * `query({...})` factory form, which never carries a builder chain at all).
314
+ */
315
+ usesRls: boolean;
316
+ /** `"internal"` when the procedure uses `internalQuery` / `internalMutation` / `internalAction`. */
317
+ visibility: "internal" | "public";
318
+ }
319
+ /**
320
+ * Normalized, feeder-agnostic view of a schema that lints run against. Both the
321
+ * runtime `@lunora/server` {@link Schema} (record-shaped) and `@lunora/codegen`'s
322
+ * `SchemaIR` (array-shaped, AST-derived) collapse to this same shape, so a lint
323
+ * is written once and runs in either place. It carries only what the lints
324
+ * read — tables, their columns, indexes, and relations.
325
+ */
326
+ interface AdvisorSchema {
327
+ tables: ReadonlyArray<AdvisorTable>;
328
+ }
329
+ /** A table plus the column/index/relation metadata lints inspect. */
330
+ interface AdvisorTable {
331
+ /**
332
+ * `true` when the table is written outside Lunora's discoverable insert path
333
+ * — declared via `.externallyManaged()` (e.g. `@lunora/auth`'s better-auth
334
+ * tables, `@lunora/ratelimit`'s store). Insert-path lints
335
+ * (`table_without_insert`) skip such tables. Defaults to `false`.
336
+ */
337
+ externallyManaged?: boolean;
338
+ /**
339
+ * Declared column names (the `defineTable({...})` keys). Excludes the
340
+ * framework-managed system fields `_id` / `_creationTime`, which every table
341
+ * has implicitly — lints that resolve a column treat those as always valid.
342
+ */
343
+ fields: ReadonlyArray<string>;
344
+ /** Every declared index, across all kinds (secondary / search / rank / vector). */
345
+ indexes: ReadonlyArray<AdvisorIndex>;
346
+ /** Table name. */
347
+ name: string;
348
+ /**
349
+ * Column names that are optional or nullable and therefore may legally hold
350
+ * `null` / `undefined` in stored rows. Populated by {@link fromServerSchema}
351
+ * from the runtime validator graph (`v.optional(...)` → kind `"optional"`;
352
+ * `.nullable()` → `column.notNull === false`). When absent (e.g. from the
353
+ * codegen feeder, which does not supply this field), constraint lints that
354
+ * check NOT NULL should skip the check entirely or treat every field as
355
+ * required (the codegen feeder never runs runtime lints anyway).
356
+ */
357
+ optionalFields?: ReadonlySet<string>;
358
+ /** Declared relations (`.relations((r) => …)`). */
359
+ relations: ReadonlyArray<AdvisorRelation>;
360
+ }
361
+ /**
362
+ * One declared index, flattened across Lunora's index kinds so a single lint can
363
+ * reason about every column an index touches. `kind` distinguishes the DSL that
364
+ * declared it — only `index` (a btree secondary index) covers a foreign-key
365
+ * equality lookup, so the FK lint filters on it. `fields` is every column the
366
+ * index references (a secondary index's columns; a search index's text +
367
+ * filter fields; a rank index's sort + partition fields; a vector index's
368
+ * source field). `unique` is set only for unique secondary indexes.
369
+ */
370
+ interface AdvisorIndex {
371
+ fields: ReadonlyArray<string>;
372
+ kind: "index" | "rank" | "search" | "vector";
373
+ name: string;
374
+ unique?: boolean;
375
+ }
376
+ /**
377
+ * One declared relation. For a `one` relation the FK column `field` lives on
378
+ * the holding table; for `many` it lives on the target. `name` is the accessor
379
+ * the relation is loaded under.
380
+ */
381
+ interface AdvisorRelation {
382
+ field: string;
383
+ kind: "many" | "one";
384
+ name: string;
385
+ onDelete?: "cascade" | "restrict" | "set null";
386
+ references: string;
387
+ table: string;
388
+ }
389
+ /**
390
+ * Adapt the runtime `@lunora/server` {@link Schema} into an {@link AdvisorSchema}.
391
+ * Runtime callers (the studio backend, a live shard) hold the real schema
392
+ * object; this collapses its record-keyed `tables`/`relationMap` into the array
393
+ * form lints consume and flattens the per-kind index arrays into one list. The
394
+ * codegen feeder builds the same shape from its AST IR independently (it never
395
+ * imports `@lunora/server`).
396
+ */
397
+ declare const fromServerSchema: (schema: Schema) => AdvisorSchema;
398
+ /**
399
+ * One secret-shaped string literal discovered in the lunora source — the input
400
+ * the `hardcoded_secret` lint consumes. The full value is never carried; only a
401
+ * redacted {@link AdvisorSecretLiteral.preview}. Produced by the codegen feeder
402
+ * (complementing the pre-commit `vis secrets` scan); runtime callers don't supply
403
+ * it, so the lint finds nothing there.
404
+ */
405
+ interface AdvisorSecretLiteral {
406
+ /** Source file relative to the lunora dir, no extension. */
407
+ file: string;
408
+ /** Heuristic that matched, e.g. `stripe_live_key` / `aws_access_key` / `private_key` / `high_entropy`. */
409
+ kind: string;
410
+ /** 1-based line of the literal, or `0` when unknown. */
411
+ line: number;
412
+ /** Redacted preview (first few chars + length) — never the full secret. */
413
+ preview: string;
414
+ }
415
+ /**
416
+ * One shard's observed traffic share — the input the `hot_shard` runtime lint
417
+ * consumes. Produced by the studio backend, which fans out over a sharded
418
+ * function's shards and reads each shard's recorded request volume from the
419
+ * durable `__lunora_metrics` accumulator (`SUM(calls)`) — or, equivalently, the
420
+ * per-shard request-log count. Codegen and other static callers don't supply
421
+ * it, so the lint simply finds nothing there.
422
+ *
423
+ * The lint is a pure function over its context, so it can't fan out over shards
424
+ * itself; the caller does the cross-shard read and hands the aggregated
425
+ * distribution here, exactly as the codegen feeder hands `AdvisorQueryRead`s for
426
+ * the static query lints.
427
+ */
428
+ interface AdvisorShardTraffic {
429
+ /**
430
+ * The sharded function group these shards belong to, when the caller scopes
431
+ * the distribution to one `.shardBy(...)` function. Used only to name the
432
+ * finding; empty when the traffic is the whole deployment's shard set.
433
+ */
434
+ group?: string;
435
+ /** Total requests (function dispatches) recorded against this shard over the observed window. */
436
+ requests: number;
437
+ /**
438
+ * The shard key (the Durable Object id name) traffic was attributed to —
439
+ * a user / tenant / room id, depending on the `.shardBy(...)` key. Empty for
440
+ * the unnamed root DO.
441
+ */
442
+ shardKey: string;
443
+ }
444
+ /**
445
+ * One `ctx.sql` tagged-template interpolation that splices an unparameterized
446
+ * string-building expression into the query — the input the `sql_injection_risk`
447
+ * lint consumes. A `${…}` placeholder that simply names a value is bound as a
448
+ * parameter by the Hyperdrive driver and is *not* recorded; only in-place string
449
+ * construction (`"… " + raw`, a nested template literal) reaches here. Produced by
450
+ * the codegen feeder; runtime callers don't supply it, so the lint finds nothing
451
+ * there.
452
+ */
453
+ interface AdvisorSqlInterpolation {
454
+ /** The exported binding name of the procedure performing the `ctx.sql` call. */
455
+ exportName: string;
456
+ /** Source file relative to the lunora dir, no extension. */
457
+ file: string;
458
+ /** 1-based line of the interpolation, or `0` when unknown. */
459
+ line: number;
460
+ }
461
+ /**
462
+ * A bounded sample of rows from one table, fed into the constraint-validator
463
+ * lint by the studio backend (via `readTablePage`). The cap prevents unbounded
464
+ * scans while still catching obvious violations on small-to-medium tables.
465
+ *
466
+ * The studio notes the cap to the operator when the row count exceeds it
467
+ * (`truncated: true`), so violations on rows beyond the sample window are not
468
+ * silently missed — the finding description mentions the cap.
469
+ */
470
+ interface AdvisorTableSample {
471
+ /** The cap applied; equals `rows.length` when not truncated. */
472
+ readonly cap: number;
473
+ /**
474
+ * The row ids of every existing row in this table (bounded to `cap`), used
475
+ * for FK referential-integrity checks: if a FK value does not appear in the
476
+ * target table's `existingIds`, it is a dangling reference.
477
+ */
478
+ readonly existingIds: ReadonlySet<string>;
479
+ /** Sampled rows (up to `cap`). Each row includes `_id` and all declared columns. */
480
+ readonly rows: ReadonlyArray<Record<string, unknown>>;
481
+ /** The table's name. */
482
+ readonly table: string;
483
+ /** Whether more rows exist beyond the cap. */
484
+ readonly truncated: boolean;
485
+ }
486
+ /**
487
+ * The two workflow-shaped inputs the `workflow_*` lints consume, produced by the
488
+ * codegen feeder. {@link AdvisorWorkflow} is the declaration side (one per
489
+ * `defineWorkflow` export in `lunora/workflows.ts`); {@link AdvisorWorkflowCall}
490
+ * is the use side (one per `ctx.workflows.get("name")` call discovered in a
491
+ * function body). Runtime callers don't supply either, so the workflow lints
492
+ * simply find nothing there.
493
+ *
494
+ * Both are structural subsets of codegen's `WorkflowIR` / `WorkflowCallIR`, so
495
+ * the feeder passes the IR arrays straight through without conversion (mirrors
496
+ * how `AdvisorContainer` tracks `ContainerIR` and `AdvisorInsertWrite` tracks
497
+ * `InsertWriteIR`).
498
+ */
499
+ /** One workflow declared via a `defineWorkflow()` export in `lunora/workflows.ts`. */
500
+ interface AdvisorWorkflow {
501
+ /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
502
+ exportName: string;
503
+ }
504
+ /** One `ctx.workflows.get("name")` call discovered in a function body. */
505
+ interface AdvisorWorkflowCall {
506
+ /** The exported function performing the call (e.g. `create`). */
507
+ exportName: string;
508
+ /** Source file the call appears in (relative to the lunora dir, no extension). */
509
+ file: string;
510
+ /** 1-based line of the `get(...)` call, or `0` when unknown. */
511
+ line: number;
512
+ /** The referenced workflow export name; empty when the `get(...)` argument is not a string literal. */
513
+ workflow: string;
514
+ }
515
+ /**
516
+ * Severity of a finding, mirroring splinter's `level`. `ERROR` is a definite
517
+ * problem, `WARN` a likely one, `INFO` an advisory nudge.
518
+ */
519
+ type Level = "ERROR" | "INFO" | "WARN";
520
+ /**
521
+ * Who the finding concerns, mirroring splinter's `facing`. `EXTERNAL` findings
522
+ * affect clients of the app (performance/security a user can feel); `INTERNAL`
523
+ * ones are operator-only hygiene.
524
+ */
525
+ type Facing = "EXTERNAL" | "INTERNAL";
526
+ /**
527
+ * Concern bucket a lint belongs to. `SCHEMA` covers shape/correctness nits that
528
+ * are neither a perf nor a security issue (missing primary key, duplicate
529
+ * index). `PERFORMANCE` and `SECURITY` match splinter's two categories.
530
+ */
531
+ type Category = "PERFORMANCE" | "SCHEMA" | "SECURITY";
532
+ /**
533
+ * Where a lint draws its evidence from.
534
+ *
535
+ * `static` runs against the declared {@link AdvisorSchema} alone (tables,
536
+ * indexes, relations) — deterministic, runnable at codegen/build time, and
537
+ * catches a problem _before_ it ships. This is the edge Lunora has over a
538
+ * live-DB-only advisor like Supabase's.
539
+ *
540
+ * `runtime` needs observed signal from a running shard (full-scan attribution,
541
+ * function call stats). Added in a later slice; the context grows optional
542
+ * fields the runtime lints read.
543
+ */
544
+ type LintSource = "runtime" | "static";
545
+ /**
546
+ * One emitted advisory, shaped after splinter's lint-view row so the studio
547
+ * Advisors table can render any lint uniformly. `cacheKey` is a stable,
548
+ * content-derived id used to dedup across runs and to let an operator dismiss a
549
+ * specific finding without silencing the whole lint.
550
+ */
551
+ interface Finding {
552
+ /** Stable identifier for dedup/dismissal across runs. */
553
+ cacheKey: string;
554
+ /** The lint's concern buckets (usually one). */
555
+ categories: Category[];
556
+ /** Human-readable explanation of the rule in general terms. */
557
+ description: string;
558
+ /** The specific violation message for _this_ occurrence. */
559
+ detail: string;
560
+ /** Who the finding concerns. */
561
+ facing: Facing;
562
+ /** Severity. */
563
+ level: Level;
564
+ /** Structured context (table, field, index, …) for the UI and deep links. */
565
+ metadata: Record<string, unknown>;
566
+ /** The lint id that produced this finding, e.g. `unindexed_foreign_key`. */
567
+ name: string;
568
+ /** How to fix it — a doc URL or short imperative guidance. */
569
+ remediation: string;
570
+ /** Short headline for the finding. */
571
+ title: string;
572
+ }
573
+ /**
574
+ * Everything a lint may inspect. Static lints read only {@link LintContext.schema};
575
+ * runtime lints will additionally read observed-signal fields added here later.
576
+ */
577
+ interface LintContext {
578
+ /**
579
+ * `httpRoute.&lt;verb>("/admin/…")` routes on admin/privileged-looking paths and
580
+ * whether each references an auth/admin guard — the `admin_route_without_guard`
581
+ * input. Supplied by the codegen feeder; absent for runtime callers, where the
582
+ * lint finds nothing.
583
+ */
584
+ adminRoutes?: ReadonlyArray<AdvisorAdminRoute>;
585
+ /**
586
+ * Per-public-procedure argument validators that weaken input safety — the
587
+ * `public_arg_uses_any` (`v.any()` args) and `unbounded_string_arg` (length-less
588
+ * `v.string()` args) input. Supplied by the codegen feeder for public procedures
589
+ * only; absent for runtime callers, where the lints find nothing.
590
+ */
591
+ argValidators?: ReadonlyArray<AdvisorArgumentValidator>;
592
+ /**
593
+ * `ctx.authApi.&lt;method>(...)` calls discovered in function bodies (the
594
+ * `auth_api_call_without_headers` input). Supplied by the codegen feeder; absent
595
+ * for runtime callers, where the lint finds nothing.
596
+ */
597
+ authApiCalls?: ReadonlyArray<AdvisorAuthApiCall>;
598
+ /**
599
+ * Containers declared in `lunora/containers.ts` — the `container_*` lint
600
+ * input. Supplied by the codegen feeder; absent for runtime callers, where
601
+ * the container lints find nothing.
602
+ */
603
+ containers?: ReadonlyArray<AdvisorContainer>;
604
+ /**
605
+ * Hyperdrive `ctx.sql` accesses discovered lexically inside `query`/`mutation`
606
+ * handler bodies — the `hyperdrive_outside_action` input. Supplied by the
607
+ * codegen feeder, which omits `action` handlers (where `ctx.sql` is the typed,
608
+ * intended surface); absent for runtime callers, where the lint finds nothing.
609
+ */
610
+ hyperdriveCalls?: ReadonlyArray<AdvisorHyperdriveCall>;
611
+ /**
612
+ * Per-declared-index hit counts observed at runtime (the dead-index half of
613
+ * the `index_utilization` lint input). Supplied by the studio backend, which
614
+ * sums the per-`(table, index)` reads each shard records in the durable
615
+ * `__lunora_metrics_index` table and surfaces through the `getMetrics` admin
616
+ * RPC (see {@link AdvisorIndexHit}). Absent for static callers, where the
617
+ * dead-index check finds nothing.
618
+ */
619
+ indexHits?: ReadonlyArray<AdvisorIndexHit>;
620
+ /**
621
+ * Insert writes discovered in function bodies (the `table_without_insert`
622
+ * input). Supplied by the codegen feeder; absent for runtime callers, where
623
+ * the write-shaped lints simply find nothing.
624
+ */
625
+ inserts?: ReadonlyArray<AdvisorInsertWrite>;
626
+ /**
627
+ * Per-procedure column-masking usage discovered in function bodies (the
628
+ * `mask_uncovered_pii_column` input). Carries whether each procedure's builder
629
+ * chain includes `.use(mask(...))`, which `(table, column)` pairs its mask
630
+ * policy declares, and which tables the procedure reads/writes. Supplied by
631
+ * the codegen feeder; absent for runtime callers, where the lint finds
632
+ * nothing.
633
+ */
634
+ maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
635
+ /**
636
+ * Non-deterministic API calls (`Date.now`, `Math.random`,
637
+ * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
638
+ * inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
639
+ * input. Supplied by the codegen feeder, which omits `action` handlers (their
640
+ * non-determinism is intentional); absent for runtime callers, where the lint
641
+ * finds nothing.
642
+ */
643
+ nondeterministicCalls?: ReadonlyArray<AdvisorNondeterministicCall>;
644
+ /**
645
+ * Per-procedure protective-middleware snapshots — the
646
+ * `public_mutation_without_ratelimit` and `user_creating_mutation_without_captcha`
647
+ * input. Records which `.use(...)` guards (`rateLimit`, captcha, `rls`, `mask`,
648
+ * the `protectPublic` bundle) each procedure carries and whether it writes a
649
+ * user table or sends mail. Supplied by the codegen feeder; absent for runtime
650
+ * callers, where the lints find nothing.
651
+ */
652
+ procedureProtections?: ReadonlyArray<AdvisorProcedureProtection>;
653
+ /**
654
+ * Query reads discovered in function bodies (the `filter_without_index`
655
+ * input). Supplied by the codegen feeder; absent for runtime callers, where
656
+ * the query-shaped lints simply find nothing.
657
+ */
658
+ queries?: ReadonlyArray<AdvisorQueryRead>;
659
+ /**
660
+ * Per-procedure RLS usage discovered in function bodies (the
661
+ * `rls_uncovered_table` input). Carries whether each procedure's builder chain
662
+ * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
663
+ * tables its RLS policy array names. Supplied by the codegen feeder; absent for
664
+ * runtime callers, where the lint finds nothing.
665
+ */
666
+ rlsProcedures?: ReadonlyArray<AdvisorRlsProcedure>;
667
+ /** The declared schema under audit, normalized to the feeder-agnostic {@link AdvisorSchema}. */
668
+ schema: AdvisorSchema;
669
+ /**
670
+ * Secret-shaped string literals discovered in the lunora source — the
671
+ * `hardcoded_secret` input. Each carries only a redacted preview, never the
672
+ * full value. Supplied by the codegen feeder; absent for runtime callers,
673
+ * where the lint finds nothing.
674
+ */
675
+ secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
676
+ /**
677
+ * Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
678
+ * studio backend, which fans out over a sharded function's shards and reads
679
+ * each shard's recorded request volume from the durable `__lunora_metrics`
680
+ * accumulator. Absent for static callers, where the lint finds nothing.
681
+ */
682
+ shardTraffic?: ReadonlyArray<AdvisorShardTraffic>;
683
+ /**
684
+ * `ctx.sql` tagged-template interpolations that splice an unparameterized
685
+ * string-building expression into the query — the `sql_injection_risk` input.
686
+ * Supplied by the codegen feeder; absent for runtime callers, where the lint
687
+ * finds nothing.
688
+ */
689
+ sqlInterpolations?: ReadonlyArray<AdvisorSqlInterpolation>;
690
+ /**
691
+ * Bounded row samples per table — the `constraint_validator` lint input.
692
+ * Supplied by the studio backend, which reads up to the configured row cap
693
+ * from each table via `readTablePage` and assembles the existing-id set for
694
+ * FK referential-integrity checks. Absent for static callers or codegen
695
+ * feeders, where the constraint lint simply finds nothing.
696
+ *
697
+ * Each entry carries `existingIds` (every `_id` in the sample window) so
698
+ * FK columns can be cross-checked across tables in O(1) per value. When
699
+ * `truncated` is `true`, violations on rows beyond the cap are not reported
700
+ * — the finding description notes the sample cap so the operator understands
701
+ * the bounded window.
702
+ */
703
+ tableSamples?: ReadonlyArray<AdvisorTableSample>;
704
+ /**
705
+ * Per-table full-scan volume observed at runtime (the hot-scan half of the
706
+ * `index_utilization` lint input). Sourced from the per-`(function, table)`
707
+ * full-scan attribution the runtime records (`__lunora_metrics_scans`,
708
+ * surfaced as `FunctionCallStat.scannedTables`), aggregated across functions
709
+ * and shards. Absent for static callers, where the lint finds nothing.
710
+ */
711
+ tableScans?: ReadonlyArray<AdvisorTableScan>;
712
+ /**
713
+ * `ctx.workflows.get("name")` call sites discovered in function bodies — the
714
+ * use-side input the `workflow_unused` and `workflow_unknown_target` lints
715
+ * cross-reference against {@link LintContext.workflows}. Supplied by the
716
+ * codegen feeder; absent for runtime callers, where the workflow lints find
717
+ * nothing.
718
+ */
719
+ workflowCalls?: ReadonlyArray<AdvisorWorkflowCall>;
720
+ /**
721
+ * Workflows declared via `defineWorkflow` exports in `lunora/workflows.ts` —
722
+ * the declaration-side input for the `workflow_*` lints. Supplied by the
723
+ * codegen feeder; absent for runtime callers, where the workflow lints find
724
+ * nothing.
725
+ */
726
+ workflows?: ReadonlyArray<AdvisorWorkflow>;
727
+ }
728
+ /**
729
+ * A single advisory rule. `run` is pure over its {@link LintContext} so lints are
730
+ * trivially testable and order-independent. Each rule owns the static metadata
731
+ * (`name`/`title`/…) that its findings inherit, keeping individual `Finding`
732
+ * construction to just the per-occurrence `detail`/`metadata`/`cacheKey`.
733
+ */
734
+ interface Lint {
735
+ /** Concern buckets every finding from this lint carries. */
736
+ categories: Category[];
737
+ /** General-purpose description shared by every finding. */
738
+ description: string;
739
+ /** Default audience for this lint's findings. */
740
+ facing: Facing;
741
+ /** Default severity for this lint's findings. */
742
+ level: Level;
743
+ /** Unique lint id, snake_case (e.g. `unindexed_foreign_key`). */
744
+ name: string;
745
+ /** Fix guidance shared by every finding. */
746
+ remediation: string;
747
+ /** Produce zero or more findings for the given context. */
748
+ run: (context: LintContext) => Finding[];
749
+ /** Evidence source — see {@link LintSource}. */
750
+ source: LintSource;
751
+ /** Short headline shared by every finding. */
752
+ title: string;
753
+ }
754
+ /**
755
+ * Minimal structural view of the `@lunora/analytics` SQL client — just its
756
+ * `query(sql)` method. Kept structural (not an `import type` from
757
+ * `@lunora/analytics`) so the advisor needn't depend on the analytics package;
758
+ * the real `AnalyticsSqlClient` satisfies it, as does a plain test double.
759
+ */
760
+ interface AnalyticsMetricsSource {
761
+ query: (sql: string) => Promise<{
762
+ rows: ReadonlyArray<Record<string, unknown>>;
763
+ }>;
764
+ }
765
+ /**
766
+ * The AE event-name + dimension-column contract the runtime writes and this
767
+ * reader reads. `blob1` is the event name; dimensions start at `blob2`.
768
+ */
769
+ declare const AE_METRIC_EVENTS: {
770
+ /** `lunora.index.hit` — one row per `(table, index)` use. `blob2`=table, `blob3`=index. */
771
+ readonly indexHit: {
772
+ readonly event: "lunora.index.hit";
773
+ readonly index: "blob3";
774
+ readonly table: "blob2";
775
+ };
776
+ /** `lunora.shard.request` — one row per shard dispatch. `blob2`=shardKey, `blob3`=group. */
777
+ readonly shardRequest: {
778
+ readonly event: "lunora.shard.request";
779
+ readonly group: "blob3";
780
+ readonly shardKey: "blob2";
781
+ };
782
+ /** `lunora.table.scan` — one row per full-scan. `blob2`=table. */
783
+ readonly tableScan: {
784
+ readonly event: "lunora.table.scan";
785
+ readonly table: "blob2";
786
+ };
787
+ };
788
+ /** Options for the AE-backed runtime-metrics feeder. */
789
+ interface AnalyticsMetricsOptions {
790
+ /** The AE dataset (the wrangler `analytics_engine_datasets[].dataset`) to read from. */
791
+ dataset: string;
792
+ /**
793
+ * Declared index names per table, used to synthesise the `reads: 0` rows the
794
+ * `index_utilization` dead-index half needs. AE only stores rows for indexes
795
+ * that were *used*, so a never-hit index has no AE row at all; supplying the
796
+ * declared set lets the reader emit an explicit `reads: 0` entry for any
797
+ * declared index absent from the AE hit feed. Omit it to report only the
798
+ * positive hit counts AE returns.
799
+ */
800
+ declaredIndexes?: ReadonlyArray<{
801
+ index: string;
802
+ table: string;
803
+ }>;
804
+ /**
805
+ * Restrict the shard-traffic read to one sharded-function group (`blob3`).
806
+ * Omit to read the whole deployment's shard set.
807
+ */
808
+ group?: string;
809
+ }
810
+ /** The runtime-lint input arrays this module reconstructs from AE. */
811
+ interface AnalyticsRuntimeMetrics {
812
+ indexHits: AdvisorIndexHit[];
813
+ shardTraffic: AdvisorShardTraffic[];
814
+ tableScans: AdvisorTableScan[];
815
+ }
816
+ /**
817
+ * Reconstruct the runtime-lint input arrays (`shardTraffic` / `tableScans` /
818
+ * `indexHits`) from the Analytics Engine SQL API. The three reads run
819
+ * concurrently; each degrades to an empty array on a query failure, so a
820
+ * partially-misconfigured read path still returns what it can.
821
+ *
822
+ * Feed the result into a {@link LintContext} alongside the declared schema:
823
+ *
824
+ * ```ts
825
+ * const metrics = await loadAnalyticsRuntimeMetrics(client, { dataset: "ANALYTICS" });
826
+ * runAdvisor({ schema, ...metrics }, { source: "runtime" });
827
+ * ```
828
+ */
829
+ declare const loadAnalyticsRuntimeMetrics: (source: AnalyticsMetricsSource, options: AnalyticsMetricsOptions) => Promise<AnalyticsRuntimeMetrics>;
830
+ /**
831
+ * Constraint validator — flag rows that violate declared FK / NOT NULL / UNIQUE
832
+ * constraints by cross-checking sampled row data against the schema.
833
+ *
834
+ * This lint reads the `context.tableSamples` feed (bounded row samples supplied
835
+ * by the studio backend via `readTablePage`) and the declared schema. Three
836
+ * families of check run over each sample:
837
+ *
838
+ * FK referential integrity: for every `one` relation the holding table declares,
839
+ * check that each sampled row's FK column value appears in the target table's
840
+ * sampled id set. A dangling value means no target row exists for the reference.
841
+ *
842
+ * NOT NULL / non-optional columns: the lint surfaces rows with null/undefined in
843
+ * declared fields — inserted before a column was added or via raw import.
844
+ *
845
+ * UNIQUE index violations: for each declared unique secondary index, check the
846
+ * sampled rows for duplicate values across the index's columns.
847
+ *
848
+ * All checks are bounded by the cap in each sample; the lint never triggers an
849
+ * additional read. When a sample is truncated, findings note the caveat.
850
+ */
851
+ declare const constraintValidator: Lint;
852
+ /**
853
+ * `hot_shard` — flag a shard whose request share is disproportionately high.
854
+ *
855
+ * Sharding (`.shardBy(key)`) spreads state and load across many Durable Objects
856
+ * by user / tenant / room. Its whole value is *even* distribution: when one
857
+ * shard absorbs a dominant fraction of traffic, that single DO becomes the
858
+ * bottleneck (one request stream, one SQLite, one WS fan-out) while its siblings
859
+ * idle — the hot-key skew sharding is meant to avoid. That usually means the
860
+ * shard key has too little cardinality, or one entity is unusually busy and
861
+ * needs its own split.
862
+ *
863
+ * The per-shard request volume comes from the runtime feeder
864
+ * (`context.shardTraffic`): the studio backend fans out over the function's
865
+ * shards and reads each shard's recorded `__lunora_metrics` call total. The lint
866
+ * is pure over that distribution, so it only fires once the window has more than
867
+ * one shard and enough total requests (`MIN_TOTAL_REQUESTS`) for the proportion
868
+ * to be trustworthy.
869
+ */
870
+ declare const hotShard: Lint;
871
+ /**
872
+ * `index_utilization` — flag indexes the workload doesn't pay for. Two
873
+ * complementary checks over recorded reads.
874
+ *
875
+ * Dead index — a declared index that recorded reads never used. An unused index
876
+ * is pure overhead: every write maintains it, every byte of storage holds it,
877
+ * and nothing reads through it. Fired off the per-index hit feed
878
+ * (`context.indexHits`); a declared index whose recorded `reads` is `0` is dead.
879
+ * The runtime records this in the durable `__lunora_metrics_index` table (every
880
+ * index use stamps a per-`(table, index)` counter via `onIndexUse`) and surfaces
881
+ * it through the `getMetrics` admin RPC; the studio sums the per-shard arrays
882
+ * into `context.indexHits` (see `AdvisorIndexHit`). The counter is cumulative and
883
+ * never decays, so a non-zero index never reverts to "dead" — `reads: 0` means
884
+ * the index has not been used once since the counter was created.
885
+ *
886
+ * Hot unindexed scan — a table read hot with no index at all. Fired off the
887
+ * full-scan attribution the runtime does record (`context.tableScans`, sourced
888
+ * from `__lunora_metrics_scans` / `FunctionCallStat.scannedTables`): a table
889
+ * whose scan count clears `HOT_SCAN_THRESHOLD` is one the app keeps
890
+ * full-scanning, the runtime-confirmed counterpart to the static
891
+ * `filter_without_index` advisory.
892
+ */
893
+ declare const indexUtilization: Lint;
894
+ /**
895
+ * Flags an `httpRoute` on an admin/privileged-looking path whose handler shows no
896
+ * auth/admin guard.
897
+ *
898
+ * REST routes (unlike queries/mutations) aren't covered by RLS — they run whatever
899
+ * the handler does, so an `/admin/*` (or `/internal/*`, `/_*`) route with no
900
+ * session/admin check is an open privilege door: anyone who can reach the URL can
901
+ * invoke it. The handler must assert an authenticated, authorized caller
902
+ * (`ctx.auth` / `getSession` / a `requireAdmin`-style guard) before doing
903
+ * privileged work.
904
+ *
905
+ * Detection is heuristic: the feeder records whether the handler body references
906
+ * any known guard token. Runs only when the codegen feeder supplies route evidence
907
+ * (`context.adminRoutes`); a runtime caller flags nothing.
908
+ */
909
+ declare const adminRouteWithoutGuard: Lint;
910
+ /**
911
+ * Flags a `ctx.authApi.&lt;method>(...)` call whose argument object omits `headers`.
912
+ *
913
+ * `@lunora/auth`'s `withAuthPlugins` middleware attaches the full privileged
914
+ * better-auth API to `ctx.authApi` — `banUser`, `setRole`, impersonation,
915
+ * `createOrganization`, `removeMember`, etc. better-auth authorizes these calls
916
+ * from the caller's session carried in the `headers` you pass. Called
917
+ * **without** `headers`, better-auth treats the invocation as a trusted
918
+ * server-to-server call and **skips session authorization entirely**. So a
919
+ * header-less `ctx.authApi.banUser({ body })` runs with full privileges
920
+ * regardless of who the caller is — an authorization bypass.
921
+ *
922
+ * This lint runs when the codegen feeder has supplied call evidence
923
+ * (`context.authApiCalls` present); a runtime caller with no evidence flags
924
+ * nothing rather than raising false alarms.
925
+ */
926
+ declare const authApiCallWithoutHeaders: Lint;
927
+ /**
928
+ * Detect FK cycles in the declared relation graph via a DFS.
929
+ *
930
+ * A "circular FK" exists when a chain of `one` relations forms a loop — for
931
+ * example `A.authorId → B`, `B.ownerId → C`, `C.postId → A`. Such cycles can
932
+ * cause unexpected behavior during DELETE operations: a CASCADE chain may loop
933
+ * forever (or deadlock), and even a RESTRICT cycle prevents deletion of any row
934
+ * in the loop without temporarily disabling constraints.
935
+ *
936
+ * Only `one` relations are followed because they are the side that owns the FK
937
+ * column (the `field` lives on the holding table). `many` relations point back
938
+ * to the same edge from the opposite side and would cause every edge to be
939
+ * double-counted; skipping them gives the correct directed graph.
940
+ *
941
+ * A single-table self-reference (`A.parentId → A`) is **not** reported: a
942
+ * self-referential FK is the canonical, intentional shape for trees/hierarchies
943
+ * (categories, org charts, threaded comments), so flagging every such schema
944
+ * would be noise. Only multi-table cycles — the ones the description illustrates
945
+ * — are surfaced.
946
+ *
947
+ * Each unique cycle is reported once: the cycle path is canonicalized to its
948
+ * lexicographically smallest rotation so two DFS traversals that enter the same
949
+ * ring at different nodes emit the same cacheKey and detail. A representative
950
+ * cycle is reported for each distinct simple cycle in the graph; overlapping or
951
+ * chord cycles that share interior nodes are each detected independently.
952
+ */
953
+ declare const circularFk: Lint;
954
+ /**
955
+ * Flags a container declared on a large instance type. The big `standard-3` /
956
+ * `standard-4` sizes (and large custom shapes) are billed on their provisioned
957
+ * memory + disk for the whole time an instance runs, so an over-provisioned
958
+ * container is a standing cost. Informational — a real workload may need it —
959
+ * but worth surfacing so the choice is deliberate.
960
+ */
961
+ declare const containerOversizedInstance: Lint;
962
+ /**
963
+ * Flags a container that leaves outbound internet access at the platform
964
+ * default (`enableInternet: true`). Egress is billed per GB and an open
965
+ * outbound path widens the attack surface, so a container that doesn't call
966
+ * external services should set `enableInternet: false` explicitly. We can't
967
+ * tell from config whether egress is actually used, so this is an INFO nudge to
968
+ * make the choice deliberate, not an error.
969
+ *
970
+ * Only fires when the field was omitted (or a non-literal we couldn't read) —
971
+ * an explicit `enableInternet: true` is treated as a deliberate opt-in and left
972
+ * alone.
973
+ */
974
+ declare const containerPublicInternet: Lint;
975
+ /**
976
+ * Lunora port of splinter's `0009_duplicate_index`.
977
+ *
978
+ * A btree secondary index is redundant when another index already serves every
979
+ * lookup it does — i.e. its columns are a leading prefix of the other's
980
+ * (SQLite's leftmost-prefix rule means `["a", "b"]` already covers `["a"]`).
981
+ * Exact duplicates are the degenerate case. A redundant index is pure overhead:
982
+ * extra storage and a write amplified on every insert/update/delete.
983
+ *
984
+ * Only `kind: "index"` participates — search/rank/vector indexes are distinct
985
+ * structures, never redundant with a btree. A `unique` index is never reported
986
+ * as redundant even when its columns are a prefix: it enforces a constraint the
987
+ * covering index does not, so dropping it would change behavior.
988
+ */
989
+ declare const duplicateIndex: Lint;
990
+ /**
991
+ * Flags a secondary index declared with no columns (`.index("x", [])`). The
992
+ * `.index(name, fields)` builder types `fields` as `string[]`, not
993
+ * `keyof Shape[]`, so an empty array slips past the compiler — but an index over
994
+ * zero columns indexes nothing and can never narrow a read. Almost always a
995
+ * leftover from a refactor. (Search / rank / vector indexes always carry at
996
+ * least one field by construction, so only `kind: "index"` is checked.)
997
+ */
998
+ declare const emptyIndex: Lint;
999
+ /**
1000
+ * Flags a query read that calls `.filter()` without first narrowing with
1001
+ * `.withIndex()` / `.withSearchIndex()`. Such a read loads *every* row of the
1002
+ * table and applies the predicate in memory — a full table scan that degrades
1003
+ * linearly as the table grows. The healthy pattern is `.withIndex(...)` to
1004
+ * narrow, then `.filter(...)` only for predicates the index can't express
1005
+ * (which is why an indexed read with a trailing `.filter()` is NOT flagged).
1006
+ *
1007
+ * The query reads come from the codegen feeder, which parses
1008
+ * `ctx.db.query("table")…` chains out of function bodies. Runtime callers supply
1009
+ * no `queries`, so this lint is a no-op there.
1010
+ */
1011
+ declare const filterWithoutIndex: Lint;
1012
+ /**
1013
+ * Flags a secret-shaped string literal checked into the lunora source.
1014
+ *
1015
+ * A live API key, access key, private key, or high-entropy token committed to the
1016
+ * codebase leaks the moment the repo is cloned, forked, or its history is read —
1017
+ * and rotating it means a redeploy. Secrets belong in `.dev.vars` locally and
1018
+ * `wrangler secret put` in production, read at runtime via `env`. This lint
1019
+ * surfaces the same class of finding the pre-commit `vis secrets` gate catches,
1020
+ * inside the studio Advisors table.
1021
+ *
1022
+ * Runs only when the codegen feeder supplies secret evidence
1023
+ * (`context.secretLiterals`); a runtime caller flags nothing. One finding per
1024
+ * literal.
1025
+ */
1026
+ declare const hardcodedSecret: Lint;
1027
+ /**
1028
+ * Flags a Hyperdrive `ctx.sql` access inside a `query(...)` or `mutation(...)`
1029
+ * handler body.
1030
+ *
1031
+ * Hyperdrive (`@lunora/hyperdrive`) points at an **external** Postgres/MySQL
1032
+ * database Lunora does not own. A `ctx.sql` query is a network round-trip with a
1033
+ * mutable result — non-deterministic, exactly like `fetch` — so it breaks the
1034
+ * determinism the coordinator relies on when it re-runs a query on subscription
1035
+ * re-evaluation or a mutation on OCC retry. Worse, external writes are invisible
1036
+ * to the DO/SQLite change-feed, so a subscription will never re-fire on them.
1037
+ * `ctx.sql` is therefore wired onto `ActionCtx` **only** and belongs exclusively
1038
+ * in `action(...)` handlers; using it in a query/mutation is the same class of
1039
+ * bug as `fetch`/`Date.now`.
1040
+ *
1041
+ * This is the enforcement teeth behind the action-only rule — runtime
1042
+ * enforcement is still absent (see `MEMORY.md` "Query/mutation determinism not
1043
+ * enforced"), so the lint is the guardrail.
1044
+ *
1045
+ * This lint runs when the codegen feeder has supplied access evidence
1046
+ * (`context.hyperdriveCalls` present); a runtime caller with no evidence flags
1047
+ * nothing rather than raising false alarms. The feeder records accesses only
1048
+ * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1049
+ */
1050
+ declare const hyperdriveOutsideAction: Lint;
1051
+ /**
1052
+ * A correctness lint with no splinter analogue — it exploits Lunora's static
1053
+ * edge: the schema is fully declared, so a typo'd index column is catchable at
1054
+ * codegen time rather than surfacing as a runtime error or a silently
1055
+ * never-matching index.
1056
+ *
1057
+ * Every index (secondary / search / rank / vector) names the columns it covers;
1058
+ * each must be a declared column of the table (or a system field). A reference
1059
+ * to an unknown column is almost always a typo or a column that was renamed
1060
+ * without updating the index.
1061
+ */
1062
+ declare const indexReferencesUnknownField: Lint;
1063
+ /**
1064
+ * Flags a public procedure that reads a table for which at least one other
1065
+ * procedure declares a column mask (evidence the developer decided that table
1066
+ * carries sensitive columns), but whose own builder chain does NOT include
1067
+ * `.use(mask(...))`.
1068
+ *
1069
+ * Lunora masking is **opt-in per procedure**: a `mask(policies)` object only
1070
+ * redacts columns inside procedures whose builder chain includes
1071
+ * `.use(mask(policies))`. A procedure without it returns the raw column value —
1072
+ * even when another procedure in the same app declares that column maskable.
1073
+ * This is the "one procedure masks `users.email`, another leaks it" failure
1074
+ * mode, the column-level sibling of `rls_uncovered_table`.
1075
+ *
1076
+ * **Granularity**: the lint is table-granular, not column-precise. Statically
1077
+ * proving that a procedure *returns* a specific masked column would need
1078
+ * return-shape analysis that is infeasible over the IR; instead the lint flags a
1079
+ * public procedure that *reads* a mask-covered table without any
1080
+ * `.use(mask(...))` of its own, and the finding lists the masked columns the
1081
+ * developer flagged elsewhere. Only reads are considered — masking is a
1082
+ * read/return-path concern, so writes never trigger it.
1083
+ *
1084
+ * **Scope**: only `public` procedures are flagged. `internal*` procedures
1085
+ * (e.g. `internalQuery`, `internalMutation`) intentionally bypass masking, so
1086
+ * flagging them would produce only noise. Remediation text notes this exemption.
1087
+ *
1088
+ * **Evidence supply**: this lint runs only when the codegen feeder has supplied
1089
+ * `context.maskProcedures`; a runtime caller with no evidence flags nothing
1090
+ * rather than raising false alarms.
1091
+ *
1092
+ * **Conservative policy detection**: when a procedure calls `mask(policies)`
1093
+ * with a non-literal object (a variable reference), the feeder cannot statically
1094
+ * enumerate the masked `(table, column)` pairs. In that case the procedure is
1095
+ * still marked `usesMask: true` (so it is NOT itself flagged), but its pairs
1096
+ * contribute nothing to the masked-column map. The lint may under-report (false
1097
+ * negatives) when policies are extracted into named constants, but never
1098
+ * over-reports (no false positives).
1099
+ */
1100
+ declare const maskUncoveredPiiColumn: Lint;
1101
+ /**
1102
+ * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
1103
+ * handler body.
1104
+ *
1105
+ * Lunora queries and mutations must be **deterministic**: the coordinator may
1106
+ * re-run a mutation on optimistic-concurrency (OCC) retry and a query on
1107
+ * subscription re-evaluation, so a handler that reads wall-clock time, draws
1108
+ * randomness, or hits the network can produce different results on each run —
1109
+ * breaking read-your-writes, cache invalidation, and replayable history.
1110
+ * `Date.now`, `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`, and
1111
+ * `fetch` are therefore disallowed in query/mutation handlers and belong in an
1112
+ * `action(...)`, which runs exactly once and may use ambient/non-deterministic
1113
+ * APIs freely (pass the result into a mutation as an argument).
1114
+ *
1115
+ * This lint runs when the codegen feeder has supplied call evidence
1116
+ * (`context.nondeterministicCalls` present); a runtime caller with no evidence
1117
+ * flags nothing rather than raising false alarms. The feeder records calls only
1118
+ * inside `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1119
+ */
1120
+ declare const nondeterministicQueryMutation: Lint;
1121
+ /**
1122
+ * Flags an RLS policy whose `table` names a table that does not exist in the
1123
+ * schema.
1124
+ *
1125
+ * A policy is bound to a table by a plain string (`definePolicy({ table:
1126
+ * "documents", … })`). The `rls()` middleware only applies a policy to reads and
1127
+ * writes of that exact table name — so a typo, a stale name after a rename, or a
1128
+ * copy-paste mistake produces a policy that silently matches **nothing**. The
1129
+ * table the developer believes is gated is left completely ungated, which is a
1130
+ * security gap, not a mere dead-code wart: every read of the real table returns
1131
+ * unrestricted rows and every write is allowed.
1132
+ *
1133
+ * This is strictly worse than `rls_uncovered_table` (a procedure forgetting the
1134
+ * middleware): here the middleware *is* wired up, the policy *is* in the list,
1135
+ * and it still does nothing — the failure is invisible at every call site.
1136
+ *
1137
+ * **Evidence supply**: like `rls_uncovered_table`, this runs only when the
1138
+ * codegen feeder supplies `context.rlsProcedures`. The covered-table names come
1139
+ * from each procedure's statically-read `rls(policies)` array (`rlsTables`); a
1140
+ * policies argument that isn't a literal array contributes no names, so the lint
1141
+ * under-reports rather than raising false alarms.
1142
+ */
1143
+ declare const policyReferencesUnknownTable: Lint;
1144
+ /**
1145
+ * Flags a `v.any()` argument on a public procedure.
1146
+ *
1147
+ * `v.any()` disables validation: the field accepts arbitrary, untyped,
1148
+ * arbitrarily-large input straight from an untrusted client. That defeats the
1149
+ * end-to-end type safety Lunora exists to provide and opens the door to injection,
1150
+ * prototype pollution, and oversized-payload abuse. Public input should be a
1151
+ * precise validator (`v.object`, `v.string`, `v.union`, …).
1152
+ *
1153
+ * Runs only when the codegen feeder supplies arg evidence
1154
+ * (`context.argValidators`, public procedures only); a runtime caller flags
1155
+ * nothing. One finding per offending arg.
1156
+ */
1157
+ declare const publicArgumentUsesAny: Lint;
1158
+ /**
1159
+ * Flags a public `mutation`/`action` whose builder chain installs no rate limit.
1160
+ *
1161
+ * Every publicly-callable write is a flood target: without a `rateLimit`
1162
+ * middleware a single client can hammer it to exhaust D1 writes, send-mail quota,
1163
+ * or paid credits, and brute-force auth-shaped endpoints (login / reset / OTP).
1164
+ * Lunora ships `rateLimit()` (`@lunora/ratelimit`) and the `protectPublic({...})`
1165
+ * bundle for exactly this; this lint fires when neither is present on a public
1166
+ * write.
1167
+ *
1168
+ * Runs only when the codegen feeder supplies protection evidence
1169
+ * (`context.procedureProtections`); a runtime caller with no evidence flags
1170
+ * nothing. `query` is read-only and excluded; internal functions are
1171
+ * server-called and excluded.
1172
+ */
1173
+ declare const publicMutationWithoutRatelimit: Lint;
1174
+ /**
1175
+ * A correctness lint covering the columns a relation wires together: the FK
1176
+ * `field` and the `references` column must each exist on their respective
1177
+ * tables, or the join can never resolve. Caught here at codegen time rather
1178
+ * than as a runtime failure.
1179
+ */
1180
+ declare const relationReferencesUnknownField: Lint;
1181
+ /**
1182
+ * A correctness lint: every relation declared via `.relations((r) => …)` names a
1183
+ * target table, which must exist in the schema. A target that resolves to no
1184
+ * table is a typo or a reference to a table that was removed/renamed — the
1185
+ * relation can never load. Caught here at codegen time rather than at runtime.
1186
+ *
1187
+ * (Extension tables are already namespaced and their relation targets rewritten
1188
+ * by the time the schema reaches a lint, so a surviving unknown target is a real
1189
+ * miss, not an unresolved cross-package reference.)
1190
+ */
1191
+ declare const relationReferencesUnknownTable: Lint;
1192
+ /**
1193
+ * Flags a public procedure that reads or writes a table named in at least one
1194
+ * other procedure's `rls(policies)` list, but whose own builder chain does NOT
1195
+ * include `.use(rls(...))`.
1196
+ *
1197
+ * Lunora RLS is **opt-in per procedure**: a policy list only takes effect
1198
+ * inside procedures whose builder chain includes `.use(rls(policies))`. A
1199
+ * procedure without it sees the raw, unwrapped `ctx.db` and silently bypasses
1200
+ * every policy in the list — even when another procedure in the same app
1201
+ * declares that table as policy-gated.
1202
+ *
1203
+ * The lint surfaces the most dangerous subclass of this failure mode: a table
1204
+ * that the developer explicitly decided to gate with RLS (evidenced by naming
1205
+ * it in at least one procedure's policy list) is nonetheless accessible
1206
+ * without restriction from a procedure that forgot the `.use(rls(...))` call.
1207
+ *
1208
+ * **Scope**: only `public` procedures are flagged. `internal*` procedures
1209
+ * (e.g. `internalQuery`, `internalMutation`) are intentional server-side
1210
+ * helpers that legitimately bypass the user-facing RLS gate, so flagging them
1211
+ * would produce only noise. Remediation text notes this exemption so authors
1212
+ * know to use `internalQuery`/`internalMutation`/`internalAction` when they
1213
+ * truly need unwrapped access.
1214
+ *
1215
+ * **Evidence supply**: this lint runs only when the codegen feeder has supplied
1216
+ * `context.rlsProcedures`; a runtime caller with no evidence flags nothing
1217
+ * rather than raising false alarms.
1218
+ *
1219
+ * **Conservative policy-table detection**: when a procedure calls
1220
+ * `rls(policies)` with a non-literal array (a variable reference), the feeder
1221
+ * cannot statically enumerate the covered tables. In that case the procedure is
1222
+ * still marked `usesRls: true` (so it is NOT itself flagged), but its tables
1223
+ * contribute nothing to `policyCoveredTables`. This means the lint may
1224
+ * under-report (false negatives) when policies are extracted into named
1225
+ * constants, but it never over-reports (no false positives).
1226
+ */
1227
+ declare const rlsUncoveredTable: Lint;
1228
+ /**
1229
+ * Flags a `ctx.sql` tagged-template that splices an unparameterized
1230
+ * string-building expression into the query.
1231
+ *
1232
+ * The Hyperdrive `ctx.sql\`…\`` driver binds each `${value}` placeholder as a
1233
+ * query parameter — safe by construction. But a placeholder that *builds* a string
1234
+ * in place (`ctx.sql\`… ${"WHERE name='" + name + "'"}\``, or a nested template
1235
+ * literal) splices raw, attacker-controlled text into the SQL, reopening classic
1236
+ * SQL injection. The fix is always to pass the value through a placeholder so the
1237
+ * driver parameterizes it.
1238
+ *
1239
+ * Runs only when the codegen feeder supplies interpolation evidence
1240
+ * (`context.sqlInterpolations`); a runtime caller flags nothing. One finding per
1241
+ * interpolation.
1242
+ */
1243
+ declare const sqlInjectionRisk: Lint;
1244
+ /**
1245
+ * Flags a declared table that no function inserts into.
1246
+ *
1247
+ * Using `@lunora/codegen`'s write-side discovery (the analog of the read
1248
+ * discovery that feeds `filter_without_index`), this lint cross-references every
1249
+ * schema table against the set of tables some exported function writes via
1250
+ * `ctx.db.insert("&lt;table>", …)`. A table with no such write either is dead schema
1251
+ * or is populated through a path the static analysis can't see — a migration/seed,
1252
+ * cross-region replication, the `ctx.orm.insert(...)` builder, or a trusted
1253
+ * snapshot import. Hence `INFO`/`INTERNAL`: a nudge to confirm intent, not an error.
1254
+ *
1255
+ * A table declared `.externallyManaged()` is skipped — that flag is the explicit
1256
+ * acknowledgement that its rows are written outside Lunora (an adapter/migration/
1257
+ * middleware), so `@lunora/auth`'s better-auth tables and `@lunora/ratelimit`'s
1258
+ * store never flag here.
1259
+ *
1260
+ * Only runs when the write feeder supplied evidence (`context.inserts` present);
1261
+ * a runtime caller with no insert signal flags nothing rather than every table.
1262
+ */
1263
+ declare const tableWithoutInsert: Lint;
1264
+ /**
1265
+ * Flags a public `v.string()` argument with no length bound.
1266
+ *
1267
+ * A string field that accepts an unbounded value lets a client send megabytes of
1268
+ * text per request — inflating storage, blowing the row/document size budget, and
1269
+ * driving CPU/memory on every handler that processes it. A `.check()`/`.meta()`
1270
+ * max-length bound caps the blast radius. Advisory (INFO): a deliberately-open
1271
+ * free-text field is sometimes legitimate, so this nudges rather than blocks.
1272
+ *
1273
+ * Runs only when the codegen feeder supplies arg evidence
1274
+ * (`context.argValidators`, public procedures only); a runtime caller flags
1275
+ * nothing. One finding per offending arg.
1276
+ */
1277
+ declare const unboundedStringArgument: Lint;
1278
+ /**
1279
+ * Lunora port of splinter's `0001_unindexed_foreign_keys`.
1280
+ *
1281
+ * A `one` (many-to-one) relation declares a foreign-key column (`relation.field`)
1282
+ * on the holder table pointing at the target's `references` column. If no index
1283
+ * leads with that column, every read that filters or joins on the FK degrades to
1284
+ * a full table scan — the canonical silent performance cliff as a table grows.
1285
+ *
1286
+ * Coverage follows SQLite's leftmost-prefix rule: a composite index
1287
+ * `["authorId", "createdAt"]` covers lookups on `authorId`, so the FK is
1288
+ * satisfied when it is the *leading* column of any declared index. `many`
1289
+ * relations are skipped here — their FK lives on the opposite table and is
1290
+ * caught when that table's own `one` side is audited.
1291
+ */
1292
+ declare const unindexedForeignKey: Lint;
1293
+ /**
1294
+ * The to-many counterpart of `unindexed_foreign_key`.
1295
+ *
1296
+ * A `many` relation declares its foreign-key column (`relation.field`) on the
1297
+ * target table — `users.posts = r.many("posts", { field: "authorId" })` puts
1298
+ * `authorId` on `posts`. A relation predicate over that relation (`{ posts: {
1299
+ * some|none|every: W } }` in a `where`/RLS policy) and a `with:` child load both
1300
+ * resolve by querying the target table on that FK column, so an unindexed FK
1301
+ * there is the same silent full-scan cliff `unindexed_foreign_key` warns about —
1302
+ * just on the other side of the relation.
1303
+ *
1304
+ * `unindexed_foreign_key` only audits a table's own `one` relations, so it
1305
+ * catches this column **only when the target table declares the inverse `one`
1306
+ * relation** (`posts.author = r.one("users", { field: "authorId" })`). A
1307
+ * one-directional `many` (declared on the parent, with no inverse `one` on the
1308
+ * child) slips through — that exact gap is this lint's job. To stay strictly
1309
+ * complementary it skips any FK the target already covers via its own `one`
1310
+ * relation (reported there) and only fires on the otherwise-unaudited column.
1311
+ */
1312
+ declare const unindexedRelationTarget: Lint;
1313
+ /**
1314
+ * Flags a public `mutation`/`action` that creates a user/session or sends mail but
1315
+ * installs no CAPTCHA / bot check.
1316
+ *
1317
+ * Endpoints that mint accounts or trigger emails are the classic automated-abuse
1318
+ * surface: credential-stuffing sign-ups, mailbox-flooding "forgot password" loops,
1319
+ * and disposable-account farming. A server-verified human check (Turnstile) in
1320
+ * front of them is the defense. Lunora ships `verifyTurnstile()` (`@lunora/auth`)
1321
+ * and the `protectPublic({ captcha })` bundle; this lint fires when a public
1322
+ * procedure writes a user/session/account-shaped table (or references `ctx.mail`)
1323
+ * with no captcha middleware.
1324
+ *
1325
+ * Runs only when the codegen feeder supplies protection evidence
1326
+ * (`context.procedureProtections`); a runtime caller with no evidence flags
1327
+ * nothing.
1328
+ */
1329
+ declare const userCreatingMutationWithoutCaptcha: Lint;
1330
+ /**
1331
+ * A correctness lint: every `ctx.workflows.get("name")` call must reference a
1332
+ * workflow that exists — i.e. a `defineWorkflow` export in `lunora/workflows.ts`.
1333
+ * A `.get("x")` whose `"x"` resolves to no declared workflow is a typo or a
1334
+ * reference to a workflow that was removed/renamed; codegen wires the typed
1335
+ * `ctx.workflows` accessor off the declared set, so the call throws at runtime.
1336
+ * Caught here at codegen time instead.
1337
+ *
1338
+ * Calls with a non-literal name (`workflow === ""`) are skipped — they can't be
1339
+ * statically resolved, so they're neither confirmed-unknown here nor counted as
1340
+ * a typo. Only runs when both feeders supplied evidence (declared workflows and
1341
+ * discovered calls); a runtime caller flags nothing.
1342
+ */
1343
+ declare const workflowUnknownTarget: Lint;
1344
+ /**
1345
+ * Flags a declared workflow that nothing starts.
1346
+ *
1347
+ * Cross-references every `defineWorkflow` export against the set of workflow
1348
+ * names some function references via `ctx.workflows.get("&lt;name>")`. A workflow
1349
+ * with no such call is either dead code (declared, deployed as a billable
1350
+ * `WorkflowEntrypoint`, never triggered) or is started through a path the static
1351
+ * analysis can't see — the Cloudflare REST API, a `wrangler` invocation, or a
1352
+ * cross-service binding. Hence `INFO`/`INTERNAL`: a nudge to confirm intent.
1353
+ *
1354
+ * Suppressed entirely when any call uses a non-literal name
1355
+ * (`ctx.workflows.get(someVariable)`), because a dynamic dispatch could target
1356
+ * any declared workflow — flagging "unused" workflows then would be a false
1357
+ * positive. Only runs when the declaration feeder supplied evidence
1358
+ * (`context.workflows` present); a runtime caller flags nothing.
1359
+ */
1360
+ declare const workflowUnused: Lint;
1361
+ /**
1362
+ * Every lint that runs against the declared schema (and, for
1363
+ * `filter_without_index`, the discovered query reads) — no running shard
1364
+ * required. Correctness lints (`*_unknown_*`, `empty_index`) come first so a
1365
+ * broken schema's errors surface above the performance advisories.
1366
+ */
1367
+ declare const STATIC_LINTS: ReadonlyArray<Lint>;
1368
+ /**
1369
+ * Every lint that needs observed runtime signal (recorded metrics) rather than
1370
+ * just the declared schema. They read the feeder-supplied
1371
+ * {@link LintContext.shardTraffic} / {@link LintContext.tableScans} /
1372
+ * {@link LintContext.indexHits}; absent that signal (a static caller) each is a
1373
+ * no-op. Run them with `runAdvisor(ctx, { source: "runtime" })` against a live
1374
+ * deployment's aggregated metrics.
1375
+ */
1376
+ declare const RUNTIME_LINTS: ReadonlyArray<Lint>;
1377
+ /** The default lint set: the static lints, then the runtime lints. A caller filters by `source` to run one tier. */
1378
+ declare const ALL_LINTS: ReadonlyArray<Lint>;
1379
+ /** Options for {@link runAdvisor}. */
1380
+ interface RunAdvisorOptions {
1381
+ /** Lints to run (default: {@link ALL_LINTS}). */
1382
+ lints?: ReadonlyArray<Lint>;
1383
+ /** Restrict to a single evidence source — e.g. `"static"` at codegen time. */
1384
+ source?: LintSource;
1385
+ }
1386
+ /**
1387
+ * Run lints against a context and return their findings in lint-declaration
1388
+ * order. Filtering by {@link RunAdvisorOptions.source} lets a caller run only
1389
+ * `static` lints at build time and defer `runtime` lints to a live shard.
1390
+ */
1391
+ declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
1392
+ 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 AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, 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, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };