@lunora/codegen 1.0.0-alpha.11 → 1.0.0-alpha.111

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 (79) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/index.d.mts +2994 -959
  3. package/dist/index.d.ts +2994 -959
  4. package/dist/index.mjs +1 -30
  5. package/dist/packem_shared/AGENTS_FILENAME-CPHo8Se5.mjs +1 -0
  6. package/dist/packem_shared/CONTAINERS_FILENAME-Bvn6LZP6.mjs +1 -0
  7. package/dist/packem_shared/CodegenDiagnosticError-BUXlG3P-.mjs +1 -0
  8. package/dist/packem_shared/DEFAULT_TARGET-CIH5PRk1.mjs +1 -0
  9. package/dist/packem_shared/FLAGS_FILENAME-DtBWyUKq.mjs +1 -0
  10. package/dist/packem_shared/GENERATED_HEADER-GSCh67g3.mjs +1 -0
  11. package/dist/packem_shared/LUNORA_ERROR_CODES-BYg8Lpgb.mjs +1 -0
  12. package/dist/packem_shared/MUTATORS_FILENAME-CRmTpN4y.mjs +1 -0
  13. package/dist/packem_shared/NOTIFY_FILENAME-BybR3Xbd.mjs +1 -0
  14. package/dist/packem_shared/OPENRPC_VERSION-DB1PSt8W.mjs +3 -0
  15. package/dist/packem_shared/QUEUES_FILENAME-BnGLtAIu.mjs +1 -0
  16. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-BYsNQnIS.mjs +9 -0
  17. package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-DIqnj_32.mjs +2 -0
  18. package/dist/packem_shared/SDK_LANGUAGES-DcXlLVZ1.mjs +371 -0
  19. package/dist/packem_shared/SHAPES_FILENAME-DxoIrQhA.mjs +1 -0
  20. package/dist/packem_shared/SchemaSnapshotParseError-Dc1aZAph.mjs +16 -0
  21. package/dist/packem_shared/WORKFLOWS_FILENAME-B8y51Frf.mjs +1 -0
  22. package/dist/packem_shared/buildOpenApiDocument-B2mW3f5a.mjs +3 -0
  23. package/dist/packem_shared/describeErrorLevelFindings-D23VSkFP.mjs +1 -0
  24. package/dist/packem_shared/discover-ast-D33zP-NZ.mjs +1 -0
  25. package/dist/packem_shared/discover-queries-Dfj_0WJG.mjs +1 -0
  26. package/dist/packem_shared/discoverAuthApiCalls-DUjcl0SB.mjs +1 -0
  27. package/dist/packem_shared/discoverCrons-BUJO2ipU.mjs +1 -0
  28. package/dist/packem_shared/discoverFunctions-h8i51Mec.mjs +1 -0
  29. package/dist/packem_shared/discoverHttpRoutes-MX6y-iVE.mjs +1 -0
  30. package/dist/packem_shared/discoverInserts-YX8AmrzL.mjs +1 -0
  31. package/dist/packem_shared/discoverMaskProcedures-DhcK-YMG.mjs +1 -0
  32. package/dist/packem_shared/discoverMigrations-4Jjf5DJE.mjs +1 -0
  33. package/dist/packem_shared/discoverNondeterministicCalls-CjS2Mznw.mjs +1 -0
  34. package/dist/packem_shared/discoverQueries-C5S9tFoD.mjs +1 -0
  35. package/dist/packem_shared/discoverR2sqlCalls-DuTuaSMM.mjs +1 -0
  36. package/dist/packem_shared/discoverRlsMetadata-BYVq6cVp.mjs +1 -0
  37. package/dist/packem_shared/discoverSandboxUsage-L0lV17de.mjs +1 -0
  38. package/dist/packem_shared/discoverSchema-BtlNz9b_.mjs +1 -0
  39. package/dist/packem_shared/discoverStorageRulesMetadata-CKPFWHT5.mjs +1 -0
  40. package/dist/packem_shared/emit-Ba3NiOwK.mjs +2061 -0
  41. package/dist/packem_shared/emitApp-DZlIvflD.mjs +526 -0
  42. package/dist/packem_shared/formatAdvisories-BjTV22uR.mjs +2 -0
  43. package/dist/packem_shared/isTypedSchema-BBIBMZTg.mjs +2 -0
  44. package/dist/packem_shared/module-specifiers-WF8iAydP.mjs +1 -0
  45. package/dist/packem_shared/parse-validator-DtALyvid.mjs +1 -0
  46. package/dist/packem_shared/paths-C-9fwEgQ.mjs +1 -0
  47. package/dist/packem_shared/readPackageDependencies-C6h6FZuP.mjs +1 -0
  48. package/dist/packem_shared/redact-Cy14LEnc.mjs +1 -0
  49. package/dist/packem_shared/schemaFromIr-BWFnvCVg.mjs +1 -0
  50. package/package.json +12 -7
  51. package/dist/packem_shared/CONTAINERS_FILENAME-DlP6YM_Q.mjs +0 -224
  52. package/dist/packem_shared/CodegenDiagnosticError-DeblMkzO.mjs +0 -23
  53. package/dist/packem_shared/GENERATED_HEADER-r1OZrKAx.mjs +0 -2834
  54. package/dist/packem_shared/LUNORA_ERROR_CODES-CySpQPD3.mjs +0 -61
  55. package/dist/packem_shared/OPENRPC_VERSION-BYRhDy-7.mjs +0 -60
  56. package/dist/packem_shared/QUEUES_FILENAME-B5_eWCRe.mjs +0 -119
  57. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-CgJUPhI3.mjs +0 -954
  58. package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-wWGP2_5E.mjs +0 -245
  59. package/dist/packem_shared/WORKFLOWS_FILENAME-D62dcBGg.mjs +0 -84
  60. package/dist/packem_shared/buildOpenApiDocument-C8NKrvpw.mjs +0 -183
  61. package/dist/packem_shared/discover-ast-CT6BgBr4.mjs +0 -13
  62. package/dist/packem_shared/discoverAuthApiCalls-CoirYbg6.mjs +0 -62
  63. package/dist/packem_shared/discoverCrons-Cev7RRAf.mjs +0 -257
  64. package/dist/packem_shared/discoverFunctions-BWMczzBx.mjs +0 -465
  65. package/dist/packem_shared/discoverHttpRoutes-CfP6cMzt.mjs +0 -131
  66. package/dist/packem_shared/discoverInserts-C7zxXkUf.mjs +0 -61
  67. package/dist/packem_shared/discoverMaskProcedures-Cm81kwrb.mjs +0 -217
  68. package/dist/packem_shared/discoverMigrations-Bi5nJ0mJ.mjs +0 -97
  69. package/dist/packem_shared/discoverNondeterministicCalls-C4M8AXmQ.mjs +0 -122
  70. package/dist/packem_shared/discoverQueries-B0wGT-xe.mjs +0 -62
  71. package/dist/packem_shared/discoverR2sqlCalls-BVNMd428.mjs +0 -80
  72. package/dist/packem_shared/discoverRlsMetadata-BS9GOGC5.mjs +0 -280
  73. package/dist/packem_shared/discoverSchema-BnWHHJ4T.mjs +0 -822
  74. package/dist/packem_shared/discoverStorageRulesMetadata-Da8BKXcI.mjs +0 -97
  75. package/dist/packem_shared/emitApp-B1DgLrM8.mjs +0 -617
  76. package/dist/packem_shared/formatAdvisories-8NIv1k0I.mjs +0 -69
  77. package/dist/packem_shared/parse-validator-Cabb60UV.mjs +0 -133
  78. package/dist/packem_shared/paths-BRd6JHuF.mjs +0 -11
  79. package/dist/packem_shared/schemaFromIr-DTYsLBaA.mjs +0 -57
package/dist/index.d.ts CHANGED
@@ -1,14 +1,137 @@
1
- import { Finding } from '@lunora/advisor';
1
+ import { AdvisorExportSink, AdvisorGeoIndexUsage, AdvisorNotifyCall, AdvisorNotifyConfig, Finding, LintContext, AdvisorProcedureProtection } from '@lunora/advisor';
2
2
  export type { Finding } from '@lunora/advisor';
3
- import { Node, Project } from 'ts-morph';
4
- import { StudioFeaturesResult } from '@lunora/do';
3
+ import { Project, Node } from 'ts-morph';
4
+ import { LunoraError } from '@lunora/errors';
5
+ export { MESSAGE_SOLUTIONS as LUNORA_SOLUTION_RULES, type Solution as LunoraSolution, type SolutionRule as LunoraSolutionRule, findSolutionByMessage as findLunoraSolution } from '@lunora/errors';
6
+ import { StudioFeaturesResult } from '@lunora/shard-engine';
5
7
  import { Schema } from '@lunora/server';
6
8
  import { JsonSchema } from '@lunora/values';
9
+ import { LanguageName } from 'quicktype-core';
10
+ /**
11
+ * The structural schema-snapshot format and its diff, shared by `@lunora/codegen`
12
+ * (which builds snapshots from the parsed schema IR and gates deploys on the
13
+ * diff) and `@lunora/studio` (which renders the same diff as a visual migration
14
+ * history).
15
+ *
16
+ * Having ONE diff is the point: the Studio's schema-history view and the
17
+ * pre-deploy drift gate must classify a change identically, or the UI will say a
18
+ * change is safe while `lunora deploy` refuses it. `@lunora/codegen` is a
19
+ * build-time package the browser bundle cannot import, and neither package sits
20
+ * below the other, so — like `shared/quote-identifier.ts` — this lives outside
21
+ * both and is bundler-inlined into each. Keep it genuinely zero-dependency
22
+ * (relative/built-in imports only) or inlining breaks. Consumers must drop
23
+ * `outDir`/`rootDir` from their `tsconfig.json` (a set `rootDir` raises TS6059
24
+ * for this out-of-package file under `tsc --noEmit`).
25
+ *
26
+ * What stays in `@lunora/codegen`: building a snapshot from `SchemaIR`, the
27
+ * `SchemaSnapshotParseError` class (it extends `LunoraError`), and the deploy
28
+ * gate's policy (`evaluateSchemaDrift`). What lives here: the format, the diff,
29
+ * the severity classification, and the content hash.
30
+ */
31
+ /** Current snapshot format version. Bumped if the structural shape below changes. */
32
+ declare const SCHEMA_SNAPSHOT_VERSION: 1;
33
+ /** A single field's structural shape: its value kind and whether it is optional. */
34
+ interface FieldSnapshot {
35
+ /** The validator kind (`string`, `number`, `id`, `object`, …) after unwrapping `v.optional`. */
36
+ kind: string;
37
+ /** True when declared `v.optional(...)` — accepts `undefined` / absent on insert. */
38
+ optional: boolean;
39
+ }
40
+ /** A single secondary index's structural shape. */
41
+ interface IndexSnapshot {
42
+ fields: ReadonlyArray<string>;
43
+ unique: boolean;
44
+ }
45
+ /** A single relation's structural shape. */
46
+ interface RelationSnapshot {
47
+ field: string;
48
+ kind: "many" | "one";
49
+ table: string;
50
+ }
51
+ /** Structural snapshot of one table. */
52
+ interface TableSnapshot {
53
+ /** Field name → {@link FieldSnapshot}, in declared order. */
54
+ fields: Record<string, FieldSnapshot>;
55
+ /** Index name → {@link IndexSnapshot}. */
56
+ indexes: Record<string, IndexSnapshot>;
57
+ /** Relation accessor name → {@link RelationSnapshot}. */
58
+ relations: Record<string, RelationSnapshot>;
59
+ /**
60
+ * `"root"` (default single-DO), `"global"` (D1-replicated), or
61
+ * `"shardBy:<field>"` (partitioned). Encoded as a string so the snapshot
62
+ * stays a plain JSON-stable value.
63
+ */
64
+ shardMode: string;
65
+ }
66
+ /** A deterministic structural view of the whole schema at one point in time. */
67
+ interface SchemaSnapshot {
68
+ /**
69
+ * Cloudflare DO data-residency jurisdiction declared via `.jurisdiction("…")`,
70
+ * or absent. Tracked because changing it strands all existing Durable Object
71
+ * data (a DO name maps to a different ID per jurisdiction). Optional, so old
72
+ * baselines written before this field parse cleanly (absent ⇒ undefined).
73
+ *
74
+ * Typed as a plain `string` (not the authoring union) on purpose: this is
75
+ * STORED data that a newer Lunora may have written with a jurisdiction this
76
+ * version doesn't yet know. Preserving the raw value keeps the breaking
77
+ * `changedJurisdiction` diff correct under a downgrade — coercing an unknown
78
+ * value to `undefined` would fail OPEN and hide the most destructive change.
79
+ */
80
+ jurisdiction?: string;
81
+ /** Sorted list of every declared `defineMigration` id at capture time. */
82
+ migrationIds: ReadonlyArray<string>;
83
+ /** Table name → {@link TableSnapshot}, keys sorted for stable serialization. */
84
+ tables: Record<string, TableSnapshot>;
85
+ version: typeof SCHEMA_SNAPSHOT_VERSION;
86
+ }
87
+ /**
88
+ * Whether a change is anchored to one table's own shape, or to the schema as a
89
+ * whole.
90
+ *
91
+ * This is the signal a UI needs to decide which tables to mark as changed, and
92
+ * it lives HERE — next to the change union it classifies — rather than as a
93
+ * hand-maintained set of type names in the consumer. A set in the consumer gives
94
+ * zero compile-time pressure: adding a variant to `DriftChange["type"]` would
95
+ * silently render an affected table as untouched, which is exactly the
96
+ * UI-disagrees-with-the-deploy-gate divergence this module exists to prevent.
97
+ */
98
+ type DriftScope = "schema" | "table";
99
+ /** One classified structural change between two snapshots. */
100
+ interface DriftChange {
101
+ /**
102
+ * `"table"` means this table's own DDL moved (fields, indexes, shard mode) —
103
+ * a relation whose foreign key lives on the OTHER table stays `"schema"`, so
104
+ * the "changed" signal keeps meaning "this table's shape moved".
105
+ */
106
+ scope: DriftScope;
107
+ /** `"breaking"` changes need a data migration; `"safe"` changes are additive. */
108
+ severity: "breaking" | "safe";
109
+ /** Human-readable, actionable description (used in the gate message). */
110
+ summary: string;
111
+ /** The table this change belongs to. Always set when `scope` is `"table"`. */
112
+ table?: string;
113
+ /** A machine-readable change discriminator. */
114
+ type: "addedIndex" | "addedOptionalField" | "addedRelation" | "addedRequiredField" | "addedTable" | "changedFieldKind" | "changedIndex" | "changedJurisdiction" | "changedShardMode" | "fieldOptionalToRequired" | "fieldRequiredToOptional" | "removedField" | "removedIndex" | "removedRelation" | "removedTable";
115
+ }
116
+ /** The result of diffing two snapshots: every classified change. */
117
+ interface SchemaDrift {
118
+ /** Every classified change, in a stable order (added/changed per table, then removals). */
119
+ changes: ReadonlyArray<DriftChange>;
120
+ }
121
+ /** Serialize a snapshot to the exact bytes written to `lunora/.lunora-schema.json` (trailing newline). */
122
+ declare const serializeSchemaSnapshot: (snapshot: SchemaSnapshot) => string;
7
123
  /**
8
- * AST-observable subset of a column's modifier chain (`.unique()`, `.default()`,
9
- * …). Function-valued modifiers (`.$defaultFn`/`.$onUpdateFn`) can't be
10
- * serialized, so only their *presence* is recorded.
11
- */
124
+ * Diff the current snapshot against a baseline and classify every structural
125
+ * change. Pure no I/O. When `baseline` is `undefined` (no snapshot yet) there
126
+ * is no drift to report: every table is treated as a fresh additive
127
+ * `addedTable`, so a first deploy is never blocked.
128
+ */
129
+ declare const diffSchemaSnapshots: (baseline: SchemaSnapshot | undefined, current: SchemaSnapshot) => SchemaDrift;
130
+ /**
131
+ * AST-observable subset of a column's modifier chain (`.unique()`, `.default()`,
132
+ * …). Function-valued modifiers (`.$defaultFn`/`.$onUpdateFn`) can't be
133
+ * serialized, so only their *presence* is recorded.
134
+ */
12
135
  interface ColumnMetaIR {
13
136
  /** `.default(...)` or `.$defaultFn(...)` present — field is optional on insert. */
14
137
  hasDefault?: boolean;
@@ -26,13 +149,13 @@ interface ValidatorIR {
26
149
  /** Column modifiers (`.unique()`, `.default()`, `.nullable()`, …) when present. */
27
150
  column?: ColumnMetaIR;
28
151
  /**
29
- * `true` when this validator carries a `.check(...)` refinement. The predicate
30
- * is a runtime closure the AST→IR step can't represent, so the node keeps its
31
- * base `kind` but records the refinement's presence here. The AOT args-validator
32
- * compiler declines any node with this flag (compiling it would silently skip
33
- * the predicate). `.meta(...)` is pure metadata with no parse effect and does
34
- * NOT set this.
35
- */
152
+ * `true` when this validator carries a `.check(...)` refinement. The predicate
153
+ * is a runtime closure the AST→IR step can't represent, so the node keeps its
154
+ * base `kind` but records the refinement's presence here. The AOT args-validator
155
+ * compiler declines any node with this flag (compiling it would silently skip
156
+ * the predicate). `.meta(...)` is pure metadata with no parse effect and does
157
+ * NOT set this.
158
+ */
36
159
  hasRefinement?: boolean;
37
160
  /** For `v.optional(inner)` / `v.array(inner)`. */
38
161
  inner?: ValidatorIR;
@@ -50,6 +173,14 @@ interface ValidatorIR {
50
173
  sourceText?: string;
51
174
  /** For `v.id("table")` — the table name. */
52
175
  tableName?: string;
176
+ /**
177
+ * For `v.from(externalSchema)` — the wrapped Standard Schema's inferred type,
178
+ * rendered as TS source. Recovered through the type checker from
179
+ * `~standard.types.output`, mirroring the runtime's `InferStandardOutput`.
180
+ * Absent when it could not be recovered safely, in which case the emitted
181
+ * type falls back to `unknown`.
182
+ */
183
+ tsType?: string;
53
184
  valueType?: ValidatorIR;
54
185
  }
55
186
  interface IndexIR {
@@ -58,11 +189,32 @@ interface IndexIR {
58
189
  unique?: boolean;
59
190
  }
60
191
  interface SearchIndexIR {
61
- /** Primary text-search field. */
192
+ /** Primary text-search field; a dot-separated path reads a nested field. */
62
193
  field: string;
63
194
  /** Optional filter fields surfaced alongside the FTS column. */
64
195
  filterFields?: ReadonlyArray<string>;
196
+ /** Text-analysis profile (accent folding + that language's stopwords). */
197
+ language?: string;
198
+ name: string;
199
+ /** Skip the migration-time backfill of the search companion (large tables index out-of-band). */
200
+ staged?: boolean;
201
+ /** `"native"` opts into the engine's own full-text index where it has one (Postgres). */
202
+ strategy?: string;
203
+ }
204
+ /** A `.geoIndex(name, { field, precision? })` declaration — a geohash companion over a `v.geoPoint()` column. */
205
+ interface GeoIndexIR {
206
+ /** The `v.geoPoint()` column feeding the geohash. */
207
+ field: string;
65
208
  name: string;
209
+ /** Geohash precision (characters) maintained on the companion; omitted ⇒ the runtime default. */
210
+ precision?: number;
211
+ }
212
+ /** A `.ttl(field, { after? })` declaration — declarative table-level auto-expiry. */
213
+ interface TtlIR {
214
+ /** Millisecond offset added to `field` to derive the expiry (`field + after`); omitted ⇒ `field` is the absolute expiry. */
215
+ after?: number;
216
+ /** The epoch-millisecond expiry column. */
217
+ field: string;
66
218
  }
67
219
  interface VectorIndexIR {
68
220
  dimensions?: number;
@@ -76,19 +228,19 @@ interface VectorIndexIR {
76
228
  table: string;
77
229
  }
78
230
  /**
79
- * One ordering key on a rank index's `sortBy`: the column and direction.
80
- * Mirrors the runtime `RankSortKey` (defaults `direction` to `"asc"`).
81
- */
231
+ * One ordering key on a rank index's `sortBy`: the column and direction.
232
+ * Mirrors the runtime `RankSortKey` (defaults `direction` to `"asc"`).
233
+ */
82
234
  interface RankSortKeyIR {
83
235
  direction: "asc" | "desc";
84
236
  field: string;
85
237
  }
86
238
  /**
87
- * A `.rankIndex(name, { sortBy, partitionBy?, where? })` declaration. The owning
88
- * table is always the table the index is declared on, so — unlike a vector index
89
- * — there is no separate `on`/`table` reference to carry: it rides along on its
90
- * {@link TableIR}. Only the fields needed for type emission are captured.
91
- */
239
+ * A `.rankIndex(name, { sortBy, partitionBy?, where? })` declaration. The owning
240
+ * table is always the table the index is declared on, so — unlike a vector index
241
+ * — there is no separate `on`/`table` reference to carry: it rides along on its
242
+ * {@link TableIR}. Only the fields needed for type emission are captured.
243
+ */
92
244
  interface RankIndexIR {
93
245
  name: string;
94
246
  /** Columns scoping each ranking; omitted ⇒ one global rank over the table. */
@@ -109,21 +261,79 @@ interface RelationIR {
109
261
  /** Target table name. */
110
262
  table: string;
111
263
  }
264
+ /**
265
+ * Statically-discovered `.source(...)` config (plan 077). Only the bits the
266
+ * advisor lints + DO wiring need are captured; `map`/`tenantBy` are functions and
267
+ * cannot be serialized, so their presence is recorded as `hasTenantBy` rather than
268
+ * the function itself.
269
+ */
270
+ interface ExternalSourceIR {
271
+ /** The wrangler Hyperdrive binding name. */
272
+ binding: string;
273
+ /** Whether a `columns` projection allow-list was given. */
274
+ columns?: ReadonlyArray<string>;
275
+ /** `true` when a `reconcileEveryMs` was given — one of the two incremental delete-visibility paths the `external_source_incremental_no_delete_path` lint checks. */
276
+ hasReconcile?: boolean;
277
+ /** `true` when a `softDeleteColumn` was given — the other incremental delete-visibility path. */
278
+ hasSoftDelete?: boolean;
279
+ /** `true` when a `tenantBy` mapper was given — the tenant-isolation boundary the `external_source_unscoped` lint checks. */
280
+ hasTenantBy: boolean;
281
+ /** The `idColumn` literal, when given (defaults to `"id"` at runtime). */
282
+ idColumn?: string;
283
+ /** Delete-detection mode literal, when given (`"full-pull"` today). */
284
+ mode?: string;
285
+ /** The membership query literal, when statically knowable. */
286
+ query?: string;
287
+ /**
288
+ * `true` when `.source(...)` was present but its argument was **not** a static
289
+ * object literal (e.g. `.source(buildConfig())`), so none of the fields above
290
+ * could be read. The source still exists — this flag lets `hasSourcedTables`
291
+ * (codegen) and the `external_source_*` lints treat it as a source that can't be
292
+ * verified, instead of mistaking it for no `.source()` at all.
293
+ */
294
+ unanalyzable?: boolean;
295
+ }
112
296
  interface TableIR {
113
297
  /**
114
- * `true` when the table chain carried `.externallyManaged()` its rows are
115
- * written outside Lunora's discoverable insert path (adapter/migration/
116
- * middleware), so advisor insert-path lints skip it. Optional: hand-built
117
- * IR and the runtime `fromServerSchema` path default it to `false`.
118
- */
298
+ * The `defineSchemaExtension` key that contributed this table, set when it
299
+ * arrived through `defineSchema(...).extend(...)`. Absent for a table the app
300
+ * declared itself.
301
+ *
302
+ * Drives the generated `AppTableName` union: an add-on's tables
303
+ * (`ratelimit_buckets`, …) are real tables and stay in `TableName`, but an app
304
+ * enumerating "my tables" should not have to know about them.
305
+ */
306
+ extensionKey?: string;
307
+ /**
308
+ * `true` when the table chain carried `.externallyManaged()` — its rows are
309
+ * written outside Lunora's discoverable insert path (adapter/migration/
310
+ * middleware), so advisor insert-path lints skip it. Optional: hand-built
311
+ * IR and the runtime `fromServerSchema` path default it to `false`.
312
+ */
119
313
  externallyManaged?: boolean;
120
314
  /**
121
- * Storage backend for a `.global()` table: `"d1"` (default) or
122
- * `"hyperdrive"` (a Postgres/MySQL database via Cloudflare Hyperdrive). Only
123
- * meaningful when `shardMode === "global"`; absent for sharded/root tables.
124
- */
315
+ * Set when the chain carried `.source(...)` — the table is materialized from an
316
+ * external Hyperdrive-backed database by a system poll loop (plan 077). Carries
317
+ * the statically-knowable bits the advisor lints read; the functions (`map`,
318
+ * `tenantBy`) are not serialized, only their presence (`hasTenantBy`).
319
+ */
320
+ externalSource?: ExternalSourceIR;
321
+ /**
322
+ * Storage backend for a `.global()` table: `"d1"` (default) or
323
+ * `"hyperdrive"` (a Postgres/MySQL database via Cloudflare Hyperdrive). Only
324
+ * meaningful when `shardMode === "global"`; absent for sharded/root tables.
325
+ */
326
+ /** Geospatial indexes declared inline via `.geoIndex(name, …)`. Optional so hand-built IR may omit it (discovery always sets it). */
327
+ geoIndexes?: ReadonlyArray<GeoIndexIR>;
125
328
  globalBackend?: "d1" | "hyperdrive";
126
329
  indexes: ReadonlyArray<IndexIR>;
330
+ /**
331
+ * `true` when the table chain carried `.public()` — an explicit opt-OUT of
332
+ * the schema's `.rls("required")` enforcement for this one table. Optional:
333
+ * hand-built IR and tables that never called `.public()` default it to
334
+ * `false`.
335
+ */
336
+ isPublic?: boolean;
127
337
  name: string;
128
338
  /** Rank indexes declared inline via `.rankIndex(name, …)`. */
129
339
  rankIndexes: ReadonlyArray<RankIndexIR>;
@@ -139,6 +349,8 @@ interface TableIR {
139
349
  softDelete?: {
140
350
  field: string;
141
351
  };
352
+ /** Set when the chain carried `.ttl(field, { after? })` — the declarative auto-expiry policy read by the DO alarm sweep. */
353
+ ttl?: TtlIR;
142
354
  /** Vector indexes declared inline via `.vectorize()` (DSL Shape A). */
143
355
  vectorIndexes: ReadonlyArray<VectorIndexIR>;
144
356
  }
@@ -146,54 +358,98 @@ interface TableIR {
146
358
  type JurisdictionIR = "eu" | "fedramp" | "us";
147
359
  interface SchemaIR {
148
360
  /**
149
- * Cloudflare data-residency jurisdiction declared via
150
- * `defineSchema(...).jurisdiction("…")`. Emitted into the generated worker's
151
- * `createWorker({ jurisdiction })` (and `ctx.scheduler` / `ctx.containers`).
152
- * Absent ⇒ un-pinned.
153
- */
361
+ * Cloudflare data-residency jurisdiction declared via
362
+ * `defineSchema(...).jurisdiction("…")`. Emitted into the generated worker's
363
+ * `createWorker({ jurisdiction })` (and `ctx.scheduler` / `ctx.containers`).
364
+ * Absent ⇒ un-pinned.
365
+ */
154
366
  jurisdiction?: JurisdictionIR;
367
+ /**
368
+ * Set when `defineSchema(...).rls("required")` was chained onto the schema —
369
+ * every table's `ctx.db` write path is denied without an RLS-covering
370
+ * procedure unless the table itself is `.public()` (see {@link TableIR.isPublic}).
371
+ * Absent when the schema never called `.rls("required")`.
372
+ */
373
+ rlsMode?: "required";
155
374
  tables: ReadonlyArray<TableIR>;
156
375
  /** All vector indexes (inline Shape A hoisted + standalone Shape B), flattened. */
157
376
  vectorIndexes: ReadonlyArray<VectorIndexIR>;
158
377
  }
378
+ /**
379
+ * Statically-read mirror of `@lunora/server`'s `RestCacheConfig`. Every field is
380
+ * optional because discovery only records literals it could actually read off the
381
+ * `.expose({ cache: … })` object.
382
+ */
383
+ interface ExposeCacheIR {
384
+ maxAge?: number;
385
+ scope?: "private" | "public";
386
+ staleWhileRevalidate?: number;
387
+ tag?: string;
388
+ vary?: string;
389
+ }
159
390
  interface FunctionIR {
160
391
  args: Record<string, ValidatorIR>;
161
392
  exportName: string;
162
- /** Path relative to `&lt;projectRoot>/lunora/` without extension, e.g. "messages". */
393
+ /**
394
+ * Set by the `.expose({ rest: true })` builder modifier (plan 167). When
395
+ * `rest` is `true` the function is published on the public REST surface, so the
396
+ * OpenAPI emitter describes it as a real `/_lunora/rest/<namespace>/<fn>` path
397
+ * (the single source of truth the runtime router also derives from). Absent →
398
+ * RPC-only (the default; not on the REST surface).
399
+ *
400
+ * `cache` mirrors the `RestCacheConfig` the runtime turns into response
401
+ * headers, so the emitted spec can document the `Cache-Control` a caller will
402
+ * actually observe. Only statically-readable literal fields are carried; a
403
+ * computed value is simply absent (the spec under-documents rather than lies).
404
+ */
405
+ expose?: {
406
+ cache?: ExposeCacheIR;
407
+ rest?: boolean;
408
+ };
409
+ /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "messages". */
163
410
  filePath: string;
164
411
  kind: "action" | "mutation" | "query" | "stream";
165
412
  /**
166
- * Set on connection-lifecycle hooks (`onConnect`/`onDisconnect`): the socket
167
- * side the hook fires on. Such a function is also an internal mutation (so it
168
- * lands in `LUNORA_FUNCTIONS` for path dispatch); emit additionally collects
169
- * it into the `LUNORA_LIFECYCLE_HOOKS` manifest keyed by this side. Absent on
170
- * ordinary functions.
171
- */
413
+ * Set on connection-lifecycle hooks (`onConnect`/`onDisconnect`): the socket
414
+ * side the hook fires on. Such a function is also an internal mutation (so it
415
+ * lands in `LUNORA_FUNCTIONS` for path dispatch); emit additionally collects
416
+ * it into the `LUNORA_LIFECYCLE_HOOKS` manifest keyed by this side. Absent on
417
+ * ordinary functions.
418
+ */
172
419
  lifecycle?: "connect" | "disconnect";
173
420
  /**
174
- * Serialized TS source for the handler's return type, with `Promise&lt;T>`
175
- * unwrapped so callers see `T` directly. Defaults to `"unknown"` when
176
- * ts-morph cannot resolve the type (typically because the consuming
177
- * project lacks a tsconfig that can reach `@lunora/server`).
178
- */
421
+ * The `.output(validator)` declaration, when the chain has one.
422
+ *
423
+ * Takes precedence over {@link FunctionIR.returnType} (the handler's
424
+ * inferred type) for the emitted `FunctionReference`. `.output()` is what
425
+ * validates at runtime and what a reader takes as the contract, so the two
426
+ * must agree — see the emit-side note for what went wrong when they did not.
427
+ */
428
+ output?: ValidatorIR;
429
+ /**
430
+ * Serialized TS source for the handler's return type, with `Promise<T>`
431
+ * unwrapped so callers see `T` directly. Defaults to `"unknown"` when
432
+ * ts-morph cannot resolve the type (typically because the consuming
433
+ * project lacks a tsconfig that can reach `@lunora/server`).
434
+ */
179
435
  returnType: string;
180
436
  /**
181
- * Call surface the function is exposed on. Absent (or `"public"`) means it
182
- * lands in the generated `api`; `"internal"` routes it to the separate
183
- * `internal` object and is rejected by the DO's external RPC path.
184
- */
437
+ * Call surface the function is exposed on. Absent (or `"public"`) means it
438
+ * lands in the generated `api`; `"internal"` routes it to the separate
439
+ * `internal` object and is rejected by the DO's external RPC path.
440
+ */
185
441
  visibility?: "internal" | "public";
186
442
  }
187
443
  /**
188
- * A `defineMigration({...})` declaration discovered in the user's lunora
189
- * sources. The emitted `LUNORA_MIGRATIONS` registry keys on {@link MigrationIR.id}; the
190
- * import wiring needs {@link MigrationIR.exportName}/{@link MigrationIR.filePath}. {@link MigrationIR.table} is
191
- * informational (the runtime object carries the authoritative value).
192
- */
444
+ * A `defineMigration({...})` declaration discovered in the user's lunora
445
+ * sources. The emitted `LUNORA_MIGRATIONS` registry keys on {@link MigrationIR.id}; the
446
+ * import wiring needs {@link MigrationIR.exportName}/{@link MigrationIR.filePath}. {@link MigrationIR.table} is
447
+ * informational (the runtime object carries the authoritative value).
448
+ */
193
449
  interface MigrationIR {
194
450
  /** Export binding name, used to reference the module member in generated imports. */
195
451
  exportName: string;
196
- /** Path relative to `&lt;projectRoot>/lunora/` without extension, e.g. "migrations". */
452
+ /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "migrations". */
197
453
  filePath: string;
198
454
  /** Stable migration id — the registry key and per-shard run-state key. */
199
455
  id: string;
@@ -201,12 +457,109 @@ interface MigrationIR {
201
457
  table: string;
202
458
  }
203
459
  /**
204
- * A single cron job lifted from a `cronJobs()` builder in `lunora/crons.ts`.
205
- * Mirrors `@lunora/scheduler`'s `CronJob`: {@link CronJobIR.cron} is the compiled
206
- * standard cron expression, {@link CronJobIR.functionPath} is the target
207
- * `__lunoraRef` (`namespace:fn`), and {@link CronJobIR.args} is the static
208
- * argument object passed at registration.
209
- */
460
+ * A `defineShape({...})` declaration discovered in `lunora/shapes.ts`
461
+ * (local-first sync engine, Phase 7). The emitted `LUNORA_SHAPES` registry keys
462
+ * on {@link ShapeIR.exportName}; the generated DO's `resolveShape` override
463
+ * dispatches a `shape_subscribe` to the matching registered shape. Discovery is
464
+ * marker-driven (the `__lunoraShape` brand) — no field metadata is lifted here
465
+ * because the runtime object (`columns`/`compileWhere`) carries the authority.
466
+ */
467
+ interface ShapeIR {
468
+ /**
469
+ * The shape's `args` validator map — its partition selector. Lifted so
470
+ * `_generated/collections.ts` can type the selector a caller passes instead of
471
+ * widening it to `Record<string, unknown>`. `{}` for a parameterless shape.
472
+ */
473
+ args: Record<string, ValidatorIR>;
474
+ /** Export binding name — the shape's registry key and import member. */
475
+ exportName: string;
476
+ /** Path relative to `<projectRoot>/lunora/` without extension — always `"shapes"`. */
477
+ filePath: string;
478
+ /**
479
+ * The `table` string literal from the `defineShape({ table })` call, lifted
480
+ * only for static advisor lints (the runtime object stays authoritative).
481
+ * `undefined` when `table` is not a plain string literal — lints skip those.
482
+ */
483
+ table?: string;
484
+ }
485
+ /**
486
+ * The single `defineIdentity({...})` claim contract discovered in
487
+ * `lunora/identity.ts`. Discovery is **marker-driven** (the `__lunoraIdentity`
488
+ * brand, exactly like {@link ShapeIR}) — no claim metadata is lifted here
489
+ * because the emitted `_generated/server.ts` recovers the claim *type* from the
490
+ * declaration itself (`InferIdentity` over the contract's `typeof`), and the
491
+ * runtime object (`validate`/`onInvalid`) carries the authority at the boundary.
492
+ * Exactly one per app; absent ⇒ generated output is byte-identical to today.
493
+ */
494
+ interface IdentityIR {
495
+ /** Export binding name — the namespace member `_generated/server.ts` reads via `typeof`. */
496
+ exportName: string;
497
+ }
498
+ /**
499
+ * The single `defineEnv({...})` contract discovered in `lunora/env.ts`. Like
500
+ * {@link IdentityIR}, only the export binding is lifted — the emitted
501
+ * `_generated/server.ts` recovers the validated shape from the declaration
502
+ * itself (`ReturnType` over the accessor's `typeof`), and the generated ShardDO
503
+ * applies the same accessor to the worker `env` at ctx-build time to populate
504
+ * `ctx.env`. Exactly one per app; absent ⇒ generated output is byte-identical.
505
+ */
506
+ interface EnvIR {
507
+ /** Export binding name — the namespace member `_generated/server.ts` reads via `typeof`. */
508
+ exportName: string;
509
+ }
510
+ /**
511
+ * A `defineMutator({...})` declaration discovered in `lunora/mutators.ts`
512
+ * (local-first sync engine, Phase 7). The emitted registry registers the
513
+ * authoritative `server` impl into the DO's `LUNORA_FUNCTIONS` table (so
514
+ * `handleRpc` transaction-wraps it) and records its path in
515
+ * `LUNORA_MUTATOR_PATHS` so the DO's `isCustomMutator` override routes the
516
+ * client-watermark push protocol. The client `client` impl is split into the
517
+ * browser bundle separately — only the path crosses to the server side.
518
+ */
519
+ interface MutatorIR {
520
+ /**
521
+ * The mutator's `args` validator map, parsed exactly as a procedure's is, so
522
+ * the emitted `api.mutators.<name>` reference carries the arg type a client
523
+ * `defineMutator` infers instead of restating. `{}` for a parameterless
524
+ * mutator (or one whose `args` isn't an inline object literal).
525
+ */
526
+ args: Record<string, ValidatorIR>;
527
+ /** Export binding name — the mutator's registry key and import member. */
528
+ exportName: string;
529
+ /** Path relative to `<projectRoot>/lunora/` without extension — always `"mutators"`. */
530
+ filePath: string;
531
+ /**
532
+ * Serialized TS source for the authoritative `server` impl's return type,
533
+ * `Promise<T>` unwrapped. `"unknown"` when ts-morph can't resolve it — same
534
+ * contract as {@link FunctionIR.returnType}.
535
+ */
536
+ returnType: string;
537
+ }
538
+ /**
539
+ * A whole-row `ctx.db.replace(id, document)` write discovered inside a custom
540
+ * mutator's inline `server` impl (`lunora/mutators.ts`) — the input the
541
+ * `mutator_full_row_replace` advisor lint consumes. A `replace` overwrites the
542
+ * entire row, so a concurrent edit to a different column on a synced table is
543
+ * clobbered; `ctx.db.patch(id, { field })` merges at the column level instead.
544
+ * Structurally identical to `AdvisorMutatorWrite` so it passes straight through
545
+ * to the advisor without conversion, exactly as `InsertWriteIR` does for
546
+ * `AdvisorInsertWrite`.
547
+ */
548
+ interface MutatorWriteIR {
549
+ /** The mutator export whose `server` impl performs the replace, e.g. `renameChannel`. */
550
+ exportName: string;
551
+ /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
552
+ file: string;
553
+ /** 1-based line of the `replace(...)` call. */
554
+ line: number;
555
+ }
556
+ /**
557
+ * A single cron job lifted from a `cronJobs()` builder in `lunora/crons.ts`.
558
+ * Mirrors `@lunora/scheduler`'s `CronJob`: {@link CronJobIR.cron} is the compiled
559
+ * standard cron expression, {@link CronJobIR.functionPath} is the target
560
+ * `__lunoraRef` (`namespace:fn`), and {@link CronJobIR.args} is the static
561
+ * argument object passed at registration.
562
+ */
210
563
  interface CronJobIR {
211
564
  /** Static args object (source-text JSON), defaults to `{}`. */
212
565
  args: Record<string, unknown>;
@@ -217,25 +570,25 @@ interface CronJobIR {
217
570
  /** Unique, human-readable job name. */
218
571
  name: string;
219
572
  /**
220
- * Set when the job targets a durable workflow (a `lunora/workflows.ts`
221
- * export) instead of a function: the workflow's `WORKFLOW_*` binding name
222
- * plus its export name. On each fire the worker starts a new workflow
223
- * INSTANCE (the {@link CronJobIR.args} become its `params`) rather than
224
- * dispatching a one-shot function.
225
- */
573
+ * Set when the job targets a durable workflow (a `lunora/workflows.ts`
574
+ * export) instead of a function: the workflow's `WORKFLOW_*` binding name
575
+ * plus its export name. On each fire the worker starts a new workflow
576
+ * INSTANCE (the {@link CronJobIR.args} become its `params`) rather than
577
+ * dispatching a one-shot function.
578
+ */
226
579
  workflow?: {
227
580
  binding: string;
228
581
  exportName: string;
229
582
  };
230
583
  }
231
584
  /**
232
- * A container lifted from a `defineContainer()` export in
233
- * `lunora/containers.ts`. Carries everything the emitters and the config layer
234
- * need to wire wrangler (`containers[]` + the Durable Object binding +
235
- * migration class) and the generated `_generated/containers.ts` DO class.
236
- * Names are derived via `@lunora/container`'s shared helpers so codegen and
237
- * the config layer can never disagree.
238
- */
585
+ * A container lifted from a `defineContainer()` export in
586
+ * `lunora/containers.ts`. Carries everything the emitters and the config layer
587
+ * need to wire wrangler (`containers[]` + the Durable Object binding +
588
+ * migration class) and the generated `_generated/containers.ts` DO class.
589
+ * Names are derived via `@lunora/container`'s shared helpers so codegen and
590
+ * the config layer can never disagree.
591
+ */
239
592
  interface ContainerIR {
240
593
  /** Durable Object binding name, e.g. `CONTAINER_TRANSCODER`. */
241
594
  bindingName: string;
@@ -244,18 +597,18 @@ interface ContainerIR {
244
597
  /** Generated DO class name, e.g. `TranscoderContainer`. */
245
598
  className: string;
246
599
  /**
247
- * Whether the container may open outbound internet connections, when the
248
- * value was a static literal. `undefined` means the field was omitted (the
249
- * platform default is `true`) or wasn't a literal. Lifted for the advisor.
250
- */
600
+ * Whether the container may open outbound internet connections, when the
601
+ * value was a static literal. `undefined` means the field was omitted (the
602
+ * platform default is `true`) or wasn't a literal. Lifted for the advisor.
603
+ */
251
604
  enableInternet?: boolean;
252
605
  /** The `lunora/containers.ts` export name, e.g. `transcoder`. */
253
606
  exportName: string;
254
607
  /**
255
- * Normalized image source: a local Dockerfile (`dockerfile`), a pre-built
256
- * registry reference (`registry`), or a Railpack source directory (`build`)
257
- * that the deploy step builds and pushes before wrangler runs.
258
- */
608
+ * Normalized image source: a local Dockerfile (`dockerfile`), a pre-built
609
+ * registry reference (`registry`), or a Railpack source directory (`build`)
610
+ * that the deploy step builds and pushes before wrangler runs.
611
+ */
259
612
  image: {
260
613
  buildContext: string;
261
614
  dockerfilePath: string;
@@ -283,20 +636,20 @@ interface ContainerIR {
283
636
  stepPercentage?: number;
284
637
  };
285
638
  /**
286
- * The static `sleepAfter` value, when it was a literal. `undefined` means
287
- * omitted (platform default `"10m"`) or non-literal. Lifted for the advisor.
288
- */
639
+ * The static `sleepAfter` value, when it was a literal. `undefined` means
640
+ * omitted (platform default `"10m"`) or non-literal. Lifted for the advisor.
641
+ */
289
642
  sleepAfter?: number | string;
290
643
  }
291
644
  /**
292
- * A workflow lifted from a `defineWorkflow()` export in `lunora/workflows.ts`.
293
- * Carries what the emitters and the config layer need to wire the wrangler
294
- * `workflows[]` entry and the generated `_generated/workflows.ts`
295
- * `WorkflowEntrypoint` class. Unlike containers, workflows are NOT Durable
296
- * Objects — wrangler gets only a `workflows[]` entry, never a `durable_objects`
297
- * binding or a migration class. Names are derived via `@lunora/workflow`'s
298
- * shared helpers so codegen and the config layer can never disagree.
299
- */
645
+ * A workflow lifted from a `defineWorkflow()` export in `lunora/workflows.ts`.
646
+ * Carries what the emitters and the config layer need to wire the wrangler
647
+ * `workflows[]` entry and the generated `_generated/workflows.ts`
648
+ * `WorkflowEntrypoint` class. Unlike containers, workflows are NOT Durable
649
+ * Objects — wrangler gets only a `workflows[]` entry, never a `durable_objects`
650
+ * binding or a migration class. Names are derived via `@lunora/workflow`'s
651
+ * shared helpers so codegen and the config layer can never disagree.
652
+ */
300
653
  interface WorkflowIR {
301
654
  /** The Cloudflare `Workflow` binding name, e.g. `WORKFLOW_ORDER_PIPELINE`. */
302
655
  bindingName: string;
@@ -305,21 +658,97 @@ interface WorkflowIR {
305
658
  /** The `lunora/workflows.ts` export name, e.g. `orderPipeline`. */
306
659
  exportName: string;
307
660
  /**
308
- * The stable wrangler `workflows[].name`. Defaults to the kebab-cased export
309
- * name (`orderPipeline` → `order-pipeline`); a static `name:` literal in the
310
- * definition overrides it.
311
- */
661
+ * The stable wrangler `workflows[].name`. Defaults to the kebab-cased export
662
+ * name (`orderPipeline` → `order-pipeline`); a static `name:` literal in the
663
+ * definition overrides it.
664
+ */
665
+ name: string;
666
+ /**
667
+ * Durable step labels lifted from the handler body — the first string-literal
668
+ * argument of every `ctx.step.do` / `.sleep` / `.sleepUntil` / `.waitForEvent`
669
+ * call. Feeds the duplicate-step-name lint, which flags a name used twice
670
+ * (Cloudflare memoizes by name, so the second call silently returns the
671
+ * first's cached result). Calls with a non-literal name are omitted (not
672
+ * statically comparable).
673
+ */
674
+ steps: ReadonlyArray<WorkflowStepIR>;
675
+ }
676
+ /**
677
+ * An agent lifted from a `defineAgent()` export in `lunora/agents.ts`. A
678
+ * `defineAgent` compiles its durable tool-loop onto a Cloudflare Workflow, so —
679
+ * like {@link WorkflowIR} — an agent is NOT a Durable Object: wrangler gets only
680
+ * a `workflows[]` entry, never a `durable_objects` binding or a migration class.
681
+ * Carries what the emitters and the config layer need to wire the generated
682
+ * agent `WorkflowEntrypoint` class (e.g. `SupportAgentWorkflow`), the typed
683
+ * per-agent `ctx.agents` producer, and the reconciled wrangler `workflows[]`
684
+ * entry. Names are derived via `@lunora/agent`'s shared helpers so codegen and
685
+ * the config layer can never disagree.
686
+ */
687
+ interface AgentIR {
688
+ /** The Cloudflare `Workflow` binding name, e.g. `AGENT_SUPPORT`. */
689
+ bindingName: string;
690
+ /** Generated `WorkflowEntrypoint` class name, e.g. `SupportAgentWorkflow`. */
691
+ className: string;
692
+ /** The `lunora/agents.ts` export name, e.g. `support`. */
693
+ exportName: string;
694
+ /**
695
+ * The stable wrangler `workflows[].name`. Defaults to the kebab-cased export
696
+ * name (`support` → `agent-support`); a static `name:` literal in the
697
+ * definition overrides it.
698
+ */
699
+ name: string;
700
+ /**
701
+ * Whether the definition declares an `onEmail` mapper on
702
+ * `defineAgent({ onEmail: … })`. When `true` the emitter wires this agent
703
+ * onto the worker's top-level `email()` handler (via `@lunora/agent/inbound`)
704
+ * so inbound mail starts a durable run. Detected by AST PRESENCE — the
705
+ * closure is never evaluated — and written to IR only when present, so
706
+ * email-free agents (and agent-free projects) stay byte-identical.
707
+ */
708
+ onEmail?: boolean;
709
+ /**
710
+ * Whether the definition opted into public run-starts via
711
+ * `defineAgent({ publicRun: true })` — emitted into the `ctx.agents` wiring
712
+ * spec so the public `agents:agentRun` mutation can gate on it fail-closed.
713
+ * Absent (falsy) means server-side starts only; the field is written to IR
714
+ * only when the literal is `true`, so agent-free and non-opted-in output is
715
+ * byte-identical.
716
+ */
717
+ publicRun?: boolean;
718
+ /**
719
+ * Whether the definition opted into a real-time voice session via a `voice`
720
+ * block on `defineAgent({ voice: … })`. Unlike the durable loop (a Workflow),
721
+ * the voice path IS a Durable Object — so when this is `true` the emitter
722
+ * generates the `voiceClassName` `VoiceSessionDO` subclass and the
723
+ * `api.agents.{name}Voice` client reference, and the config layer reconciles
724
+ * a `durable_objects` binding (`voiceBindingName`) + `new_sqlite_classes`
725
+ * migration. Written to IR only when the literal is present, so voice-free
726
+ * agents (and agent-free projects) stay byte-identical.
727
+ */
728
+ voice?: boolean;
729
+ /** The voice DO's Cloudflare `DurableObjectNamespace` binding name, e.g. `VOICE_SUPPORT`. Present only when `voice`. */
730
+ voiceBindingName?: string;
731
+ /** Generated `VoiceSessionDO` subclass name, e.g. `SupportVoiceDO`. Present only when `voice`. */
732
+ voiceClassName?: string;
733
+ }
734
+ /** One durable step call lifted from a workflow handler body (the use side of {@link WorkflowIR.steps}). */
735
+ interface WorkflowStepIR {
736
+ /** 1-based line of the durable step call. */
737
+ line: number;
738
+ /** The native step method invoked: `do` / `sleep` / `sleepUntil` / `waitForEvent`. */
739
+ method: string;
740
+ /** The step's static label (the first string-literal argument). */
312
741
  name: string;
313
742
  }
314
743
  /**
315
- * A queue lifted from a `defineQueue()` export in `lunora/queues.ts`. Carries
316
- * what the emitters and the config layer need to wire the typed `ctx.queues`
317
- * producer, the generated worker `queue()` dispatch, and the wrangler
318
- * `queues.producers[]` / `queues.consumers[]` entries. Like workflows, a queue
319
- * is NOT a Durable Object — wrangler gets only `queues.*` entries. Names are
320
- * derived via `@lunora/queue`'s shared helpers so codegen and the config layer
321
- * can never disagree.
322
- */
744
+ * A queue lifted from a `defineQueue()` export in `lunora/queues.ts`. Carries
745
+ * what the emitters and the config layer need to wire the typed `ctx.queues`
746
+ * producer, the generated worker `queue()` dispatch, and the wrangler
747
+ * `queues.producers[]` / `queues.consumers[]` entries. Like workflows, a queue
748
+ * is NOT a Durable Object — wrangler gets only `queues.*` entries. Names are
749
+ * derived via `@lunora/queue`'s shared helpers so codegen and the config layer
750
+ * can never disagree.
751
+ */
323
752
  interface QueueIR {
324
753
  /** The Cloudflare `Queue` producer binding name, e.g. `QUEUE_EMAIL`. */
325
754
  bindingName: string;
@@ -328,10 +757,10 @@ interface QueueIR {
328
757
  /** How the queue is consumed: `"push"` (a worker `queue()` handler) or `"pull"` (external HTTP). */
329
758
  mode: "pull" | "push";
330
759
  /**
331
- * The stable wrangler queue name (`queues.producers[].queue`). Defaults to
332
- * the kebab-cased export name (`emailQueue` → `email-queue`); a static
333
- * `name:` literal in the definition overrides it.
334
- */
760
+ * The stable wrangler queue name (`queues.producers[].queue`). Defaults to
761
+ * the kebab-cased export name (`emailQueue` → `email-queue`); a static
762
+ * `name:` literal in the definition overrides it.
763
+ */
335
764
  name: string;
336
765
  /** Push-consumer batch/retry tuning, mirrored onto the wrangler `queues.consumers[]` entry. */
337
766
  tuning: {
@@ -343,18 +772,42 @@ interface QueueIR {
343
772
  };
344
773
  }
345
774
  /**
346
- * A `ctx.workflows.get("name")…` call discovered in a function body the
347
- * use-site analog of {@link WorkflowIR} (which is the declaration side). Feeds
348
- * the `workflow_unused` lint (a declared workflow with zero call sites) and the
349
- * `workflow_unknown_target` lint (a `.get("x")` whose `x` isn't declared — a
350
- * typo catcher). {@link WorkflowCallIR.workflow} is `""` when the `get(...)`
351
- * argument is not a string literal (a dynamic name — which suppresses the
352
- * unused-workflow heuristic rather than producing a false positive).
353
- */
775
+ * The feature-flag provider declared by the default export of `lunora/flags.ts`
776
+ * (`defineFlags({ provider, })`). Discovery is **metadata-only** codegen
777
+ * imports the real module at runtime for the provider value; this IR exists so
778
+ * the config layer can reconcile/validate the wrangler `flagship` binding when
779
+ * the app uses Flagship in binding mode. A `custom` provider (any other
780
+ * OpenFeature factory) carries no binding to reconcile.
781
+ */
782
+ interface FlagsIR {
783
+ /**
784
+ * The wrangler `flagship[].binding` name — set **only** for a flagship
785
+ * `provider` in binding mode (`flagshipProvider({ binding: "FLAGS" })`). The
786
+ * config layer hints/validates a matching `flagship` binding from this.
787
+ */
788
+ bindingName?: string;
789
+ /**
790
+ * Flagship operating mode — `"binding"` (wrangler binding, needs a
791
+ * `flagship` entry) or `"http"` (no binding); `undefined` for a `custom`
792
+ * provider or when the mode can't be read statically.
793
+ */
794
+ mode?: "binding" | "http";
795
+ /** `"flagship"` when the provider is `flagshipProvider(...)`, else `"custom"` (any other OpenFeature provider factory). */
796
+ provider: "custom" | "flagship";
797
+ }
798
+ /**
799
+ * A `ctx.workflows.get("name")…` call discovered in a function body — the
800
+ * use-site analog of {@link WorkflowIR} (which is the declaration side). Feeds
801
+ * the `workflow_unused` lint (a declared workflow with zero call sites) and the
802
+ * `workflow_unknown_target` lint (a `.get("x")` whose `x` isn't declared — a
803
+ * typo catcher). {@link WorkflowCallIR.workflow} is `""` when the `get(...)`
804
+ * argument is not a string literal (a dynamic name — which suppresses the
805
+ * unused-workflow heuristic rather than producing a false positive).
806
+ */
354
807
  interface WorkflowCallIR {
355
808
  /** Export binding name of the function performing the call, e.g. `create`. */
356
809
  exportName: string;
357
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension (the api namespace). */
810
+ /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
358
811
  file: string;
359
812
  /** 1-based line of the `get(...)` call. */
360
813
  line: number;
@@ -362,14 +815,24 @@ interface WorkflowCallIR {
362
815
  workflow: string;
363
816
  }
364
817
  /**
365
- * A `ctx.db.query("table")…` read discovered in a function body, reduced to what
366
- * the `filter_without_index` advisor lint needs: which table, whether the chain
367
- * narrows with an index, and whether it filters. `table` is `""` when the
368
- * `query(...)` argument is not a string literal (a dynamic table — not lintable).
369
- */
818
+ * A `ctx.db.query("table")…` read discovered in a function body, reduced to what
819
+ * the query advisor lints need: which table, whether the chain narrows with an
820
+ * index, whether it filters, and which terminal materializes the result.
821
+ * `table` is `""` when the `query(...)` argument is not a string literal (a
822
+ * dynamic table — not lintable).
823
+ */
370
824
  interface QueryReadIR {
371
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
825
+ /** Exported procedure the read sits in, or `""` at module scope. */
826
+ exportName: string;
827
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
372
828
  file: string;
829
+ /**
830
+ * True when the chain's `.filter()` predicate compares `_id`
831
+ * (`(d) => d._id === args.id`) — a full scan for a row that `ctx.db.get`
832
+ * addresses directly. Optional so a feeder predating this field still
833
+ * typechecks; absent is treated as "not a primary-key filter".
834
+ */
835
+ filtersPrimaryKey?: boolean;
373
836
  /** The chain calls `.filter(...)`. */
374
837
  hasFilter: boolean;
375
838
  /** The chain narrows with `.withIndex(...)` or `.withSearchIndex(...)`. */
@@ -378,18 +841,30 @@ interface QueryReadIR {
378
841
  line: number;
379
842
  /** Queried table name, or `""` when the argument is not a string literal. */
380
843
  table: string;
844
+ /**
845
+ * The materializing call the chain ends in — `"collect"`, `"take"`,
846
+ * `"paginate"`, `"first"`, `"unique"`, … — i.e. how much of the narrowed set
847
+ * the read actually loads.
848
+ *
849
+ * `undefined` when the chain reaches no recognised terminal (a reader passed
850
+ * on, a bare `query(...)`) AND when a feeder predating this field produced
851
+ * the read. The two are deliberately not distinguished: no consumer could act
852
+ * on the difference, so the terminal-shaped lints skip the read either way
853
+ * rather than guessing a terminal.
854
+ */
855
+ terminal?: string;
381
856
  }
382
857
  /**
383
- * A `ctx.authApi.&lt;method>(...)` call discovered in a function body, attributed
384
- * to the exported function (and its file = api namespace) that performs it.
385
- * Structurally identical to `AdvisorAuthApiCall` so it passes straight through
386
- * to the advisor lint without conversion, exactly as `InsertWriteIR` does for
387
- * `AdvisorInsertWrite`.
388
- */
858
+ * A `ctx.authApi.<method>(...)` call discovered in a function body, attributed
859
+ * to the exported function (and its file = api namespace) that performs it.
860
+ * Structurally identical to `AdvisorAuthApiCall` so it passes straight through
861
+ * to the advisor lint without conversion, exactly as `InsertWriteIR` does for
862
+ * `AdvisorInsertWrite`.
863
+ */
389
864
  interface AuthApiCallIR {
390
865
  /** Export binding name of the function performing the call, e.g. "createOrg". */
391
866
  exportName: string;
392
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
867
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
393
868
  file: string;
394
869
  /** True when the call's argument object includes a `headers` property. */
395
870
  hasHeaders: boolean;
@@ -399,16 +874,16 @@ interface AuthApiCallIR {
399
874
  method: string;
400
875
  }
401
876
  /**
402
- * A `ctx.db.insert("table", …)` write discovered in a function body, attributed
403
- * to the exported function (and its file = api namespace) that performs it — the
404
- * write-side analog of {@link QueryReadIR}. Lets tooling wire a table's write
405
- * action by behavior (which function inserts into it) rather than by naming.
406
- * {@link InsertWriteIR.table} is `""` when the argument is not a string literal.
407
- */
877
+ * A `ctx.db.insert("table", …)` write discovered in a function body, attributed
878
+ * to the exported function (and its file = api namespace) that performs it — the
879
+ * write-side analog of {@link QueryReadIR}. Lets tooling wire a table's write
880
+ * action by behavior (which function inserts into it) rather than by naming.
881
+ * {@link InsertWriteIR.table} is `""` when the argument is not a string literal.
882
+ */
408
883
  interface InsertWriteIR {
409
884
  /** Export binding name of the function performing the insert, e.g. "send". */
410
885
  exportName: string;
411
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension (the api namespace). */
886
+ /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
412
887
  file: string;
413
888
  /** 1-based line of the `insert(...)` call. */
414
889
  line: number;
@@ -416,20 +891,20 @@ interface InsertWriteIR {
416
891
  table: string;
417
892
  }
418
893
  /**
419
- * A non-deterministic API call (`Date.now`, `Math.random`, `crypto.randomUUID`,
420
- * `crypto.getRandomValues`, `fetch`) discovered lexically inside a `query(...)`
421
- * or `mutation(...)` handler body — the `nondeterministic_query_mutation` lint
422
- * input. Structurally identical to `AdvisorNondeterministicCall` so values pass
423
- * straight through to the advisor without conversion, exactly as `AuthApiCallIR`
424
- * does for `AdvisorAuthApiCall`. `action(...)` handlers are never recorded —
425
- * actions are the determinism escape hatch.
426
- */
894
+ * A non-deterministic API call (`Date.now`, `Math.random`, `crypto.randomUUID`,
895
+ * `crypto.getRandomValues`, `fetch`) discovered lexically inside a `query(...)`
896
+ * or `mutation(...)` handler body — the `nondeterministic_query_mutation` lint
897
+ * input. Structurally identical to `AdvisorNondeterministicCall` so values pass
898
+ * straight through to the advisor without conversion, exactly as `AuthApiCallIR`
899
+ * does for `AdvisorAuthApiCall`. `action(...)` handlers are never recorded —
900
+ * actions are the determinism escape hatch.
901
+ */
427
902
  interface NondeterministicCallIR {
428
903
  /** The non-deterministic API invoked, e.g. `Date.now` / `Math.random` / `crypto.randomUUID` / `fetch`. */
429
904
  callee: string;
430
905
  /** Export binding name of the function performing the call, e.g. `sendMessage`. */
431
906
  exportName: string;
432
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension (the api namespace). */
907
+ /** Source file relative to `<projectRoot>/lunora/`, without extension (the api namespace). */
433
908
  file: string;
434
909
  /** Which procedure kind the call lives in — only `query`/`mutation` handlers are recorded. */
435
910
  kind: "mutation" | "query";
@@ -437,13 +912,13 @@ interface NondeterministicCallIR {
437
912
  line: number;
438
913
  }
439
914
  /**
440
- * One `ctx.r2sql` access lexically inside a `query`/`mutation` handler — the
441
- * `r2sql_outside_action` advisor lint input. Structurally identical to the
442
- * advisor's `AdvisorR2sqlCall` (same field set) so values pass straight through
443
- * `lintSchema` without conversion, exactly as `NondeterministicCallIR` does.
444
- * Only `query`/`mutation` handlers are recorded; `action(...)` is the intended
445
- * home for `ctx.r2sql` and is skipped.
446
- */
915
+ * One `ctx.r2sql` access lexically inside a `query`/`mutation` handler — the
916
+ * `r2sql_outside_action` advisor lint input. Structurally identical to the
917
+ * advisor's `AdvisorR2sqlCall` (same field set) so values pass straight through
918
+ * `lintSchema` without conversion, exactly as `NondeterministicCallIR` does.
919
+ * Only `query`/`mutation` handlers are recorded; `action(...)` is the intended
920
+ * home for `ctx.r2sql` and is skipped.
921
+ */
447
922
  interface R2sqlCallIR {
448
923
  /** The accessed `ctx.r2sql` surface, e.g. `ctx.r2sql.query` / `ctx.r2sql.from`. */
449
924
  callee: string;
@@ -457,21 +932,21 @@ interface R2sqlCallIR {
457
932
  line: number;
458
933
  }
459
934
  /**
460
- * Per-procedure RLS usage snapshot, produced by `discoverRlsProcedures` for the
461
- * `rls_uncovered_table` advisor lint. Structurally identical to
462
- * `AdvisorRlsProcedure` (they share the same field set) so values pass straight
463
- * through to the advisor without conversion, exactly as `AuthApiCallIR` does for
464
- * `AdvisorAuthApiCall`.
465
- */
935
+ * Per-procedure RLS usage snapshot, produced by `discoverRlsProcedures` for the
936
+ * `rls_uncovered_table` advisor lint. Structurally identical to
937
+ * `AdvisorRlsProcedure` (they share the same field set) so values pass straight
938
+ * through to the advisor without conversion, exactly as `AuthApiCallIR` does for
939
+ * `AdvisorAuthApiCall`.
940
+ */
466
941
  interface RlsProcedureIR {
467
942
  /** Export binding name of the procedure (e.g. `listDocuments`). */
468
943
  exportName: string;
469
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
944
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
470
945
  file: string;
471
946
  /**
472
- * Table names extracted from the `rls(policies)` array literal. Empty when the
473
- * policies argument is not a statically-readable array literal.
474
- */
947
+ * Table names extracted from the `rls(policies)` array literal. Empty when the
948
+ * policies argument is not a statically-readable array literal.
949
+ */
475
950
  rlsTables: string[];
476
951
  /** Tables read by the procedure via `ctx.db.query/findMany/findFirst/…`. */
477
952
  tablesRead: string[];
@@ -483,22 +958,22 @@ interface RlsProcedureIR {
483
958
  visibility: "internal" | "public";
484
959
  }
485
960
  /**
486
- * One procedure reduced to the facts the `mask_uncovered_pii_column` lint needs:
487
- * whether its builder chain includes `.use(mask(...))`, which `(table, column)`
488
- * pairs that mask declares, and which tables the procedure reads/writes. The
489
- * column-level analogue of {@link RlsProcedureIR}. Structurally identical to
490
- * `AdvisorMaskProcedure` so values pass straight through without conversion.
491
- */
961
+ * One procedure reduced to the facts the `mask_uncovered_pii_column` lint needs:
962
+ * whether its builder chain includes `.use(mask(...))`, which `(table, column)`
963
+ * pairs that mask declares, and which tables the procedure reads/writes. The
964
+ * column-level analogue of {@link RlsProcedureIR}. Structurally identical to
965
+ * `AdvisorMaskProcedure` so values pass straight through without conversion.
966
+ */
492
967
  interface MaskProcedureIR {
493
968
  /** Export binding name of the procedure (e.g. `listUsers`). */
494
969
  exportName: string;
495
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
970
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
496
971
  file: string;
497
972
  /**
498
- * `(table, column)` pairs this procedure's `mask(policies)` object literal
499
- * declares. Empty when the policies argument is not a statically-readable
500
- * object literal (conservative: `usesMask` is still `true`).
501
- */
973
+ * `(table, column)` pairs this procedure's `mask(policies)` object literal
974
+ * declares. Empty when the policies argument is not a statically-readable
975
+ * object literal (conservative: `usesMask` is still `true`).
976
+ */
502
977
  maskColumns: {
503
978
  column: string;
504
979
  table: string;
@@ -513,14 +988,14 @@ interface MaskProcedureIR {
513
988
  visibility: "internal" | "public";
514
989
  }
515
990
  /**
516
- * One masked column surfaced to the studio's data-browser mask preview: a
517
- * `(table, column)` pair plus the declared {@link MaskProcedureIR} strategy so
518
- * the preview can pick redact-vs-hash-vs-custom rendering. Aggregated across the
519
- * project's `.use(mask(...))` chains by `discoverMaskMetadata`; the descriptive
520
- * twin of {@link RlsPolicyIR}. `"custom"` covers any non-string strategy (a
521
- * `(value, ctx) => …` function) — its logic is an opaque closure, never read by
522
- * the UI; the preview renders a fixed sentinel for it.
523
- */
991
+ * One masked column surfaced to the studio's data-browser mask preview: a
992
+ * `(table, column)` pair plus the declared {@link MaskProcedureIR} strategy so
993
+ * the preview can pick redact-vs-hash-vs-custom rendering. Aggregated across the
994
+ * project's `.use(mask(...))` chains by `discoverMaskMetadata`; the descriptive
995
+ * twin of {@link RlsPolicyIR}. `"custom"` covers any non-string strategy (a
996
+ * `(value, ctx) => …` function) — its logic is an opaque closure, never read by
997
+ * the UI; the preview renders a fixed sentinel for it.
998
+ */
524
999
  interface MaskColumnMetadataIR {
525
1000
  /** Column the mask policy redacts. */
526
1001
  column: string;
@@ -530,24 +1005,48 @@ interface MaskColumnMetadataIR {
530
1005
  table: string;
531
1006
  }
532
1007
  /**
533
- * Schema-wide masking metadata the codegen emits into the generated ShardDO so
534
- * the studio's data-browser mask toggle can preview what a non-privileged caller
535
- * would see. Aggregated across every `.use(mask(...))` chain in the project —
536
- * purely descriptive (table + column + strategy), never the masking closure. The
537
- * column-level analogue of {@link RlsMetadataIR}.
538
- */
1008
+ * Schema-wide masking metadata the codegen emits into the generated ShardDO so
1009
+ * the studio's data-browser mask toggle can preview what a non-privileged caller
1010
+ * would see. Aggregated across every `.use(mask(...))` chain in the project —
1011
+ * purely descriptive (table + column + strategy), never the masking closure. The
1012
+ * column-level analogue of {@link RlsMetadataIR}.
1013
+ */
539
1014
  interface MaskMetadataIR {
540
1015
  /** Every statically-discovered masked column, deduped by `(table, column)` (first declaration wins). */
541
1016
  columns: MaskColumnMetadataIR[];
542
1017
  }
543
1018
  /**
544
- * One statically-readable policy entry from an `rls([...])` array literal,
545
- * surfaced to the studio's read-only RLS inspector via the generated
546
- * `rlsPolicies()` hook. Captures the policy's `table` + `on` operation and the
547
- * procedure it guards never the `when` predicate, which is an opaque JS
548
- * closure (its logic stays in code, not the UI). Produced by
549
- * `discoverRlsProcedures` alongside the lint IR.
550
- */
1019
+ * One masked column whose `mask(policies)` strategy is a statically-known
1020
+ * literal (`"hash"` or `"redact"`) the `mask_weak_hash_strategy_on_pii` lint
1021
+ * input. Unlike {@link MaskColumnMetadataIR} (app-wide, deduped by `(table,
1022
+ * column)`, studio-preview evidence), this is per declaration site (file + line
1023
+ * + enclosing export), undeduped, so the lint can point at the exact
1024
+ * `mask(...)` call that applies a weak strategy. A `MaskFn` (custom, non-literal)
1025
+ * strategy carries no lint-relevant signal and is never recorded here.
1026
+ * Structurally identical to `AdvisorMaskStrategy`.
1027
+ */
1028
+ interface MaskStrategyIR {
1029
+ /** Masked column name. */
1030
+ column: string;
1031
+ /** Export binding name of the procedure whose `.use(mask(...))` chain declared this column, or `"<module>"` when declared at file scope. */
1032
+ exportName: string;
1033
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1034
+ file: string;
1035
+ /** 1-based line of the masked column's strategy property. */
1036
+ line: number;
1037
+ /** The statically-known strategy literal: `"hash"` or `"redact"`. */
1038
+ strategy: string;
1039
+ /** Logical table the masked column belongs to. */
1040
+ table: string;
1041
+ }
1042
+ /**
1043
+ * One statically-readable policy entry from an `rls([...])` array literal,
1044
+ * surfaced to the studio's read-only RLS inspector via the generated
1045
+ * `rlsPolicies()` hook. Captures the policy's `table` + `on` operation and the
1046
+ * procedure it guards — never the `when` predicate, which is an opaque JS
1047
+ * closure (its logic stays in code, not the UI). Produced by
1048
+ * `discoverRlsProcedures` alongside the lint IR.
1049
+ */
551
1050
  interface RlsPolicyIR {
552
1051
  /** Source file (relative to `lunora/`, without extension) the policy is declared in. */
553
1052
  file: string;
@@ -559,12 +1058,12 @@ interface RlsPolicyIR {
559
1058
  table: string;
560
1059
  }
561
1060
  /**
562
- * One statically-readable role entry from an `rls(policies, { roles: [...] })`
563
- * call, surfaced to the studio's RLS inspector. Captures the role's `name`,
564
- * optional `description`, and the names of the permissions it grants (string
565
- * literals or `definePermission("name")` calls). Produced by
566
- * `discoverRlsProcedures`.
567
- */
1061
+ * One statically-readable role entry from an `rls(policies, { roles: [...] })`
1062
+ * call, surfaced to the studio's RLS inspector. Captures the role's `name`,
1063
+ * optional `description`, and the names of the permissions it grants (string
1064
+ * literals or `definePermission("name")` calls). Produced by
1065
+ * `discoverRlsProcedures`.
1066
+ */
568
1067
  interface RlsRoleIR {
569
1068
  /** Optional human-readable description from `defineRole(name, { description })`. */
570
1069
  description?: string;
@@ -574,11 +1073,11 @@ interface RlsRoleIR {
574
1073
  permissions: string[];
575
1074
  }
576
1075
  /**
577
- * Schema-wide RLS metadata the codegen emits into the generated ShardDO so the
578
- * studio's read-only inspector can list, per table, which policies guard it and
579
- * what roles are defined. Aggregated across every `.use(rls(...))` chain in the
580
- * project — purely descriptive, never the predicate logic.
581
- */
1076
+ * Schema-wide RLS metadata the codegen emits into the generated ShardDO so the
1077
+ * studio's read-only inspector can list, per table, which policies guard it and
1078
+ * what roles are defined. Aggregated across every `.use(rls(...))` chain in the
1079
+ * project — purely descriptive, never the predicate logic.
1080
+ */
582
1081
  interface RlsMetadataIR {
583
1082
  /** Every statically-discovered policy `(table, on, procedure)` entry. */
584
1083
  policies: RlsPolicyIR[];
@@ -597,27 +1096,36 @@ interface StorageRuleIR {
597
1096
  procedure: string;
598
1097
  }
599
1098
  /**
600
- * Schema-wide storage-access-rule metadata emitted into the generated ShardDO so
601
- * the studio's read-only inspector can list, per bucket, which operations are
602
- * gated and under what key prefix. Aggregated across every
603
- * `.use(storageRules(...))` chain — descriptive only, never the predicate logic.
604
- */
1099
+ * Schema-wide storage-access-rule metadata emitted into the generated ShardDO so
1100
+ * the studio's read-only inspector can list, per bucket, which operations are
1101
+ * gated and under what key prefix. Aggregated across every
1102
+ * `.use(storageRules(...))` chain — descriptive only, never the predicate logic.
1103
+ */
605
1104
  interface StorageRulesMetadataIR {
606
1105
  rules: StorageRuleIR[];
607
1106
  }
608
1107
  /**
609
- * A typed REST route declared with the `httpRoute.&lt;verb>("/path")…` builder in
610
- * `@lunora/server` and mounted on `httpRouter()`. Captured statically from the
611
- * builder chain so the OpenAPI emitter can render a real `paths` entry: the verb
612
- * + path become the operation's method + URL, and the accumulated validator maps
613
- * become its query parameters, path parameters, and request body.
614
- */
1108
+ * A typed REST route declared with the `httpRoute.<verb>("/path")…` builder in
1109
+ * `@lunora/server` and mounted on `httpRouter()`. Captured statically from the
1110
+ * builder chain so the OpenAPI emitter can render a real `paths` entry: the verb
1111
+ * + path become the operation's method + URL, and the accumulated validator maps
1112
+ * become its query parameters, path parameters, and request body.
1113
+ */
615
1114
  interface HttpRouteIR {
616
1115
  /** `v.*` validators decoding the JSON request body (`.body({...})`), keyed by field. */
617
1116
  body: Record<string, ValidatorIR>;
1117
+ /**
1118
+ * Rendered TS type of one SSE chunk — the `R` the `.stream(handler)`
1119
+ * handler yields — inferred from the handler via the type checker. Present
1120
+ * only when {@link HttpRouteIR.stream} is `true`; `"unknown"` when the
1121
+ * checker can't resolve enough context. Feeds the emitted
1122
+ * `HttpStreamRef<Chunk, …>` so the chunk type flows to the client.
1123
+ * @experimental Part of the HTTP-SSE stream surface (the `httpStreams.*` emission).
1124
+ */
1125
+ chunkType?: string;
618
1126
  /** Export binding name of the route handler (used only for diagnostics / dedupe). */
619
1127
  exportName: string;
620
- /** Path relative to `&lt;projectRoot>/lunora/` without extension, e.g. "http". */
1128
+ /** Path relative to `<projectRoot>/lunora/` without extension, e.g. "http". */
621
1129
  filePath: string;
622
1130
  /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
623
1131
  method: string;
@@ -625,7 +1133,7 @@ interface HttpRouteIR {
625
1133
  output?: ValidatorIR;
626
1134
  /** `v.*` validators decoding the hono path params (`.params({...})`), keyed by `:name`. */
627
1135
  params: Record<string, ValidatorIR>;
628
- /** The route path passed to `httpRoute.&lt;verb>(path)`, e.g. `/api/todos/:id`. */
1136
+ /** The route path passed to `httpRoute.<verb>(path)`, e.g. `/api/todos/:id`. */
629
1137
  path: string;
630
1138
  /** `v.*` validators decoding the URL query string (`.searchParams({...})`), keyed by name. */
631
1139
  searchParams: Record<string, ValidatorIR>;
@@ -633,28 +1141,69 @@ interface HttpRouteIR {
633
1141
  stream: boolean;
634
1142
  }
635
1143
  /**
636
- * Per-procedure protective-middleware snapshot, produced by
637
- * `discoverProcedureMiddleware` for the security lints
638
- * (`public_mutation_without_ratelimit`, `user_creating_mutation_without_captcha`).
639
- * Records which `.use(...)` guards a procedure's builder chain carries plus the
640
- * behavioural facts that decide whether a guard is *expected* (does it write a
641
- * user/session table, does it send mail). `protectPublic({ rateLimit, captcha })`
642
- * is unwrapped: the bundle's object-literal keys set `usesRateLimit`/`usesCaptcha`
643
- * exactly as the individual `.use(rateLimit(...))` / `.use(verifyTurnstile(...))`
644
- * steps would. Structurally identical to `AdvisorProcedureProtection` so values
645
- * pass straight through to the advisor without conversion.
646
- */
1144
+ * Per-procedure protective-middleware snapshot, produced by
1145
+ * `discoverProcedureMiddleware` for the security lints
1146
+ * (`public_mutation_without_ratelimit`, `user_creating_mutation_without_captcha`).
1147
+ * Records which `.use(...)` guards a procedure's builder chain carries plus the
1148
+ * behavioural facts that decide whether a guard is *expected* (does it write a
1149
+ * user/session table, does it send mail). `protectPublic({ rateLimit, captcha })`
1150
+ * is unwrapped: the bundle's object-literal keys set `usesRateLimit`/`usesCaptcha`
1151
+ * exactly as the individual `.use(rateLimit(...))` / `.use(verifyTurnstile(...))`
1152
+ * steps would. Structurally identical to `AdvisorProcedureProtection` so values
1153
+ * pass straight through to the advisor without conversion.
1154
+ */
647
1155
  interface ProcedureMiddlewareIR {
648
- /** `true` when the handler (or a helper inside it) references `ctx.mail` / `ctx.email`. */
649
- callsMail: boolean;
1156
+ /**
1157
+ * `true` when the handler body could be read statically — an inline function
1158
+ * expression/arrow, or a same-file identifier resolved to one. `false` for a
1159
+ * genuinely cross-file handler (an imported function, or an identifier that
1160
+ * doesn't resolve in this file), in which case every behavioural fact below
1161
+ * is `undefined` rather than a false "not observed" — the feeder never saw
1162
+ * the body, so it has nothing to report.
1163
+ */
1164
+ analyzableBody: boolean;
1165
+ /** `true` when the handler (or a helper inside it) references `ctx.mail` / `ctx.email`. `undefined` when `analyzableBody` is `false`. */
1166
+ callsMail?: boolean;
1167
+ /** `true` when the handler emits a structured observability event (`ctx.log` / `ctx.span` / `ctx.trace`). */
1168
+ emitsEvent?: boolean;
1169
+ /** `true` when a `// lunora-advisor-exempt` directive sits above the export. */
1170
+ exempt: boolean;
1171
+ /** The `-- reason` from that directive, or `""`. */
1172
+ exemptReason: string;
650
1173
  /** Export binding name of the procedure (e.g. `signUp`). */
651
1174
  exportName: string;
652
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
1175
+ /** `true` when the handler fans work out to a privileged, cost-bearing dispatch surface (scheduler `runAfter`/`runAt`, a queue producer send, or a workflow create). Feeds the privileged-fanout lint. `undefined` when `analyzableBody` is `false`. */
1176
+ fanOut?: boolean;
1177
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
653
1178
  file: string;
654
- /** Registration kind only `mutation`/`action` are write-shaped; `query` is read-only. */
1179
+ /** `true` when the handler wraps work in `try`/`catch`. */
1180
+ handlesErrors?: boolean;
1181
+ /**
1182
+ * `true` when the procedure declares an email-shaped argument (`email`,
1183
+ * `emailAddress`, `userEmail`, …), `false` when it provably declares none,
1184
+ * and **absent** when the argument list can't be read statically (a
1185
+ * `.input(sharedSchema)`, a spread, or a factory whose `args` comes from a
1186
+ * variable). Feeds `signup_mutation_without_disposable_gating`, which can
1187
+ * only be actioned when there is an address to gate — so "unreadable" must
1188
+ * stay distinguishable from "none", or the lint would clear itself on a
1189
+ * registration that may well expose one.
1190
+ */
1191
+ hasEmailArg?: boolean;
655
1192
  kind: "action" | "mutation" | "query";
1193
+ /** `true` when the handler reaches an outbound surface (`ctx.fetch`, mail, queues, storage, sql, ai, …) that can fail. */
1194
+ reachesOutbound?: boolean;
1195
+ /** `true` when the handler runs any AI generation, bounded or not. */
1196
+ runsAiGeneration?: boolean;
1197
+ /** `true` when the handler throws a bare `new Error(...)` rather than a coded `LunoraError`. */
1198
+ throwsBareError?: boolean;
1199
+ /** `true` when the handler runs an AI generation (`generateText`/`streamText`/`generateObject`/`streamObject`) with no `maxOutputTokens` bound in its config literal. Feeds the `ai_unbounded_generation_public` lint. `undefined` when `analyzableBody` is `false`. */
1200
+ unboundedAiGeneration?: boolean;
656
1201
  /** `true` when the chain carries `.use(verifyTurnstile(...))` or a `protectPublic({ captcha })` bundle. */
657
1202
  usesCaptcha: boolean;
1203
+ /** `true` when the chain carries `.use(emailGateMiddleware(...))` (`@lunora/auth`). Feeds the `signup_mutation_without_disposable_gating` lint. */
1204
+ usesEmailGate: boolean;
1205
+ /** `true` when the handler calls `ctx.db.insertManyUnsafe(...)`, bypassing validators and triggers. Feeds the `insert_many_unsafe_user_data` lint. `undefined` when `analyzableBody` is `false`. */
1206
+ usesInsertManyUnsafe?: boolean;
658
1207
  /** `true` when the chain carries `.use(mask(...))`. */
659
1208
  usesMask: boolean;
660
1209
  /** `true` when the chain carries `.use(rateLimit(...))` or a `protectPublic({ rateLimit })` bundle. */
@@ -663,23 +1212,23 @@ interface ProcedureMiddlewareIR {
663
1212
  usesRls: boolean;
664
1213
  /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
665
1214
  visibility: "internal" | "public";
666
- /** `true` when the handler inserts into a user/session/account-shaped table. */
667
- writesUserTable: boolean;
1215
+ /** `true` when the handler inserts into a user/session/account-shaped table. `undefined` when `analyzableBody` is `false`. */
1216
+ writesUserTable?: boolean;
668
1217
  }
669
1218
  /**
670
- * Per-procedure argument-validator snapshot, produced by the
671
- * argument-validator discoverer for the input-hardening lints
672
- * (`public_arg_uses_any`, `unbounded_string_arg`). Only public procedures are
673
- * recorded — internal functions take server-trusted input. Structurally identical
674
- * to `AdvisorArgumentValidator` so it passes straight through to the advisor
675
- * without conversion.
676
- */
1219
+ * Per-procedure argument-validator snapshot, produced by the
1220
+ * argument-validator discoverer for the input-hardening lints
1221
+ * (`public_arg_uses_any`, `unbounded_string_arg`). Only public procedures are
1222
+ * recorded — internal functions take server-trusted input. Structurally identical
1223
+ * to `AdvisorArgumentValidator` so it passes straight through to the advisor
1224
+ * without conversion.
1225
+ */
677
1226
  interface ArgumentValidatorIR {
678
1227
  /** Arg names declared as `v.any()` (unvalidated, untyped input). */
679
1228
  anyArgs: string[];
680
1229
  /** Export binding name of the procedure (e.g. `updateProfile`). */
681
1230
  exportName: string;
682
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
1231
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
683
1232
  file: string;
684
1233
  /** 1-based line of the registration call, or `0` when unknown. */
685
1234
  line: number;
@@ -687,13 +1236,34 @@ interface ArgumentValidatorIR {
687
1236
  unboundedStringArgs: string[];
688
1237
  }
689
1238
  /**
690
- * One secret-shaped string literal discovered in `lunora/` source the
691
- * `hardcoded_secret` lint input. Complements the pre-commit `vis secrets` scan by
692
- * surfacing the same class of finding in-IDE via the studio Advisors table.
693
- * Structurally identical to `AdvisorSecretLiteral`.
694
- */
1239
+ * One factory/constructor call in `lunora/` whose config object literal a
1240
+ * security lint inspects for a present-or-absent key the shared input for the
1241
+ * config-call security lints (payment authorize, inbound-mail verify, rate-limit
1242
+ * store, browser private-targets). Structurally identical to `AdvisorConfigCall`
1243
+ * so it passes straight through to the advisor without conversion.
1244
+ */
1245
+ interface ConfigCallIR {
1246
+ /** `true` when the config argument was a static object literal the feeder could read. */
1247
+ analyzable: boolean;
1248
+ /** The factory function or constructor name at the call site, e.g. `createPayment` / `RateLimiter`. */
1249
+ callee: string;
1250
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1251
+ file: string;
1252
+ /** 1-based line of the call site, or `0` when unknown. */
1253
+ line: number;
1254
+ /** Keys present in the config object literal (empty when not `analyzable`). */
1255
+ presentKeys: string[];
1256
+ /** Keys in the config object literal explicitly assigned the literal `true`. */
1257
+ trueKeys: string[];
1258
+ }
1259
+ /**
1260
+ * One secret-shaped string literal discovered in `lunora/` source — the
1261
+ * `hardcoded_secret` lint input. Complements the pre-commit `vis secrets` scan by
1262
+ * surfacing the same class of finding in-IDE via the studio Advisors table.
1263
+ * Structurally identical to `AdvisorSecretLiteral`.
1264
+ */
695
1265
  interface SecretLiteralIR {
696
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
1266
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
697
1267
  file: string;
698
1268
  /** Heuristic that matched, e.g. `stripe_live_key` / `aws_access_key` / `pem_private_key` / `high_entropy`. */
699
1269
  kind: string;
@@ -703,283 +1273,1677 @@ interface SecretLiteralIR {
703
1273
  preview: string;
704
1274
  }
705
1275
  /**
706
- * One `ctx.sql.query(text, …)` / `ctx.sql.unsafe(text, …)` call whose `text`
707
- * argument is built in place rather than passed as a fixed statement — the
708
- * `sql_injection_risk` lint input. The Hyperdrive driver binds ONLY the `params`
709
- * array; the `text` string is spliced verbatim into the SQL, so a `text` assembled
710
- * from a string concatenation or a substitution template literal is an injection
711
- * vector. A fixed string literal / no-substitution template is safe. Structurally
712
- * identical to `AdvisorSqlInterpolation`.
713
- */
1276
+ * One `ctx.sql.query(text, …)` / `ctx.sql.unsafe(text, …)` call whose `text`
1277
+ * argument is built in place rather than passed as a fixed statement — the
1278
+ * `sql_injection_risk` lint input. The Hyperdrive driver binds ONLY the `params`
1279
+ * array; the `text` string is spliced verbatim into the SQL, so a `text` assembled
1280
+ * from a string concatenation or a substitution template literal is an injection
1281
+ * vector. A fixed string literal / no-substitution template is safe. Structurally
1282
+ * identical to `AdvisorSqlInterpolation`.
1283
+ */
714
1284
  interface SqlInterpolationIR {
715
1285
  /** Export binding name of the procedure performing the `ctx.sql` call. */
716
1286
  exportName: string;
717
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
1287
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
718
1288
  file: string;
719
1289
  /** 1-based line of the interpolation, or `0` when unknown. */
720
1290
  line: number;
721
1291
  }
722
1292
  /**
723
- * One discovered `httpRoute.&lt;verb>("/admin/…")` route on an admin/privileged-looking
724
- * path, with whether its builder chain references an auth/admin guard — the
725
- * `admin_route_without_guard` lint input. Structurally identical to
726
- * `AdvisorAdminRoute`.
727
- */
728
- interface AdminRouteIR {
729
- /** Export binding name of the route handler. */
1293
+ * One `ctx.fetch(url, …)` call inside an action whose URL argument is derived
1294
+ * from the handler's `args` the `action_fetch_ssrf` lint input. `ctx.fetch` is
1295
+ * the action-only outbound-request escape hatch with no host allowlist, so a URL
1296
+ * assembled from request input is a server-side request forgery vector (cloud
1297
+ * metadata endpoints, internal services). Only arg-derived URLs reach here; a
1298
+ * fixed literal or a URL built from config/`ctx.*` is not recorded. Structurally
1299
+ * identical to `AdvisorArgumentDerivedFetch`.
1300
+ */
1301
+ interface ArgumentDerivedFetchIR {
1302
+ /** Export binding name of the action performing the `ctx.fetch` call. */
730
1303
  exportName: string;
731
- /** Source file relative to `&lt;projectRoot>/lunora/`, without extension. */
1304
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
732
1305
  file: string;
733
- /** HTTP verb the route binds to (uppercased), e.g. `"POST"`. */
734
- method: string;
735
- /** The route path, e.g. `/admin/users`. */
736
- path: string;
737
- /** `true` when the handler body references an auth/session/admin guard (`ctx.auth`, `getSession`, `requireAdmin`, …). */
738
- usesGuard: boolean;
1306
+ /** 1-based line of the `ctx.fetch` call, or `0` when unknown. */
1307
+ line: number;
739
1308
  }
740
- interface ProjectIR {
741
- crons: ReadonlyArray<CronJobIR>;
742
- functions: ReadonlyArray<FunctionIR>;
743
- /** Typed REST routes discovered from `httpRoute.&lt;verb>(...)` builder chains. */
744
- httpRoutes: ReadonlyArray<HttpRouteIR>;
745
- migrations: ReadonlyArray<MigrationIR>;
746
- schema: SchemaIR;
1309
+ /**
1310
+ * One `ctx.kv.<method>(key, …)` call whose namespace key is derived from the
1311
+ * handler's `args` with no server-side scoping — the `kv_unscoped_user_key_idor`
1312
+ * lint input. Workers KV is a single flat namespace, so a key taken straight from
1313
+ * request input lets any caller read, overwrite, or delete another user's entry
1314
+ * (IDOR). Only arg-derived, unscoped keys reach here; a fixed literal, or a key
1315
+ * prefixed with a server-trusted identity (`${ctx.auth.userId}:…` — references
1316
+ * `ctx`, so treated as scoped), is not recorded. `list` is excluded (it takes a
1317
+ * prefix, not a per-entry key). Structurally identical to `AdvisorKvKeyAccess`.
1318
+ */
1319
+ interface KvKeyAccessIR {
1320
+ /** Export binding name of the procedure performing the `ctx.kv` access. */
1321
+ exportName: string;
1322
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1323
+ file: string;
1324
+ /** 1-based line of the `ctx.kv` call, or `0` when unknown. */
1325
+ line: number;
1326
+ /** The `ctx.kv` method invoked: `get` / `getRaw` / `getWithMetadata` / `put` / `delete`. */
1327
+ method: string;
747
1328
  }
748
1329
  /**
749
- * Run the static lints against a discovered {@link SchemaIR} and the reads/writes/calls
750
- * found in function bodies: query reads feed `filter_without_index`, insert writes
751
- * feed `table_without_insert`, authApi calls feed `auth_api_call_without_headers`,
752
- * rls procedure snapshots feed `rls_uncovered_table`, and mask procedure
753
- * snapshots feed `mask_uncovered_pii_column`; declared containers
754
- * feed the `container_*` lints; declared workflows + `ctx.workflows.get(...)` call
755
- * sites feed the `workflow_unused` / `workflow_unknown_target` lints; non-deterministic
756
- * calls inside query/mutation handlers feed the `nondeterministic_query_mutation` lint
757
- * (all default empty for callers that don't analyze functions/containers/workflows).
758
- * The IR types are structurally identical to the advisor's evidence types so they
759
- * pass straight through without conversion. Returns the findings; surfacing them
760
- * (console, error overlay, studio Advisors table) is the caller's choice.
761
- */
762
- declare const lintSchema: (schema: SchemaIR, queries?: ReadonlyArray<QueryReadIR>, inserts?: ReadonlyArray<InsertWriteIR>, authApiCalls?: ReadonlyArray<AuthApiCallIR>, rlsProcedures?: ReadonlyArray<RlsProcedureIR>, containers?: ReadonlyArray<ContainerIR>, workflows?: ReadonlyArray<WorkflowIR>, workflowCalls?: ReadonlyArray<WorkflowCallIR>, maskProcedures?: ReadonlyArray<MaskProcedureIR>, nondeterministicCalls?: ReadonlyArray<NondeterministicCallIR>, procedureProtections?: ReadonlyArray<ProcedureMiddlewareIR>, argumentValidators?: ReadonlyArray<ArgumentValidatorIR>, secretLiterals?: ReadonlyArray<SecretLiteralIR>, sqlInterpolations?: ReadonlyArray<SqlInterpolationIR>, adminRoutes?: ReadonlyArray<AdminRouteIR>, r2sqlCalls?: ReadonlyArray<R2sqlCallIR>) => Finding[];
763
- /**
764
- * Render advisor findings as a single multi-line string for console surfacing:
765
- * a one-line summary header followed by one `[LEVEL] name: detail` line per
766
- * finding. Returns `""` when there are no findings.
767
- */
768
- declare const formatAdvisories: (findings: ReadonlyArray<Finding>) => string;
1330
+ * One `ctx.db` write (`insert` / `replace` / `patch` / `insertManyUnsafe`) that sets
1331
+ * an ownership / identity column `userId`, `ownerId`, `tenantId`, and the like —
1332
+ * from the handler's `args` instead of the server-trusted identity. The
1333
+ * `owner_field_from_args_not_auth` lint input: the ownership column decides who a
1334
+ * row belongs to, so a value taken from request input lets any caller write rows
1335
+ * owned by another user or tenant (the act-as-any-user / cross-tenant IDOR vector).
1336
+ * A column stamped from `ctx.*`, or set to a fixed literal, is not recorded; only an
1337
+ * arg-derived identity write reaches here. Structurally identical to
1338
+ * `AdvisorOwnerFieldWrite`.
1339
+ */
1340
+ /**
1341
+ * One branching `defineShape({ where })` / `definePolicy({ when })` predicate arm
1342
+ * that returns an unrestricted predicate — the `unrestricted_where_branch` lint
1343
+ * input. A denial arm must match NO rows (`deny()` / `{ OR: [] }`); `{}` matches
1344
+ * every row, so the near-miss silently replicates the whole table.
1345
+ */
1346
+ interface UnrestrictedWhereBranchIR {
1347
+ /** Export binding name of the shape / policy the predicate belongs to. */
1348
+ exportName: string;
1349
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1350
+ file: string;
1351
+ /** Which unrestricted form was returned. */
1352
+ form: "empty-object" | "undefined";
1353
+ /** The config key carrying the predicate (`where` for a shape, `when` for a policy). */
1354
+ key: string;
1355
+ /** 1-based line of the offending returned expression. */
1356
+ line: number;
1357
+ /** The declaring call (`defineShape` / `definePolicy`). */
1358
+ owner: string;
1359
+ }
1360
+ interface OwnerFieldWriteIR {
1361
+ /** Export binding name of the procedure performing the write. */
1362
+ exportName: string;
1363
+ /** The identity column being written from `args` (e.g. `userId`). */
1364
+ field: string;
1365
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1366
+ file: string;
1367
+ /** 1-based line of the `ctx.db` write call, or `0` when unknown. */
1368
+ line: number;
1369
+ /** The `ctx.db` write method (`insert` / `replace` / `patch` / `insertManyUnsafe`). */
1370
+ method: string;
1371
+ /**
1372
+ * Visibility of the enclosing procedure. `internal` procedures are not
1373
+ * reachable by a caller, so the lint's premise ("any caller can write rows
1374
+ * owned by another user") does not hold there — see
1375
+ * `owner_field_from_args_not_auth`. `undefined` when the write sits outside
1376
+ * any recognised procedure (a bare helper).
1377
+ */
1378
+ visibility?: "internal" | "public";
1379
+ }
769
1380
  /**
770
- * An error thrown by codegen discovery when the user's schema or function
771
- * source has a structural problem that can be pinpointed to a specific source
772
- * location. The `file`, `line`, and `column` properties mirror what Vite's
773
- * error-overlay `loc` field expects so the browser can display the exact spot.
774
- */
775
- declare class CodegenDiagnosticError extends Error {
776
- readonly column: number;
777
- readonly file: string;
778
- readonly line: number;
779
- constructor(message: string, file: string, line: number, column: number);
1381
+ * One `ctx.storage.<bucket>.<method>(key, …)` call whose R2 object key is derived
1382
+ * from the handler's `args` with no server-side scoping the
1383
+ * `storage_key_from_user_args` lint input. The bucket read/write/URL/delete methods
1384
+ * key by their first argument, so an object key taken straight from request input is
1385
+ * object-level IDOR (read/overwrite/delete anyone's object). A key referencing a
1386
+ * server-trusted `ctx.*` value (e.g. `${ctx.auth.userId}/…`) is treated as scoped
1387
+ * and is not recorded; only an arg-derived, `ctx`-free key reaches here.
1388
+ * Structurally identical to `AdvisorStorageKeyAccess`.
1389
+ */
1390
+ interface StorageKeyAccessIR {
1391
+ /** Export binding name of the procedure performing the storage call. */
1392
+ exportName: string;
1393
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1394
+ file: string;
1395
+ /** 1-based line of the storage call, or `0` when unknown. */
1396
+ line: number;
1397
+ /** The bucket method invoked with the arg-derived key, e.g. `get` / `put` / `delete` / `download`. */
1398
+ method: string;
1399
+ /**
1400
+ * Visibility of the enclosing procedure. `internal` procedures have no
1401
+ * untrusted caller by construction — see `owner_field_from_args_not_auth`'s
1402
+ * identical split — so `storage_key_from_user_args` drops the finding to
1403
+ * INFO rather than ERROR there. `undefined` when the access sits outside any
1404
+ * registered procedure the feeder could attribute it to.
1405
+ */
1406
+ visibility?: "internal" | "public";
780
1407
  }
781
1408
  /**
782
- * Build a {@link CodegenDiagnosticError} whose message includes the source
783
- * location and whose `file`/`line`/`column` properties are set from the
784
- * ts-morph `Node`'s position in its source file.
785
- *
786
- * Message format: `@lunora/codegen: &lt;detail> (&lt;file>:&lt;line>:&lt;column>)`
787
- *
788
- * `meta` is merged onto the returned error for callers that also carry the
789
- * project-wide `LunoraError` envelope (`code`/`name`/`status`) it never
790
- * touches `file`/`line`/`column`, and the error stays an instance of
791
- * {@link CodegenDiagnosticError} so the Vite overlay's `instanceof` location
792
- * lookup is unaffected.
793
- */
794
- declare const diagnosticAt: (node: Node, detail: string, meta?: Record<string, unknown>) => CodegenDiagnosticError;
1409
+ * One `ctx.containers.<exportName>.get(name, …)` call whose instance key is derived
1410
+ * from the handler's `args` with no server-side scoping the
1411
+ * `container_instance_key_from_user_input` lint input. Each container definition's
1412
+ * `.get(name)` accessor routes to one instance per `name`, so a key taken straight from
1413
+ * request input lets any caller reach another tenant's container (a cross-tenant IDOR). A
1414
+ * fixed literal key, or one derived from a server-trusted identity (`${ctx.auth.userId}` —
1415
+ * references `ctx`, so treated as scoped), is not recorded; only an arg-derived, unscoped
1416
+ * key reaches here. `.any()`/`.pool()` take no key and are not sinks. Structurally
1417
+ * identical to `AdvisorContainerKeyAccess`.
1418
+ */
1419
+ interface ContainerKeyAccessIR {
1420
+ /** Export binding name of the procedure performing the `ctx.containers` access. */
1421
+ exportName: string;
1422
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1423
+ file: string;
1424
+ /** 1-based line of the `ctx.containers.*.get` call, or `0` when unknown. */
1425
+ line: number;
1426
+ /** The container accessor method invoked — always `get`. */
1427
+ method: string;
1428
+ }
795
1429
  /**
796
- * Discover `ctx.authApi.&lt;method>(...)` (and bare `authApi.&lt;method>(...)`) calls
797
- * under the lunora source directory and attribute each to the exported function
798
- * (and file) performing it. Calls outside an exported declaration are dropped.
799
- */
800
- declare const discoverAuthApiCalls: (project: Project, lunoraDirectory: string) => AuthApiCallIR[];
801
- /** The only file containers may be declared in mirrors `lunora/crons.ts`. */
802
- declare const CONTAINERS_FILENAME = "containers.ts";
1430
+ * One `ctx.ai.run(model, …)` call whose model-id argument is derived from the handler's
1431
+ * `args` with no server-side scoping the `ai_raw_run_escape_hatch` lint input.
1432
+ * `ctx.ai.run` is the raw Workers AI binding passthrough, bypassing the typed
1433
+ * `ctx.ai.model(...)` + AI-SDK layer (`generateText`/`streamText`/…) that caps output and
1434
+ * enforces a schema, so an arg-derived model id lets any caller select an arbitrary model.
1435
+ * A fixed literal model, or one scoped by a server-trusted `ctx.*` value, is not recorded;
1436
+ * only an arg-derived, unscoped model id reaches here (an arg-derived `inputs` argument is
1437
+ * normal usage and is never inspected). Structurally identical to `AdvisorAiRawRun`.
1438
+ */
1439
+ interface AiRawRunIR {
1440
+ /** Export binding name of the procedure performing the `ctx.ai.run` call. */
1441
+ exportName: string;
1442
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1443
+ file: string;
1444
+ /** 1-based line of the `ctx.ai.run` call, or `0` when unknown. */
1445
+ line: number;
1446
+ }
803
1447
  /**
804
- * Discover every container the project declares: exported `defineContainer()`
805
- * calls in `lunora/containers.ts`. Returns `[]` when the file doesn't exist.
806
- * Wrangler-relevant fields (`image`, `instanceType`, `maxInstances`, `name`)
807
- * must be static literals; runtime-only fields (`env`, `sleepAfter`, …) may be
808
- * any expression since the generated class imports the definition object.
809
- */
810
- declare const discoverContainers: (project: Project, lunoraDirectory: string) => ContainerIR[];
1448
+ * One `ctx.vectors.<method>(indexName, input)` call whose `input.namespace` is derived
1449
+ * from the handler's `args` with no server-side scoping — the
1450
+ * `vectors_namespace_from_user_input` lint input. A Vectorize namespace partitions one
1451
+ * index into isolated sub-collections, so a namespace taken straight from request input
1452
+ * lets any caller read or poison another tenant's vectors. A fixed literal namespace, or
1453
+ * one prefixed with a server-trusted identity (`${ctx.auth.orgId}` — references `ctx`, so
1454
+ * treated as scoped), is not recorded; only an arg-derived, unscoped namespace reaches
1455
+ * here. Structurally identical to `AdvisorVectorNamespaceAccess`.
1456
+ */
1457
+ interface VectorNamespaceAccessIR {
1458
+ /** Export binding name of the procedure performing the `ctx.vectors` access. */
1459
+ exportName: string;
1460
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1461
+ file: string;
1462
+ /** 1-based line of the `ctx.vectors` call, or `0` when unknown. */
1463
+ line: number;
1464
+ /** The `ctx.vectors` method invoked: `query` / `upsert` / `upsertMany`. */
1465
+ method: string;
1466
+ }
811
1467
  /**
812
- * Scan every `.ts` file under `lunoraDir` for `cronJobs()` builder registrations
813
- * (`crons.interval(...)`, `crons.daily(...)`, `crons.cron(...)`, …) and lift them
814
- * into {@link CronJobIR}. Schedules are compiled to standard cron expressions;
815
- * function references are resolved to their `namespace:fn` dispatch path, while a
816
- * bare identifier naming a declared workflow (`workflows`) resolves to a durable
817
- * workflow start. Names must be unique across the project.
818
- */
819
- declare const discoverCrons: (project: Project, lunoraDirectory: string, workflows?: ReadonlyArray<WorkflowIR>) => CronJobIR[];
820
- /**
821
- * Scan all .ts files under `lunoraDir` (skipping `_generated/` and `schema.ts`)
822
- * for top-level `export const x = query/mutation/action({...})` registrations.
823
- */
824
- declare const discoverFunctions: (project: Project, lunoraDirectory: string) => FunctionIR[];
1468
+ * One `ctx.mail`/`ctx.email` `send`/`queue` call whose recipient field (`to`/`cc`/`bcc`)
1469
+ * is derived from the handler's `args` with no server-side scoping — the
1470
+ * `mail_recipient_from_request_input` lint input. A recipient taken straight from request
1471
+ * input turns the deployment into an open relay / spam amplifier (any caller can direct
1472
+ * mail to an arbitrary address). A fixed literal recipient, or one scoped by a
1473
+ * server-trusted `ctx.*` value (e.g. `ctx.auth.user.email`), is not recorded; only an
1474
+ * arg-derived, unscoped recipient reaches here. Structurally identical to
1475
+ * `AdvisorMailRecipientAccess`.
1476
+ */
1477
+ interface MailRecipientAccessIR {
1478
+ /** Export binding name of the procedure performing the `ctx.mail`/`ctx.email` call. */
1479
+ exportName: string;
1480
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1481
+ file: string;
1482
+ /** 1-based line of the `ctx.mail`/`ctx.email` call, or `0` when unknown. */
1483
+ line: number;
1484
+ /** The mailer method invoked: `send` / `queue`. */
1485
+ method: string;
1486
+ }
825
1487
  /**
826
- * Scan all `.ts` files under `lunoraDir` (skipping `_generated/` and `schema.ts`)
827
- * for `export const x = httpRoute.&lt;verb>(...)…handler(...)` typed REST routes.
828
- * These are the headline OpenAPI target: each becomes a real `paths` entry.
829
- */
830
- declare const discoverHttpRoutes: (project: Project, lunoraDirectory: string) => HttpRouteIR[];
1488
+ * One `ctx.browser.<method>(url, …)` call whose navigation URL (`arguments[0]`)
1489
+ * is derived from the handler's `args` with no server-side scoping — the
1490
+ * `browser_user_url_without_allowlist` lint input. The lint additionally
1491
+ * cross-references `createBrowser` config-call evidence to suppress findings
1492
+ * when the browser is hardened with an `allowedHosts` allowlist or
1493
+ * `resolveDns`. Structurally identical to `AdvisorBrowserUrlAccess`.
1494
+ */
1495
+ interface BrowserUrlAccessIR {
1496
+ /** Export binding name of the procedure performing the `ctx.browser` call. */
1497
+ exportName: string;
1498
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1499
+ file: string;
1500
+ /** 1-based line of the `ctx.browser` call, or `0` when unknown. */
1501
+ line: number;
1502
+ /** The browser method invoked: `content` / `pdf` / `scrape` / `screenshot`. */
1503
+ method: string;
1504
+ }
831
1505
  /**
832
- * Discover `ctx.db.insert("table", …)` writes under the lunora source directory
833
- * and attribute each to the exported function (and file) performing it. Calls
834
- * with a non-literal table argument, or outside an exported declaration, are
835
- * dropped (`table === ""` / no enclosing export).
836
- */
837
- declare const discoverInserts: (project: Project, lunoraDirectory: string) => InsertWriteIR[];
1506
+ * One runtime container-override call: a `<handle>.start({ enableInternet: true, … })`
1507
+ * launch override, or a `<handle>.egress.<method>(...)` runtime firewall mutation
1508
+ * (`allow` / `deny` / `setAllowed`) the `container_start_enable_internet_override`
1509
+ * and `container_runtime_egress_relaxation` lint input. Both shapes re-open network
1510
+ * access the static `defineContainer` declaration (and its `container_public_internet`
1511
+ * lint) assumes is locked down. Matched structurally by call shape, independent of the
1512
+ * receiver's resolved type. Structurally identical to `AdvisorContainerOverride`.
1513
+ */
1514
+ interface ContainerOverrideIR {
1515
+ /** e.g. the egress method name, or `"enableInternet: true"`. */
1516
+ detail: string;
1517
+ /** Export binding name of the procedure performing the call. */
1518
+ exportName: string;
1519
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1520
+ file: string;
1521
+ /** Which override shape matched. */
1522
+ kind: "egress_relaxation" | "enable_internet";
1523
+ /** 1-based line of the call, or `0` when unknown. */
1524
+ line: number;
1525
+ }
838
1526
  /**
839
- * Discover masking usage for every exported Lunora procedure under the lunora
840
- * source directory — the column-level twin of `discoverRlsProcedures`. For each
841
- * procedure, records whether its builder chain includes `.use(mask(...))`, which
842
- * `(table, column)` pairs that mask declares, and which tables it reads/writes
843
- * through `ctx.db`. Feeds the `mask_uncovered_pii_column` advisor lint.
844
- */
845
- declare const discoverMaskProcedures: (project: Project, lunoraDirectory: string) => MaskProcedureIR[];
1527
+ * One `buildImageDeliveryUrl({ key, })` call (`@lunora/bindings/images`) whose
1528
+ * `key` — the CDN transform's source image, an absolute URL or an
1529
+ * origin-relative key is derived from the handler's `args` with no
1530
+ * server-side scoping the `images_url_source_from_user_input` lint input.
1531
+ * `ctx.images.transform`/`info` take image *bytes*, never a URL, so they are not
1532
+ * sinks; only the `key` of `buildImageDeliveryUrl` accepts a URL-or-key source
1533
+ * and is inspected. An arg-derived `key` lets any caller point the CDN's
1534
+ * `/cdn-cgi/image/` transform at an attacker-chosen origin (SSRF / open proxy)
1535
+ * or at an arbitrary key under the account's own store. A fixed literal, or a
1536
+ * key scoped by a server-trusted `ctx.*` value, is not recorded. Structurally
1537
+ * identical to `AdvisorImageDeliveryUrlAccess`.
1538
+ */
1539
+ interface ImageDeliveryUrlAccessIR {
1540
+ /** Export binding name of the procedure performing the `buildImageDeliveryUrl` call. */
1541
+ exportName: string;
1542
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1543
+ file: string;
1544
+ /** 1-based line of the `buildImageDeliveryUrl` call, or `0` when unknown. */
1545
+ line: number;
1546
+ }
846
1547
  /**
847
- * Aggregate the schema-wide masking metadata the studio's data-browser mask
848
- * toggle reads: every statically-discovered `(table, column, strategy)` masked
849
- * column across the project's `.use(mask(...))` chains. Walks the same builder
850
- * chains as {@link discoverMaskProcedures} but carries the strategy the preview
851
- * needs to choose redact-vs-hash-vs-custom rendering. Deduped by `(table,
852
- * column)` with the first declaration winning, so a column masked by several
853
- * procedures lists once the same evidence the advisor lint uses.
854
- */
855
- /**
856
- * Scan all `.ts` files under `lunoraDir` for top-level
857
- * `export const x = defineMigration({...})` declarations and lift them into
858
- * {@link MigrationIR}. `id` must be a static string literal (it's the registry
859
- * key); `table` is best-effort and left `""` when not a literal.
860
- */
861
- declare const discoverMigrations: (project: Project, lunoraDirectory: string) => MigrationIR[];
1548
+ * One `createAuth({...})` call's configuration snapshot the shared input for
1549
+ * the five `auth_*` security lints (trusted-origins wildcard, CSRF check
1550
+ * disabled, secure cookies disabled, email verification disabled, session
1551
+ * freshAge zero). Matched by callee NAME (an `import`-agnostic, fail-closed
1552
+ * convention the other feeders share), so a re-export or alias still resolves.
1553
+ * When the config argument isn't a statically-analyzable object literal (a
1554
+ * top-level spread, or not an object literal at all), `analyzable` is `false`
1555
+ * and every boolean fact defaults to its SAFE (not-flagged) value — an opaque
1556
+ * config can't be relied on either way. Structurally identical to
1557
+ * `AdvisorAuthConfig`.
1558
+ */
1559
+ interface AuthConfigIR {
1560
+ /** `true` when the call's config argument was a static object literal the feeder could read. */
1561
+ analyzable: boolean;
1562
+ /** `advanced.disableCSRFCheck === true`. */
1563
+ disableCsrfCheck: boolean;
1564
+ /** `emailAndPassword.enabled === true`. */
1565
+ emailPasswordEnabled: boolean;
1566
+ /** Export binding name enclosing the `createAuth(...)` call. */
1567
+ exportName: string;
1568
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1569
+ file: string;
1570
+ /** 1-based line of the `createAuth(...)` call, or `0` when unknown. */
1571
+ line: number;
1572
+ /** `emailAndPassword.requireEmailVerification === true` present. */
1573
+ requireEmailVerification: boolean;
1574
+ /** `trustedOrigins` array literal contains a `"*"` element. */
1575
+ /** `plugins` includes `scim(...)` while `database` is a non-transactional Lunora adapter — a combination that throws at runtime. */
1576
+ scimOnNonTransactionalAdapter: boolean;
1577
+ /** `advanced.useSecureCookies === false`. */
1578
+ secureCookiesDisabled: boolean;
1579
+ /** `session.freshAge === 0` (explicit literal). */
1580
+ sessionFreshAgeZero: boolean;
1581
+ trustedOriginsWildcard: boolean;
1582
+ }
862
1583
  /**
863
- * Discover non-deterministic API calls (`Date.now`, `new Date()`, `Date()`,
864
- * `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues`including
865
- * `globalThis`/`self`/`window`-prefixed receivers and `fetch`) lexically inside
866
- * the handler body of every exported `query(...)` / `mutation(...)` registration
867
- * under the lunora source directory the `nondeterministic_query_mutation` lint
868
- * input. `action(...)` (and `stream(...)`) registrations are intentionally
869
- * skipped: actions run exactly once and may use ambient APIs freely.
870
- *
871
- * Traversal is scoped to the handler node (not the whole declaration), mirroring
872
- * how the auth-api / insert feeders attribute calls — so a call in a sibling
873
- * helper outside the handler, or in a nested `action(...)` passed elsewhere, is
874
- * not attributed to the query/mutation. One {@link NondeterministicCallIR} is
875
- * produced per call site.
876
- */
877
- declare const discoverNondeterministicCalls: (project: Project, lunoraDirectory: string) => NondeterministicCallIR[];
1584
+ * One `rateLimit`/`dbRateLimit` middleware call (`@lunora/ratelimit`) whose
1585
+ * `key` selectorthe per-caller rate-limit sub-key, `(ctx) => string |
1586
+ * undefined` — is derived from the handler's `args` with no server-side
1587
+ * scoping (no reference to the trusted `ctx` binding anywhere in the selector)
1588
+ * the `ratelimit_key_spoofable_or_global` lint input. A key an attacker
1589
+ * controls lets them rotate it per request and bypass the limit entirely,
1590
+ * defeating its purpose. A selector scoped by `ctx` (e.g. `ctx.auth.userId`,
1591
+ * `ctx.ip`), or one with no `args` reference at all (a fixed/global bucket —
1592
+ * the "no key" case this lint deliberately does not flag, to keep it low-FP),
1593
+ * is not recorded. Structurally identical to `AdvisorRatelimitKeySelector`.
1594
+ */
1595
+ interface RatelimitKeySelectorIR {
1596
+ /** The `rateLimit`/`dbRateLimit` callee invoked. */
1597
+ callee: string;
1598
+ /** Export binding name of the procedure whose `.use(...)` chain carries the call. */
1599
+ exportName: string;
1600
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1601
+ file: string;
1602
+ /** The rate limit's `name` argument (the second positional argument), or `""` when not a string literal. */
1603
+ limitName: string;
1604
+ /** 1-based line of the `rateLimit`/`dbRateLimit` call, or `0` when unknown. */
1605
+ line: number;
1606
+ }
878
1607
  /**
879
- * Discover `ctx.db.query("table")…` reads under the lunora source directory and
880
- * reduce each to a {@link QueryReadIR}. Only reads that call `.filter()` are
881
- * returned an unfiltered read is never a `filter_without_index` candidate, so
882
- * dropping the rest keeps the lint input small.
883
- */
884
- declare const discoverQueries: (project: Project, lunoraDirectory: string) => QueryReadIR[];
885
- /** The only file queues may be declared in mirrors `lunora/workflows.ts`. */
886
- declare const QUEUES_FILENAME = "queues.ts";
1608
+ * One payload-derived privileged dispatch — a `ctx.run`/`context.run` back into a
1609
+ * Lunora function from inside a `defineQueue` push handler or a `defineWorkflow`
1610
+ * handler, whose args reference the handler's untrusted payload (`context.params`
1611
+ * for a workflow, a `for (… of batch.messages)` body for a queue) — the
1612
+ * `privileged_dispatch_unvalidated_payload` lint input. Both handler kinds run
1613
+ * under the **system identity** (RLS disabled), so forwarding attacker-influenced
1614
+ * payload into the dispatch bypasses the target's row policy. The resolved
1615
+ * `targetFile`/`targetExport` let the lint join RLS-procedure evidence and fire
1616
+ * only for RLS-gated targets. Structurally identical to `AdvisorPrivilegedDispatch`.
1617
+ */
1618
+ interface PrivilegedDispatchIR {
1619
+ /** `"queue"` for a `defineQueue` handler, `"workflow"` for a `defineWorkflow` handler. */
1620
+ dispatchKind: "queue" | "workflow";
1621
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1622
+ file: string;
1623
+ /** Export binding name of the handler performing the dispatch. */
1624
+ handlerExport: string;
1625
+ /** 1-based line of the dispatch call, or `0` when unknown. */
1626
+ line: number;
1627
+ /** Export name of the dispatched target (`send` in `api.messages.send`). */
1628
+ targetExport: string;
1629
+ /** File path of the dispatched target relative to `lunora/` (`messages` in `api.messages.send`). */
1630
+ targetFile: string;
1631
+ }
887
1632
  /**
888
- * Discover every queue the project declares: exported `defineQueue()` calls in
889
- * `lunora/queues.ts`. Returns `[]` when the file doesn't exist. Only the
890
- * wrangler-relevant literals (`name`/`mode`/batch tuning) are read; the handler
891
- * body is runtime-only, so codegen never evaluates it.
892
- */
893
- declare const discoverQueues: (project: Project, lunoraDirectory: string) => QueueIR[];
1633
+ * One discovered `httpRoute.<verb>("/admin/…")` route on an admin/privileged-looking
1634
+ * path, with whether its builder chain references an auth/admin guard — the
1635
+ * `admin_route_without_guard` lint input. Structurally identical to
1636
+ * `AdvisorAdminRoute`.
1637
+ */
1638
+ interface AdminRouteIR {
1639
+ /** Export binding name of the route handler. */
1640
+ exportName: string;
1641
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1642
+ file: string;
1643
+ /** HTTP verb the route binds to (uppercased), e.g. `"POST"`. */
1644
+ method: string;
1645
+ /** The route path, e.g. `/admin/users`. */
1646
+ path: string;
1647
+ /** `true` when the handler body references an auth/session/admin guard (`ctx.auth`, `getSession`, `requireAdmin`, …). */
1648
+ usesGuard: boolean;
1649
+ }
894
1650
  /**
895
- * Discover `ctx.r2sql` accesses lexically inside the handler body of every
896
- * exported `query(...)` / `mutation(...)` registration under the lunora source
897
- * directory — the `r2sql_outside_action` lint input. `action(...)` (and
898
- * `stream(...)`) registrations are intentionally skipped: R2 SQL is the
899
- * external, non-reactive surface that belongs in actions.
900
- *
901
- * Traversal is scoped to the handler node (not the whole declaration), mirroring
902
- * `discoverNondeterministicCalls` so a `ctx.r2sql` touch in a sibling helper
903
- * outside the handler is not attributed to the query/mutation. One
904
- * {@link R2sqlCallIR} is produced per access site.
905
- */
906
- declare const discoverR2sqlCalls: (project: Project, lunoraDirectory: string) => R2sqlCallIR[];
1651
+ * One tracked `ctx.storage.<bucket>.<method>(...)` upload/signing call the
1652
+ * shared input for the storage config-hygiene security lints
1653
+ * (`storage_upload_without_content_type_allowlist`, `storage_upload_without_max_size`,
1654
+ * `storage_generate_upload_url_no_content_type_pin`, `storage_presigned_url_for_private_content`).
1655
+ * `upload`/`store` carry the `UploadOptions` guards (`allowedContentTypes` /
1656
+ * `maxSize`); `generateUploadUrl` carries the signed-PUT `contentType` pin;
1657
+ * `getPresignedUrl`/`getSignedUrl` carry a statically-known `expiresInSeconds`
1658
+ * literal. `presentKeys` is empty (and `expiresInSeconds` unset) when the
1659
+ * options argument was absent, a non-literal, or a spread — see `analyzable`.
1660
+ * Structurally identical to `AdvisorStorageUpload`.
1661
+ */
1662
+ interface StorageUploadIR {
1663
+ /** `true` when the call's options-object argument (or its deliberate absence) was statically resolvable. */
1664
+ analyzable: boolean;
1665
+ /** Numeric literal value of an `expiresInSeconds` option, when statically known (`getSignedUrl` / `getPresignedUrl` only). */
1666
+ expiresInSeconds?: number;
1667
+ /** Export binding name of the procedure performing the call. */
1668
+ exportName: string;
1669
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1670
+ file: string;
1671
+ /** 1-based line of the call, or `0` when unknown. */
1672
+ line: number;
1673
+ /** The `ctx.storage` method invoked. */
1674
+ method: "generateUploadUrl" | "getPresignedUrl" | "getSignedUrl" | "store" | "upload";
1675
+ /** Options-object keys present at the call site (empty when not `analyzable`, or when no options argument was passed). */
1676
+ presentKeys: string[];
1677
+ }
1678
+ /**
1679
+ * One discovered `httpAction`/`httpRoute` handler in `lunora/` that performs a
1680
+ * side effect (`ctx.runMutation` / `ctx.runAction` / a `ctx.db.{insert,patch,
1681
+ * replace,delete,insertManyUnsafe}` write) from the HTTP edge, with whether it
1682
+ * reads `ctx.auth` — the `http_action_missing_auth_guard` lint input. A handler
1683
+ * that mutates state or dispatches an action without ever consulting the request
1684
+ * identity is an unauthenticated write bypassing identity/RLS. Only handlers with
1685
+ * a statically-resolvable inline body and `ctx` binding are recorded (fail-safe
1686
+ * under-report); read-only handlers are never recorded. Structurally identical to
1687
+ * `AdvisorHttpActionGuard`.
1688
+ */
1689
+ interface HttpActionGuardIR {
1690
+ /** Export binding name of the handler (or `"<module>"` when mounted inline / not a named binding). */
1691
+ exportName: string;
1692
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1693
+ file: string;
1694
+ /** Which HTTP surface the handler is: a raw `httpAction` or a typed `httpRoute` route. */
1695
+ kind: "httpAction" | "httpRoute";
1696
+ /** 1-based line of the handler call, or `0` when unknown. */
1697
+ line: number;
1698
+ /** For an `httpRoute`, the uppercased verb (`"POST"`); absent for a raw `httpAction`. */
1699
+ method?: string;
1700
+ /** `true` when the handler reads `ctx.auth` (a direct member access or a `const { auth } = ctx` destructure). */
1701
+ readsAuth: boolean;
1702
+ /** The first side effect found, as a stable label: `runMutation`, `runAction`, or `db.<method>`. */
1703
+ sideEffect: string;
1704
+ }
1705
+ /**
1706
+ * One response-header write, inside an `httpAction` handler, whose value is derived
1707
+ * from raw request input (`request.headers`, `request.url`/query, `await
1708
+ * request.json()`) with no CR/LF sanitizer — the
1709
+ * `http_action_response_header_injection` lint input. A `Request`-derived string
1710
+ * placed verbatim into a response header lets a caller smuggle `\r\n` and inject
1711
+ * extra headers or split the response (header injection / response splitting). Only
1712
+ * sites whose value is request-tainted AND unguarded are recorded: a value routed
1713
+ * through a CR/LF guard (`isSafeHeaderValue`), a URL/URI encoder
1714
+ * (`encodeURIComponent`/`encodeURI`), a numeric coercion (`Number`/`parseInt`/
1715
+ * `parseFloat`), or `btoa` is treated as safe and never recorded (`String(...)` /
1716
+ * `.toString()` are NOT sanitizers — they don't strip CR/LF). Structurally
1717
+ * identical to `AdvisorHttpHeaderWrite`.
1718
+ */
1719
+ interface HttpHeaderWriteIR {
1720
+ /** Export binding name of the enclosing handler, or `"<module>"` when mounted inline. */
1721
+ exportName: string;
1722
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1723
+ file: string;
1724
+ /** The header name being written (`"location"`), or `""` when the key is not a string literal. */
1725
+ headerName: string;
1726
+ /** 1-based line of the request-tainted header value. */
1727
+ line: number;
1728
+ /** How the header was written. */
1729
+ via: "headers-append" | "headers-ctor" | "headers-set" | "response-init";
1730
+ }
1731
+ /**
1732
+ * One rate-limit / Turnstile middleware call in `lunora/` — the
1733
+ * `ratelimit_middleware_fail_open` lint input. `rateLimit`/`dbRateLimit`
1734
+ * (`@lunora/ratelimit`) and `verifyTurnstileMiddleware` (`@lunora/auth`) each
1735
+ * accept a `failOpen` escape hatch that admits every request when the
1736
+ * limiter/siteverify is unavailable; `failOpen` is `true` only when the options
1737
+ * literal set it to the boolean literal `true` (anything else is fail-closed).
1738
+ * The lint escalates a fail-open guard to a finding when the guarded procedure
1739
+ * (`exportName`/`limitName`) looks auth/payment-sensitive. Structurally
1740
+ * identical to `AdvisorFailOpenGuard`.
1741
+ */
1742
+ interface FailOpenGuardIR {
1743
+ /** The middleware factory at the call site: `rateLimit` / `dbRateLimit` / `verifyTurnstileMiddleware`. */
1744
+ callee: string;
1745
+ /** Export binding name of the procedure the guard is attached to, or `"<module>"` at file scope. */
1746
+ exportName: string;
1747
+ /** `true` only when the options literal set `failOpen: true` as a boolean literal; a non-literal or absent option is treated as fail-closed. */
1748
+ failOpen: boolean;
1749
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1750
+ file: string;
1751
+ /** The rate-limit `name` (second string argument) for `rateLimit`/`dbRateLimit`; `""` for `verifyTurnstileMiddleware`. */
1752
+ limitName: string;
1753
+ /** 1-based line of the middleware call, or `0` when unknown. */
1754
+ line: number;
1755
+ }
1756
+ /**
1757
+ * One `ctx.flags.boolean("key", <boolean-literal>)` read in `lunora/` — the
1758
+ * `flag_gates_security_with_unsafe_default` lint input. OpenFeature returns the
1759
+ * `defaultValue` when the provider errors, so a fail-open default on a
1760
+ * security-shaped key silently opens access during an outage. Only reads with a
1761
+ * statically-known string key and boolean-literal default are recorded; the lint
1762
+ * owns the security-shape + polarity judgment. Structurally identical to
1763
+ * `AdvisorFlagSecurityDefault`.
1764
+ */
1765
+ interface FlagSecurityDefaultIR {
1766
+ /** The boolean-literal default returned on a provider outage (fail-open value). */
1767
+ defaultValue: boolean;
1768
+ /** Export binding name of the procedure performing the flag read, or `"<module>"` at file scope. */
1769
+ exportName: string;
1770
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1771
+ file: string;
1772
+ /** The flag key — the first string-literal argument of `ctx.flags.boolean`. */
1773
+ key: string;
1774
+ /** 1-based line of the `ctx.flags.boolean` call, or `0` when unknown. */
1775
+ line: number;
1776
+ }
1777
+ /**
1778
+ * One `generateText` / `streamText` call in `lunora/` whose `tools` reach a
1779
+ * privileged side effect (a DB write, function dispatch, or outbound
1780
+ * fetch/mail/queue send). `userInputDerived` records whether the model input
1781
+ * (`prompt`/`messages`/`system`) flows from the handler's `args`; the
1782
+ * `ai_tool_side_effect_prompt_injection` lint fires only when it does.
1783
+ * Structurally identical to `AdvisorAiToolSideEffect`.
1784
+ */
1785
+ interface AiToolSideEffectIR {
1786
+ /** Export binding name of the procedure performing the call. */
1787
+ exportName: string;
1788
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1789
+ file: string;
1790
+ /** 1-based line of the generation call, or `0` when unknown. */
1791
+ line: number;
1792
+ /** The generation entrypoint invoked. */
1793
+ method: "generateText" | "streamText";
1794
+ /** The privileged side-effect sink a model-callable tool reaches (`ctx.db.insert`, `ctx.run`, `ctx.fetch`, …). */
1795
+ sideEffect: string;
1796
+ /** `true` when a model-input option is derived from the handler's `args` (a bare `args.x`, or a name destructured from `args`). */
1797
+ userInputDerived: boolean;
1798
+ }
1799
+ /**
1800
+ * One `<receiver>.identity.<key>` claim read in `lunora/`, where `<receiver>` is
1801
+ * an RLS/mask policy `auth` (or `ctx.auth`/`context.auth`). `declared` records
1802
+ * whether `<key>` is in the app's `defineIdentity({ ... })` contract (or the
1803
+ * always-present `userId`); the `identity_undeclared_claim_trusted` lint fires on
1804
+ * the undeclared reads. Emitted only when a resolvable identity contract exists.
1805
+ * Structurally identical to `AdvisorIdentityClaimRead`.
1806
+ */
1807
+ interface IdentityClaimReadIR {
1808
+ /** `true` when `key` is a declared claim (in the `defineIdentity` contract, or the always-present `userId`). */
1809
+ declared: boolean;
1810
+ /** Export binding name of the enclosing declaration (`<module>` at file scope). */
1811
+ exportName: string;
1812
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1813
+ file: string;
1814
+ /** The claim key read off the identity bag. */
1815
+ key: string;
1816
+ /** 1-based line of the read, or `0` when unknown. */
1817
+ line: number;
1818
+ }
1819
+ /**
1820
+ * One payment webhook-adapter construction in `lunora/` (`createStripeAdapter` /
1821
+ * `createPolarAdapter` / `createAutumnAdapter` / `createDodoPaymentsAdapter`).
1822
+ * `toleranceSeconds` carries the statically-known `webhookToleranceSeconds`
1823
+ * replay window when it is a plain numeric literal; the payment-webhook
1824
+ * wide-tolerance lint fires when it exceeds a conservative ceiling. Structurally
1825
+ * identical to `AdvisorPaymentWebhook`.
1826
+ */
1827
+ interface PaymentWebhookIR {
1828
+ /** The adapter factory invoked. */
1829
+ callee: "createAutumnAdapter" | "createDodoPaymentsAdapter" | "createPolarAdapter" | "createStripeAdapter";
1830
+ /** Export binding name of the enclosing declaration (`<module>` at file scope). */
1831
+ exportName: string;
1832
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1833
+ file: string;
1834
+ /** 1-based line of the construction, or `0` when unknown. */
1835
+ line: number;
1836
+ /** Statically-known `webhookToleranceSeconds` literal, when present and a plain numeric literal. */
1837
+ toleranceSeconds?: number;
1838
+ }
1839
+ /**
1840
+ * One `ctx.db.<table>.findMany({ includeDeleted })` list read whose
1841
+ * `includeDeleted` is either a hardcoded `true` or derived from the handler's
1842
+ * `args` — the `soft_delete_include_deleted_from_args` lint input. The lint joins
1843
+ * `table` against the schema's soft-delete tables and `visibility` against
1844
+ * `.public()` before flagging. Structurally identical to `AdvisorSoftDeleteRead`
1845
+ * so values pass straight through without conversion.
1846
+ */
1847
+ interface SoftDeleteReadIR {
1848
+ /** Export binding name of the procedure performing the read. */
1849
+ exportName: string;
1850
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1851
+ file: string;
1852
+ /** `true` when `includeDeleted` was derived from the handler's `args` (any caller can flip it). */
1853
+ fromArgs: boolean;
1854
+ /** `true` when `includeDeleted` was a hardcoded `true` literal (always resurfaces soft-deleted rows). */
1855
+ hardcodedTrue: boolean;
1856
+ /** 1-based line of the read call. */
1857
+ line: number;
1858
+ /** Table read, or `""` when the table-arg form's first argument wasn't a string literal. */
1859
+ table: string;
1860
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1861
+ visibility: "internal" | "public";
1862
+ }
1863
+ /**
1864
+ * One `ctx.db.<table>.findMany({ with: { <rel> } })` relation-hydrating list read
1865
+ * — the `masked_relation_leak_via_with` lint input. Column masking does not
1866
+ * descend into `with`-hydrated relations, so a masked table surfaced only through
1867
+ * a `with` on an unprotected parent read is returned in the clear. The lint
1868
+ * resolves each relation accessor to its target table and joins it against the
1869
+ * discovered mask evidence before flagging. Structurally identical to
1870
+ * `AdvisorRelationLoad` so values pass straight through without conversion.
1871
+ */
1872
+ interface RelationLoadIR {
1873
+ /** Export binding name of the procedure performing the read. */
1874
+ exportName: string;
1875
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1876
+ file: string;
1877
+ /** 1-based line of the read call. */
1878
+ line: number;
1879
+ /** Parent table the read targets, or `""` when the table-arg form's first argument wasn't a string literal. */
1880
+ parentTable: string;
1881
+ /** Relation accessor names named in the read's `with: { … }` map — matched against the parent table's declared relations. */
1882
+ relations: string[];
1883
+ /** `"internal"` for `internalQuery` / `internalMutation` / `internalAction`. */
1884
+ visibility: "internal" | "public";
1885
+ }
1886
+ /**
1887
+ * One `query` handler whose `return` hands back the raw rows of a table — the
1888
+ * result of a `ctx.db.<table>.findMany()` / `.findFirst()` / `.get()` read, or a
1889
+ * `ctx.db.query("<table>")…collect()` fluent chain — returned directly (or through
1890
+ * one local `const` hop) with no hand-built projection. The
1891
+ * `output_projection_missing_on_public_read` lint keeps only `visibility ===
1892
+ * "public"` rows with no `.output(...)` / `.use(mask(...))` on the chain, then
1893
+ * joins `table` against the schema and flags one whose columns are PII-named.
1894
+ * Structurally identical to `AdvisorRawRowReturn` so values pass straight through
1895
+ * without conversion.
1896
+ */
1897
+ interface RawRowReturnIR {
1898
+ /** Export binding name of the query returning the raw rows. */
1899
+ exportName: string;
1900
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1901
+ file: string;
1902
+ /** 1-based line of the `return` (or concise-body) expression. */
1903
+ line: number;
1904
+ /** Table whose raw rows are returned, or `""` when the read's table wasn't a string literal. */
1905
+ table: string;
1906
+ /** `true` when the procedure's builder chain carries a `.use(mask(...))` step. */
1907
+ usesMask: boolean;
1908
+ /** `true` when the procedure's builder chain carries an `.output(...)` return-shape projection. */
1909
+ usesOutput: boolean;
1910
+ /** `"internal"` for `internalQuery`; `"public"` for `query`. */
1911
+ visibility: "internal" | "public";
1912
+ }
1913
+ /**
1914
+ * One `query`/`mutation` handler that gates a `ctx.db.get`/`patch`/`delete` on a
1915
+ * null-checked `ctx.db.normalizeId(table, id)` result — the
1916
+ * `normalize_id_used_as_authorization` lint input. `normalizeId` validates an id's
1917
+ * structural shape only (it never reads the database), so a non-null result proves
1918
+ * the id is well-formed, never that the caller owns the row; gating access on it is
1919
+ * an IDOR. The lint owns the negative proof — it keeps only `visibility === "public"`
1920
+ * rows with no `.use(rls(...))` and no ownership/identity mention (`mentionsOwnership`),
1921
+ * then joins `table` against the schema's RLS mode before flagging. Structurally
1922
+ * identical to `AdvisorNormalizeIdAuthorization` so values pass straight through.
1923
+ */
1924
+ interface NormalizeIdAuthorizationIR {
1925
+ /** Export binding name of the procedure performing the normalize-then-access. */
1926
+ exportName: string;
1927
+ /** Source file relative to `<projectRoot>/lunora/`, without extension. */
1928
+ file: string;
1929
+ /** 1-based line of the `ctx.db.normalizeId(...)` call the access is gated on. */
1930
+ line: number;
1931
+ /** `true` when the handler anywhere reads an ownership-named identifier or `ctx.auth`/`ctx.identity`/… — an intervening ownership signal. */
1932
+ mentionsOwnership: boolean;
1933
+ /** The id-first `ctx.db` sink the normalized id reaches. */
1934
+ sinkMethod: "delete" | "get" | "patch";
1935
+ /** Table named in the `normalizeId` call, or `""` when its table argument wasn't a string literal. */
1936
+ table: string;
1937
+ /** `true` when the procedure's builder chain carries a `.use(rls(...))` step. */
1938
+ usesRls: boolean;
1939
+ /** `"internal"` for `internalQuery`/`internalMutation`; `"public"` for `query`/`mutation`. */
1940
+ visibility: "internal" | "public";
1941
+ }
1942
+ /**
1943
+ * One committed `wrangler.jsonc` `vars` entry whose value is a plaintext secret —
1944
+ * the `plaintext_secret_in_wrangler_vars` lint input. `vars` are baked into the
1945
+ * deployed Worker in cleartext and checked into source control, so a real API key
1946
+ * / token / private key there ships the secret to every reader of the repo and the
1947
+ * bundle; it belongs in a Secrets Store binding or `wrangler secret put`. Produced
1948
+ * by `@lunora/config` (which reads `wrangler.jsonc`), not a ts-morph feeder —
1949
+ * codegen only passes it through. Structurally identical to `AdvisorWranglerVariable`.
1950
+ */
1951
+ interface WranglerVariableIR {
1952
+ /** The `wrangler.jsonc` file the var was read from, relative to the project root. */
1953
+ file: string;
1954
+ /** The offending `vars` key (e.g. `STRIPE_SECRET_KEY`). */
1955
+ key: string;
1956
+ /** Heuristic that matched, e.g. `stripe_live_key` / `private_key` / `secret_named_var`. */
1957
+ kind: string;
1958
+ /** Redacted preview of the value (first few chars + length) for the finding detail — never the full secret. */
1959
+ preview: string;
1960
+ }
1961
+ interface ProjectIR {
1962
+ crons: ReadonlyArray<CronJobIR>;
1963
+ functions: ReadonlyArray<FunctionIR>;
1964
+ /** Typed REST routes discovered from `httpRoute.<verb>(...)` builder chains. */
1965
+ httpRoutes: ReadonlyArray<HttpRouteIR>;
1966
+ migrations: ReadonlyArray<MigrationIR>;
1967
+ schema: SchemaIR;
1968
+ }
1969
+ /**
1970
+ * One import of a migrated-away platform's SDK still present in `lunora/`
1971
+ * source. Structurally identical to the advisor's `AdvisorStaleMigrationImport`
1972
+ * so it passes through the feeder without conversion.
1973
+ */
1974
+ interface StaleMigrationImportIR {
1975
+ /** Source file relative to the lunora dir, no extension. */
1976
+ file: string;
1977
+ /** 1-based line of the import, or `0` when unknown. */
1978
+ line: number;
1979
+ /** The imported module, e.g. `@supabase/supabase-js`. */
1980
+ moduleSpecifier: string;
1981
+ /** Which migration guide covers this platform. */
1982
+ platform: "convex" | "firebase" | "supabase";
1983
+ }
1984
+ /**
1985
+ * Named inputs for {@link lintSchema}. Every feeder is a discrete key rather than
1986
+ * a positional argument: the feeder list grows every few releases and many IR
1987
+ * types are structurally similar (`{file, exportName, line}`-shaped evidence),
1988
+ * so a positional call was a silent-transposition hazard — swapping two adjacent
1989
+ * arguments could typecheck yet feed the wrong evidence to the wrong lint and
1990
+ * corrupt a security advisory. `schema` is the only required field; every other
1991
+ * feeder defaults to "not analyzed" when omitted.
1992
+ */
1993
+ interface LintSchemaOptions {
1994
+ adminRoutes?: ReadonlyArray<AdminRouteIR>;
1995
+ aiRawRuns?: ReadonlyArray<AiRawRunIR>;
1996
+ aiToolSideEffects?: ReadonlyArray<AiToolSideEffectIR>;
1997
+ argumentDerivedFetches?: ReadonlyArray<ArgumentDerivedFetchIR>;
1998
+ argumentValidators?: ReadonlyArray<ArgumentValidatorIR>;
1999
+ authApiCalls?: ReadonlyArray<AuthApiCallIR>;
2000
+ authConfigs?: ReadonlyArray<AuthConfigIR>;
2001
+ browserUrlAccesses?: ReadonlyArray<BrowserUrlAccessIR>;
2002
+ configCalls?: ReadonlyArray<ConfigCallIR>;
2003
+ containerKeyAccesses?: ReadonlyArray<ContainerKeyAccessIR>;
2004
+ containerOverrides?: ReadonlyArray<ContainerOverrideIR>;
2005
+ containers?: ReadonlyArray<ContainerIR>;
2006
+ exportSinks?: ReadonlyArray<AdvisorExportSink>;
2007
+ failOpenGuards?: ReadonlyArray<FailOpenGuardIR>;
2008
+ flagSecurityDefaults?: ReadonlyArray<FlagSecurityDefaultIR>;
2009
+ geoIndexUsages?: ReadonlyArray<AdvisorGeoIndexUsage>;
2010
+ httpActionGuards?: ReadonlyArray<HttpActionGuardIR>;
2011
+ httpHeaderWrites?: ReadonlyArray<HttpHeaderWriteIR>;
2012
+ identityClaimReads?: ReadonlyArray<IdentityClaimReadIR>;
2013
+ imageDeliveryUrlAccesses?: ReadonlyArray<ImageDeliveryUrlAccessIR>;
2014
+ inserts?: ReadonlyArray<InsertWriteIR>;
2015
+ kvKeyAccesses?: ReadonlyArray<KvKeyAccessIR>;
2016
+ mailRecipientAccesses?: ReadonlyArray<MailRecipientAccessIR>;
2017
+ maskProcedures?: ReadonlyArray<MaskProcedureIR>;
2018
+ maskStrategies?: ReadonlyArray<MaskStrategyIR>;
2019
+ mutatorWrites?: ReadonlyArray<MutatorWriteIR>;
2020
+ nondeterministicCalls?: ReadonlyArray<NondeterministicCallIR>;
2021
+ normalizeIdAuthorizations?: ReadonlyArray<NormalizeIdAuthorizationIR>;
2022
+ notifyCalls?: ReadonlyArray<AdvisorNotifyCall>;
2023
+ notifyConfig?: AdvisorNotifyConfig;
2024
+ ownerFieldWrites?: ReadonlyArray<OwnerFieldWriteIR>;
2025
+ paymentWebhooks?: ReadonlyArray<PaymentWebhookIR>;
2026
+ privilegedDispatches?: ReadonlyArray<PrivilegedDispatchIR>;
2027
+ procedureProtections?: ReadonlyArray<ProcedureMiddlewareIR>;
2028
+ queries?: ReadonlyArray<QueryReadIR>;
2029
+ queues?: ReadonlyArray<QueueIR>;
2030
+ r2sqlCalls?: ReadonlyArray<R2sqlCallIR>;
2031
+ ratelimitKeySelectors?: ReadonlyArray<RatelimitKeySelectorIR>;
2032
+ rawRowReturns?: ReadonlyArray<RawRowReturnIR>;
2033
+ relationLoads?: ReadonlyArray<RelationLoadIR>;
2034
+ rlsProcedures?: ReadonlyArray<RlsProcedureIR>;
2035
+ schema: SchemaIR;
2036
+ secretLiterals?: ReadonlyArray<SecretLiteralIR>;
2037
+ shapes?: ReadonlyArray<ShapeIR>;
2038
+ softDeleteReads?: ReadonlyArray<SoftDeleteReadIR>;
2039
+ sqlInterpolations?: ReadonlyArray<SqlInterpolationIR>;
2040
+ staleMigrationImports?: ReadonlyArray<StaleMigrationImportIR>;
2041
+ storageKeyAccesses?: ReadonlyArray<StorageKeyAccessIR>;
2042
+ storageUploads?: ReadonlyArray<StorageUploadIR>;
2043
+ unrestrictedWhereBranches?: ReadonlyArray<UnrestrictedWhereBranchIR>;
2044
+ vectorNamespaceAccesses?: ReadonlyArray<VectorNamespaceAccessIR>;
2045
+ workflowCalls?: ReadonlyArray<WorkflowCallIR>;
2046
+ workflows?: ReadonlyArray<WorkflowIR>;
2047
+ wranglerVariables?: ReadonlyArray<WranglerVariableIR>;
2048
+ }
2049
+ /**
2050
+ * Normalize feeder options into the advisor's {@link LintContext} — the input
2051
+ * both `runAdvisor` and `scoreAdvisor` take. Shared by {@link lintSchema} so the
2052
+ * lint run and the scored map always see byte-identical evidence.
2053
+ *
2054
+ * Exported instead of a `mapSchema(options)` convenience that lints *and* scores:
2055
+ * such a wrapper would either re-run every rule or need a `findings` escape hatch
2056
+ * nothing could validate against its `options`, so mismatched findings would
2057
+ * silently produce a wrong map. Two lines at the call site buys that away:
2058
+ *
2059
+ * ```ts
2060
+ * const context = toAdvisorContext(options);
2061
+ * const map = scoreAdvisor(context.procedureProtections ?? [], runAdvisor(context, { source: "static" }));
2062
+ * ```
2063
+ */
2064
+ declare const toAdvisorContext: (options: LintSchemaOptions) => LintContext;
2065
+ /**
2066
+ * Run the static lints against a discovered {@link SchemaIR} and the reads/writes/calls
2067
+ * found in function bodies: query reads feed `filter_without_index`, insert writes
2068
+ * feed `table_without_insert`, authApi calls feed `auth_api_call_without_headers`,
2069
+ * rls procedure snapshots feed `rls_uncovered_table`, mask procedure
2070
+ * snapshots feed `mask_uncovered_pii_column`, and per-column mask strategies
2071
+ * feed `mask_weak_hash_strategy_on_pii`; declared containers
2072
+ * feed the `container_*` lints; declared workflows (with their durable step labels)
2073
+ * + `ctx.workflows.get(...)` call sites feed the `workflow_unused` /
2074
+ * `workflow_unknown_target` / duplicate-step-name lints; non-deterministic
2075
+ * calls inside query/mutation handlers feed the `nondeterministic_query_mutation` lint
2076
+ * (all default empty for callers that don't analyze functions/containers/workflows).
2077
+ * The IR types are structurally identical to the advisor's evidence types so they
2078
+ * pass straight through without conversion. Returns the findings; surfacing them
2079
+ * (console, error overlay, studio Advisors table) is the caller's choice.
2080
+ */
2081
+ declare const lintSchema: (options: LintSchemaOptions) => Finding[];
2082
+ /**
2083
+ * Render advisor findings as a single multi-line string for console surfacing:
2084
+ * a one-line summary header followed by one `[LEVEL] name: detail` line per
2085
+ * finding. Returns `""` when there are no findings.
2086
+ */
2087
+ declare const formatAdvisories: (findings: ReadonlyArray<Finding>) => string;
2088
+ /**
2089
+ * The canonical capability list. **Order is load-bearing** for the `emit-app.ts`
2090
+ * long-tail: the fluent methods are emitted in the order the `appMethod`-bearing
2091
+ * rows appear here, so this array is ordered to reproduce the original
2092
+ * `LONG_TAIL` sequence (ai, analytics, browser, hyperdrive, images, kv, payment,
2093
+ * r2sql, vectors). The `serverCtxField` rows are referenced by name in the ctx
2094
+ * interface templates, so their order here is not output-affecting.
2095
+ */
2096
+ declare const CAPABILITY_ROWS: readonly [{
2097
+ readonly contextProperty: "access";
2098
+ readonly key: "access";
2099
+ readonly moduleSpecifier: "@lunora/cloudflare-access";
2100
+ }, {
2101
+ readonly appMethod: {
2102
+ readonly configKey: "ai";
2103
+ readonly doc: "Override the Workers AI binding backing `ctx.ai` (defaults to `env.AI`).";
2104
+ readonly method: "ai";
2105
+ };
2106
+ readonly contextProperty: "ai";
2107
+ readonly key: "ai";
2108
+ readonly moduleSpecifier: "@lunora/ai";
2109
+ }, {
2110
+ readonly appMethod: {
2111
+ readonly configKey: "analytics";
2112
+ readonly doc: "Override the Analytics Engine dataset backing `ctx.analytics` (defaults to `env.ANALYTICS`).";
2113
+ readonly method: "analytics";
2114
+ };
2115
+ readonly contextProperty: "analytics";
2116
+ readonly key: "analytics";
2117
+ readonly moduleSpecifier: "@lunora/bindings/analytics";
2118
+ readonly serverCtxField: {
2119
+ readonly field: "\n /** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */\n readonly analytics: import(\"@lunora/bindings/analytics\").AnalyticsClient;";
2120
+ readonly tier: "every";
2121
+ };
2122
+ }, {
2123
+ readonly appMethod: {
2124
+ readonly configKey: "browser";
2125
+ readonly doc: "Override the Browser Rendering binding backing `ctx.browser` (defaults to `env.BROWSER`).";
2126
+ readonly method: "browser";
2127
+ };
2128
+ readonly contextProperty: "browser";
2129
+ readonly key: "browser";
2130
+ readonly moduleSpecifier: "@lunora/browser";
2131
+ readonly serverCtxField: {
2132
+ readonly field: "\n /** Browser Rendering (screenshots/PDF/scrape). Non-deterministic — available only in actions. */\n readonly browser: import(\"@lunora/browser\").Browser;";
2133
+ readonly tier: "action";
2134
+ };
2135
+ }, {
2136
+ readonly contextProperty: "containers";
2137
+ readonly key: "container";
2138
+ readonly moduleSpecifier: "@lunora/container";
2139
+ }, {
2140
+ readonly contextProperty: "flags";
2141
+ readonly key: "flags";
2142
+ readonly moduleSpecifier: "@lunora/flags";
2143
+ }, {
2144
+ readonly appMethod: {
2145
+ readonly configKey: "sql";
2146
+ readonly doc: "Wire the Hyperdrive SQL client backing `ctx.sql` — build it with `createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`.";
2147
+ readonly method: "hyperdrive";
2148
+ };
2149
+ readonly contextProperty: "sql";
2150
+ readonly key: "hyperdrive";
2151
+ readonly moduleSpecifier: "@lunora/hyperdrive";
2152
+ readonly serverCtxField: {
2153
+ readonly field: "\n /**\n * External database access via Hyperdrive. Non-deterministic — available only in actions. Writes here are NOT tracked by Lunora live queries; subscriptions will not re-run on external DB changes.\n */\n readonly sql: import(\"@lunora/hyperdrive\").SqlClient;";
2154
+ readonly tier: "action";
2155
+ };
2156
+ }, {
2157
+ readonly appMethod: {
2158
+ readonly configKey: "images";
2159
+ readonly doc: "Override the Images binding backing `ctx.images` (defaults to `env.IMAGES`).";
2160
+ readonly method: "images";
2161
+ };
2162
+ readonly contextProperty: "images";
2163
+ readonly key: "images";
2164
+ readonly moduleSpecifier: "@lunora/bindings/images";
2165
+ readonly serverCtxField: {
2166
+ readonly field: "\n /** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */\n readonly images: import(\"@lunora/bindings/images\").Images;";
2167
+ readonly tier: "action";
2168
+ };
2169
+ }, {
2170
+ readonly appMethod: {
2171
+ readonly configKey: "kv";
2172
+ readonly doc: "Override the Workers KV binding backing `ctx.kv` (defaults to `env.KV`).";
2173
+ readonly method: "kv";
2174
+ };
2175
+ readonly contextProperty: "kv";
2176
+ readonly key: "kv";
2177
+ readonly moduleSpecifier: "@lunora/bindings/kv";
2178
+ readonly serverCtxField: {
2179
+ readonly field: "\n readonly kv: import(\"@lunora/bindings/kv\").Kv;";
2180
+ readonly tier: "every";
2181
+ };
2182
+ }, {
2183
+ readonly key: "mail";
2184
+ readonly moduleSpecifier: "@lunora/mail";
2185
+ }, {
2186
+ readonly contextProperty: "notify";
2187
+ readonly key: "notify";
2188
+ readonly moduleSpecifier: "@lunora/notify";
2189
+ }, {
2190
+ readonly appMethod: {
2191
+ readonly configKey: "payment";
2192
+ readonly doc: "Wire the payment options backing `ctx.payments`.";
2193
+ readonly method: "payment";
2194
+ };
2195
+ readonly contextProperty: "payments";
2196
+ readonly key: "payments";
2197
+ readonly moduleSpecifier: "@lunora/payment";
2198
+ }, {
2199
+ readonly appMethod: {
2200
+ readonly configKey: "x402";
2201
+ readonly doc: "Wire the x402 agent-wallet pay rail backing `ctx.x402` — a payment-enabled `fetch` that answers `402` challenges under a mandatory spend policy (ActionCtx-only; spends real funds).";
2202
+ readonly method: "x402";
2203
+ };
2204
+ readonly contextProperty: "x402";
2205
+ readonly key: "x402";
2206
+ readonly moduleSpecifier: "@lunora/x402/pay";
2207
+ }, {
2208
+ readonly contextProperty: "pipelines";
2209
+ readonly key: "pipelines";
2210
+ readonly moduleSpecifier: "@lunora/bindings/pipelines";
2211
+ readonly serverCtxField: {
2212
+ readonly field: "\n /** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */\n readonly pipelines: import(\"@lunora/bindings/pipelines\").PipelineClient;";
2213
+ readonly tier: "action";
2214
+ };
2215
+ }, {
2216
+ readonly appMethod: {
2217
+ readonly configKey: "r2sql";
2218
+ readonly doc: "Wire the R2 SQL client backing `ctx.r2sql` — build it with `createR2Sql({ accountId, apiToken, bucket })` (defaults to env `R2_SQL_TOKEN` / `R2_SQL_ACCOUNT_ID` / `R2_SQL_BUCKET`).";
2219
+ readonly method: "r2sql";
2220
+ };
2221
+ readonly contextProperty: "r2sql";
2222
+ readonly key: "r2sql";
2223
+ readonly moduleSpecifier: "@lunora/bindings/r2sql";
2224
+ readonly serverCtxField: {
2225
+ readonly field: "\n /**\n * R2 SQL over Apache Iceberg tables (window functions, DISTINCT, set operations). Non-deterministic — available only in actions. Reads here are NOT tracked by Lunora live queries.\n */\n readonly r2sql: import(\"@lunora/bindings/r2sql\").R2SqlClient;";
2226
+ readonly tier: "action";
2227
+ };
2228
+ }, {
2229
+ readonly contextProperty: "scheduler";
2230
+ readonly key: "scheduler";
2231
+ readonly moduleSpecifier: "@lunora/scheduler";
2232
+ }, {
2233
+ readonly contextProperty: "storage";
2234
+ readonly key: "storage";
2235
+ readonly moduleSpecifier: "@lunora/storage";
2236
+ }, {
2237
+ readonly appMethod: {
2238
+ readonly configKey: "vectors";
2239
+ readonly doc: "Wire the Vectorize index map backing `ctx.vectors`.";
2240
+ readonly method: "vectors";
2241
+ };
2242
+ readonly contextProperty: "vectors";
2243
+ readonly key: "vectors";
2244
+ readonly moduleSpecifier: "@lunora/bindings/vectors";
2245
+ }, {
2246
+ readonly contextProperty: "workflows";
2247
+ readonly key: "workflows";
2248
+ readonly moduleSpecifier: "@lunora/workflow";
2249
+ }];
2250
+ /** The literal union of every capability id — the single source of truth for `FeatureUsage`'s keys (so they cannot drift). */
2251
+ type CapabilityKey = (typeof CAPABILITY_ROWS)[number]["key"];
2252
+ /** The default codegen target — today's behavior, byte-identical goldens. */
2253
+ declare const DEFAULT_TARGET = "cloudflare";
2254
+ /**
2255
+ * Read `target` from `<projectRoot>/lunora.json`.
2256
+ *
2257
+ * This lives in `@lunora/codegen` rather than `@lunora/config` — where the rest
2258
+ * of the `lunora.json` reading lives — because `@lunora/config` depends on
2259
+ * `@lunora/codegen`, not the reverse. Putting it there and importing it here
2260
+ * would invert that edge, so config delegates to this instead and there is
2261
+ * still exactly one parser for the key.
2262
+ *
2263
+ * Best-effort and deliberately unvalidated: a missing file, malformed JSONC, or
2264
+ * a non-string value all collapse to `undefined`, because those are shape
2265
+ * errors rather than a name the user meant. An unrecognized *name* is returned
2266
+ * as-is so the caller's registry lookup rejects it — swallowing a typo into the
2267
+ * default would ship an app to the wrong provider.
2268
+ * @param projectRoot Directory containing `lunora.json`.
2269
+ * @returns the declared target, or `undefined` when none is usable.
2270
+ */
2271
+ declare const readProjectTarget: (projectRoot: string) => string | undefined;
2272
+ /**
2273
+ * The target codegen should emit for: an explicit option wins, then
2274
+ * `lunora.json`, then the default.
2275
+ *
2276
+ * `runCodegen` applies this itself so a caller that forgets to pass a target
2277
+ * still emits the surface the project declared. That default matters more than
2278
+ * it looks: a call site that silently omits the target emits the *default*
2279
+ * surface with no diagnostic to notice, and the mismatch only shows up at
2280
+ * runtime on the deployed app.
2281
+ * @param projectRoot Directory containing `lunora.json`.
2282
+ * @param explicit A caller-supplied target, if any.
2283
+ * @returns the resolved target id — not guaranteed to be registered.
2284
+ */
2285
+ declare const resolveCodegenTarget: (projectRoot: string, explicit?: string) => string;
2286
+ /**
2287
+ * The target ids codegen can gate against.
2288
+ *
2289
+ * `@lunora/config`'s driver registry (`deployTargetIds`) used to assert
2290
+ * equality against this — "two id spaces for one concept" — on the theory that
2291
+ * a target with a matrix but no driver "gates a surface nothing can deploy."
2292
+ * Plan 234 found that reasoning incomplete by registering `node` here: a
2293
+ * codegen-gateable target and a deployable target are genuinely different
2294
+ * questions, and a spike/dev-only host answers the first "yes" and the second
2295
+ * "not yet" without that being a bug in either registry. See
2296
+ * `plans/234-node-host-findings.md` for the finding and `@lunora/config`'s
2297
+ * `project-config.test.ts` for where the now-relaxed invariant lives.
2298
+ * @returns the registered matrix ids, sorted.
2299
+ */
2300
+ declare const platformMatrixIds: () => ReadonlyArray<string>;
2301
+ /** An advisor-style diagnostic about a target's platform capabilities. */
2302
+ interface PlatformDiagnostic {
2303
+ /** The codegen capability this concerns, when it is feature-specific. */
2304
+ feature?: CapabilityKey;
2305
+ /** Severity. All three names are errors — each drops or misdirects an emitted surface. */
2306
+ level: "error" | "warn";
2307
+ /** Human-readable explanation of the gap. */
2308
+ message: string;
2309
+ /** The lint id: `platform_unsupported_feature`, `platform_undeclared_feature`, or `platform_unknown_target`. */
2310
+ name: "platform_undeclared_feature" | "platform_unknown_target" | "platform_unsupported_feature";
2311
+ /** How to resolve it. */
2312
+ remediation: string;
2313
+ /** The requested deploy target. */
2314
+ target: string;
2315
+ }
2316
+ /**
2317
+ * Committed, tracked baseline file holding the blessed structural schema
2318
+ * snapshot the pre-deploy drift gate diffs against. Lives in `lunora/` (NOT the
2319
+ * gitignored `_generated/`) so it is committed alongside `schema.ts`. Leading
2320
+ * dot keeps it tucked away next to the schema it describes.
2321
+ */
2322
+ declare const SCHEMA_SNAPSHOT_FILENAME = ".lunora-schema.json";
2323
+ /**
2324
+ * Walk up from `startPath` until we find a `tsconfig.json` or hit the file
2325
+ * system root. Returns the absolute path to the tsconfig, or `undefined`.
2326
+ *
2327
+ * Exported (see the bottom-of-file `export { findTsconfig }`) so a long-lived
2328
+ * caller (the Vite dev-loop's cached-Project invalidation) can ask "which
2329
+ * tsconfig would {@link createCodegenProject} resolve right now?" without
2330
+ * duplicating the walk — recomputing it per call is a handful of `existsSync`
2331
+ * checks, negligible next to a Project rebuild.
2332
+ */
2333
+ declare const findTsconfig: (startPath: string) => string | undefined;
2334
+ /**
2335
+ * Construct the ts-morph `Project` codegen discovers over. Prefers the user's
2336
+ * `tsconfig.json` (when one is found walking up from `lunoraDirectory`) so
2337
+ * cross-file type resolution and path aliases work; falls back to an isolated
2338
+ * project otherwise. This is the exact construction {@link runCodegen} uses
2339
+ * when no `project` is injected — exported so a long-lived caller (the Vite
2340
+ * dev-loop) can build one once and reuse it across runs via
2341
+ * {@link refreshCodegenProject} instead of re-parsing the user's whole TS
2342
+ * program on every save.
2343
+ */
2344
+ declare const createCodegenProject: (lunoraDirectory: string) => Project;
2345
+ /**
2346
+ * Synchronise a reused {@link createCodegenProject} Project with the current
2347
+ * on-disk state of `lunoraDirectory`, so the next {@link runCodegen} sees the
2348
+ * same files a freshly-constructed Project would — without re-parsing the whole
2349
+ * TS program. Adds any on-disk source file the Project doesn't yet have, and
2350
+ * `refreshFromFileSystemSync()`es the ones it does (picking up edits); then
2351
+ * removes Project source files under `lunoraDirectory` that no longer exist on
2352
+ * disk (the classic stale-deleted-file cache bug).
2353
+ *
2354
+ * Files outside `lunoraDirectory` — e.g. a shared validator or type pulled in
2355
+ * via the user's tsconfig — are also `refreshFromFileSystemSync()`ed, but only
2356
+ * the ones already loaded into the Project; none are added. `resolveValidatorAlias`
2357
+ * (parse-validator.ts) follows `getAliasedSymbol()` across module boundaries, so a
2358
+ * validator defined outside `lunoraDirectory` is genuinely read from whatever
2359
+ * source the Project currently holds — leaving those files stale made a reused
2360
+ * Project (the Vite dev loop) silently disagree with a fresh one (`lunora
2361
+ * codegen`) about the same source. `node_modules` is excluded: its `.d.ts` set
2362
+ * dominates the file count and never changes mid dev-loop, so refreshing it would
2363
+ * reinstate close to the full re-parse cost this cache exists to avoid.
2364
+ */
2365
+ declare const refreshCodegenProject: (project: Project, lunoraDirectory: string) => void;
2366
+ /**
2367
+ * Top-level codegen entry. Parses `<projectRoot>/lunora/schema.ts` and every
2368
+ * function file under `<projectRoot>/lunora/`, then writes
2369
+ * `_generated/{api,server,dataModel}.ts` next to them.
2370
+ *
2371
+ * When `LUNORA_CODEGEN_TIMING` is set (truthy), a single diagnostic summary
2372
+ * line is written to stderr with the total wall time and the discovery-vs-emit
2373
+ * split — opt-in instrumentation that is otherwise zero-cost and side-effect-free
2374
+ * on the returned {@link CodegenResult}.
2375
+ */
2376
+ declare const runCodegen: (options: CodegenOptions) => CodegenResult;
2377
+ interface CodegenOptions {
2378
+ /**
2379
+ * Which machine-readable API spec(s) to emit into `_generated/`.
2380
+ *
2381
+ * `"openapi"` (the default) writes only `openapi.json` (OpenAPI 3.1; covers
2382
+ * both the RPC functions and `httpRouter()` REST routes). `"openrpc"` writes
2383
+ * only `openrpc.json` (OpenRPC 1.x; the RPC functions only — OpenRPC cannot
2384
+ * represent REST routes). `"both"` writes both files; `"none"` writes neither.
2385
+ *
2386
+ * Regardless of the choice, `CodegenResult.generated.openApi` and `.openRpc`
2387
+ * always carry the rendered string (computation is cheap and pure); only the
2388
+ * on-disk write is gated by this option.
2389
+ */
2390
+ apiSpec?: "both" | "none" | "openapi" | "openrpc";
2391
+ /**
2392
+ * When true, run discovery + emit (so any schema/function parse error
2393
+ * surfaces) but skip writing files to `_generated/`. The returned
2394
+ * `outputDirectory` is still the path that *would* have been written.
2395
+ */
2396
+ dryRun?: boolean;
2397
+ /**
2398
+ * Run the static schema advisor (unindexed FKs, …) during codegen.
2399
+ * Defaults to `true`. When `false`, `CodegenResult.advisories` is empty.
2400
+ * Computed regardless of `dryRun`; codegen never prints them — see
2401
+ * {@link CodegenResult.advisories}.
2402
+ */
2403
+ lint?: boolean;
2404
+ /** Override the lunora subdirectory name. Defaults to `"lunora"`. */
2405
+ lunoraDirectory?: string;
2406
+ /**
2407
+ * Reuse a previously-constructed ts-morph {@link Project} instead of building
2408
+ * a fresh one each run. The caller owns refreshing its source files from disk
2409
+ * (see {@link refreshCodegenProject}) — codegen does not re-read changed files
2410
+ * off an injected Project. Built via {@link createCodegenProject} when absent.
2411
+ * Used by the Vite dev-loop to avoid re-parsing the whole TS program on every
2412
+ * save; omit it (CLI one-shot path) to get the default fresh-Project behaviour.
2413
+ */
2414
+ project?: Project;
2415
+ /** Project root containing the `lunora/` directory. */
2416
+ projectRoot: string;
2417
+ /**
2418
+ * The deploy target codegen tailors the emitted `ctx.*` surface to.
2419
+ * Defaults to `"cloudflare"` — whose capability matrix marks every feature
2420
+ * native or emulated, so the default output is unchanged (byte-identical
2421
+ * goldens). A target that marks a used feature unsupported omits its
2422
+ * `ctx.*` surface and reports it in {@link CodegenResult.platformDiagnostics}.
2423
+ * Only `"cloudflare"` is registered until other per-target `@lunora/platform`
2424
+ * matrices land; an unknown target emits the full surface un-gated and a
2425
+ * `platform_unknown_target` diagnostic.
2426
+ */
2427
+ target?: string;
2428
+ /**
2429
+ * Re-bless the committed schema-drift baseline (`lunora/.lunora-schema.json`)
2430
+ * with the current structural snapshot. The baseline is ALWAYS written on
2431
+ * first capture (when the file is absent); set this to overwrite an existing
2432
+ * one — e.g. after the developer has added the data migration that justifies
2433
+ * a breaking change. Ignored when `dryRun` is true.
2434
+ */
2435
+ updateSchemaBaseline?: boolean;
2436
+ /**
2437
+ * Committed `wrangler.jsonc` `vars` entries that hold plaintext secrets — the
2438
+ * `plaintext_secret_in_wrangler_vars` lint input. Produced by `@lunora/config`
2439
+ * (which reads `wrangler.jsonc`) and threaded through by the CLI / Vite plugin;
2440
+ * codegen only forwards it to the advisor. Absent when no wrangler config is
2441
+ * present or the caller doesn't scan it.
2442
+ */
2443
+ wranglerVariables?: ReadonlyArray<WranglerVariableIR>;
2444
+ }
2445
+ interface CodegenResult {
2446
+ /**
2447
+ * The normalized advisor evidence the findings were produced from, so a
2448
+ * caller can score it into a health map (`scoreAdvisor`) without re-running
2449
+ * discovery. `undefined` under `lint: false`.
2450
+ *
2451
+ * Deliberately not scored here: the map carries a `generatedAt` stamp, and
2452
+ * codegen's result stays a pure function of the sources.
2453
+ */
2454
+ advisorContext?: LintContext;
2455
+ /**
2456
+ * Static schema advisor findings (e.g. unindexed foreign keys) produced
2457
+ * this run. Empty when `lint` is `false` or the schema is clean. Codegen
2458
+ * does not print these itself — each caller presents them through its own
2459
+ * channel (the CLI logger, the vite overlay, the studio Advisors table).
2460
+ * `formatAdvisories` is exported for a plain multi-line rendering.
2461
+ */
2462
+ advisories: ReadonlyArray<Finding>;
2463
+ /**
2464
+ * Agents discovered from `defineAgent` exports in `lunora/agents.ts` — the
2465
+ * list the config layer reconciles into wrangler's `workflows[]` array (an
2466
+ * agent compiles onto a Cloudflare Workflow). Agents are NOT Durable Objects,
2467
+ * so this adds no binding or migration. Empty when the project declares none.
2468
+ */
2469
+ agents: ReadonlyArray<AgentIR>;
2470
+ /**
2471
+ * Containers discovered from `defineContainer` exports in
2472
+ * `lunora/containers.ts` — the list the config layer reconciles into
2473
+ * wrangler's `containers[]`, `CONTAINER_*` Durable Object bindings, and
2474
+ * migration classes. Empty when the project declares no containers.
2475
+ */
2476
+ containers: ReadonlyArray<ContainerIR>;
2477
+ /**
2478
+ * Deduplicated cron schedules discovered from `cronJobs()` definitions —
2479
+ * the array the vite plugin reconciles into `wrangler.jsonc`'s
2480
+ * `triggers.crons`. Empty when the project declares no crons.
2481
+ */
2482
+ cronTriggers: ReadonlyArray<string>;
2483
+ generated: {
2484
+ /** WorkflowEntrypoint classes for declared agents (`_generated/agents.ts`); `""` (and not written) when no agents are declared. */
2485
+ agents: string;
2486
+ api: string;
2487
+ /** Fluent worker-composition builder (`_generated/app.ts`) — `defineApp()`. Always written. */
2488
+ app: string;
2489
+ /** Partial-replication collection factories (`_generated/collections.ts`); `""` (and not written) unless the project declares shapes and installs `@lunora/db`. */
2490
+ collections: string;
2491
+ /** Container DO classes (`_generated/containers.ts`); `""` (and not written) when no containers are declared. */
2492
+ containers: string;
2493
+ crons: string;
2494
+ dataModel: string;
2495
+ drizzleGlobal: string;
2496
+ drizzleShard: string;
2497
+ functions: string;
2498
+ /** OpenAPI 3.1.0 document (`_generated/openapi.json`), pretty-printed JSON. */
2499
+ openApi: string;
2500
+ /**
2501
+ * OpenAPI document as an importable TS module (`_generated/openapi.ts`) —
2502
+ * `export const openApiSpec`, the worker imports it for
2503
+ * `createWorker({ openApiSpec })`. Same document as `openApi`. Written
2504
+ * alongside `openapi.json` whenever `apiSpec` includes `openapi`.
2505
+ */
2506
+ openApiModule: string;
2507
+ /** OpenRPC 1.x document (`_generated/openrpc.json`), pretty-printed JSON. Always computed; written only when `apiSpec` includes `openrpc`. */
2508
+ openRpc: string;
2509
+ /**
2510
+ * OpenRPC document as an importable TS module (`_generated/openrpc.ts`) —
2511
+ * `export const openRpcSpec`, for `createWorker({ openRpcSpec })`. Same
2512
+ * document as `openRpc`. Written alongside `openrpc.json` whenever
2513
+ * `apiSpec` includes `openrpc`.
2514
+ */
2515
+ openRpcModule: string;
2516
+ /** Push-consumer queue registry (`_generated/queues.ts`); `""` (and not written) when no push queues are declared. */
2517
+ queues: string;
2518
+ /** Project-bound seed client (`_generated/seed.ts`); `""` (and not written) when `@lunora/seed` is not a declared dependency. */
2519
+ seed: string;
2520
+ server: string;
2521
+ shard: string;
2522
+ /** Static vector-index registry (`_generated/vectors.ts`) — `LUNORA_VECTOR_INDEXES`. Empty array body when the schema declares none. */
2523
+ vectors: string;
2524
+ /** WorkflowEntrypoint classes (`_generated/workflows.ts`); `""` (and not written) when no workflows are declared. */
2525
+ workflows: string;
2526
+ };
2527
+ outputDirectory: string;
2528
+ /**
2529
+ * Portability diagnostics for the requested {@link CodegenOptions.target}:
2530
+ * `ctx.*` features the app uses that the target does not support (omitted
2531
+ * from the emitted surface), or an unknown target. Empty for a
2532
+ * fully-supported app on the default Cloudflare target. Presentation is the
2533
+ * caller's job, like {@link CodegenResult.advisories}.
2534
+ */
2535
+ platformDiagnostics: ReadonlyArray<PlatformDiagnostic>;
2536
+ /**
2537
+ * Queues discovered from `defineQueue` exports in `lunora/queues.ts` — the
2538
+ * list the config layer reconciles into wrangler's `queues.producers[]` /
2539
+ * `queues.consumers[]`. Queues are NOT Durable Objects, so this adds no
2540
+ * binding or migration. Empty when the project declares no queues.
2541
+ */
2542
+ queues: ReadonlyArray<QueueIR>;
2543
+ /**
2544
+ * The CURRENT structural schema snapshot computed this run (tables + field
2545
+ * kinds/optionality + indexes/relations/shard mode + declared migration ids).
2546
+ * The pre-deploy drift gate diffs this against the committed baseline read
2547
+ * from {@link CodegenResult.schemaSnapshotPath}. Always present, even on a
2548
+ * `dryRun`.
2549
+ */
2550
+ schemaSnapshot: SchemaSnapshot;
2551
+ /** Absolute path of the committed baseline file (`lunora/.lunora-schema.json`). */
2552
+ schemaSnapshotPath: string;
2553
+ /**
2554
+ * Workflows discovered from `defineWorkflow` exports in
2555
+ * `lunora/workflows.ts` — the list the config layer reconciles into
2556
+ * wrangler's `workflows[]` array. Workflows are NOT Durable Objects, so this
2557
+ * adds no binding or migration. Empty when the project declares no workflows.
2558
+ */
2559
+ workflows: ReadonlyArray<WorkflowIR>;
2560
+ }
2561
+ /** Deduplicated, sorted names of every ERROR-level advisory in `advisories`. */
2562
+ declare const errorAdvisoryNames: (advisories: ReadonlyArray<Pick<Finding, "level" | "name">>) => ReadonlyArray<string>;
2563
+ /** Deduplicated, sorted names of every error-level platform diagnostic in `platformDiagnostics`. */
2564
+ declare const errorPlatformDiagnosticNames: (platformDiagnostics: ReadonlyArray<Pick<PlatformDiagnostic, "level" | "name">>) => ReadonlyArray<string>;
2565
+ /**
2566
+ * Convenience read combining both categories from a full {@link CodegenResult}
2567
+ * — the shape the Vite plugin's `buildBlockingMessage` needs, which (unlike
2568
+ * the CLI's `lunora codegen`/`lunora deploy`) folds ERROR-level advisories and
2569
+ * platform diagnostics into a single blocking message with no strict/CI
2570
+ * opt-out.
2571
+ */
2572
+ declare const describeErrorLevelFindings: (result: Pick<CodegenResult, "advisories" | "platformDiagnostics">) => {
2573
+ advisoryNames: ReadonlyArray<string>;
2574
+ platformDiagnosticNames: ReadonlyArray<string>;
2575
+ };
2576
+ /**
2577
+ * An error thrown by codegen discovery when the user's schema or function
2578
+ * source has a structural problem that can be pinpointed to a specific source
2579
+ * location. A `LunoraError` subclass (`code: "CODEGEN_DIAGNOSTIC"`); the `file`,
2580
+ * `line`, and `column` properties (also passed through as the base `loc`) mirror
2581
+ * what Vite's error-overlay `loc` field expects so the browser can display the
2582
+ * exact spot.
2583
+ */
2584
+ declare class CodegenDiagnosticError extends LunoraError {
2585
+ readonly column: number;
2586
+ readonly file: string;
2587
+ readonly line: number;
2588
+ constructor(message: string, file: string, line: number, column: number);
2589
+ }
2590
+ /**
2591
+ * Build a {@link CodegenDiagnosticError} whose message includes the source
2592
+ * location and whose `file`/`line`/`column` properties are set from the
2593
+ * ts-morph `Node`'s position in its source file.
2594
+ *
2595
+ * Message format: `@lunora/codegen: <detail> (<file>:<line>:<column>)`
2596
+ *
2597
+ * `meta` is merged onto the returned error for callers that also carry the
2598
+ * project-wide `LunoraError` envelope (`code`/`name`/`status`) — it never
2599
+ * touches `file`/`line`/`column`, and the error stays an instance of
2600
+ * {@link CodegenDiagnosticError} so the Vite overlay's `instanceof` location
2601
+ * lookup is unaffected.
2602
+ */
2603
+ declare const diagnosticAt: (node: Node, detail: string, meta?: Record<string, unknown>) => CodegenDiagnosticError;
2604
+ /** The only file agents may be declared in — mirrors `lunora/workflows.ts`. */
2605
+ declare const AGENTS_FILENAME = "agents.ts";
2606
+ /**
2607
+ * Discover every agent the project declares: exported `defineAgent()` calls in
2608
+ * `lunora/agents.ts`. Returns `[]` when the file doesn't exist. Only four things
2609
+ * are read statically — the optional `name` override (wrangler `workflows[].name`),
2610
+ * the optional `publicRun` opt-in (the `agents:agentRun` capability gate), the
2611
+ * presence of a `voice` block (which turns on the voice-session Durable Object),
2612
+ * and the presence of an `onEmail` mapper (which wires the worker `email()`
2613
+ * handler); the rest of the agent config (model / tools / memory / voice models /
2614
+ * the `onEmail` closure body) is runtime-only, so codegen never evaluates it.
2615
+ */
2616
+ declare const discoverAgents: (project: Project, lunoraDirectory: string) => AgentIR[];
2617
+ /**
2618
+ * Discover `ctx.authApi.<method>(...)` (and bare `authApi.<method>(...)`) calls
2619
+ * under the lunora source directory and attribute each to the exported function
2620
+ * (and file) performing it. Calls outside an exported declaration are dropped.
2621
+ */
2622
+ declare const discoverAuthApiCalls: (project: Project, lunoraDirectory: string) => AuthApiCallIR[];
2623
+ /** The only file containers may be declared in — mirrors `lunora/crons.ts`. */
2624
+ declare const CONTAINERS_FILENAME = "containers.ts";
2625
+ /**
2626
+ * Discover every container the project declares: exported `defineContainer()`
2627
+ * calls in `lunora/containers.ts`. Returns `[]` when the file doesn't exist.
2628
+ * Wrangler-relevant fields (`image`, `instanceType`, `maxInstances`, `name`)
2629
+ * must be static literals; runtime-only fields (`env`, `sleepAfter`, …) may be
2630
+ * any expression since the generated class imports the definition object.
2631
+ */
2632
+ declare const discoverContainers: (project: Project, lunoraDirectory: string) => ContainerIR[];
2633
+ /**
2634
+ * Scan every `.ts` file under `lunoraDir` for `cronJobs()` builder registrations
2635
+ * (`crons.interval(...)`, `crons.daily(...)`, `crons.cron(...)`, …) and lift them
2636
+ * into {@link CronJobIR}. Schedules are compiled to standard cron expressions;
2637
+ * function references are resolved to their `namespace:fn` dispatch path, while a
2638
+ * `workflows.NAME` / `agents.NAME` reference (or a bare identifier naming a
2639
+ * declared workflow) resolves to a durable workflow start. Names must be unique
2640
+ * across the project.
2641
+ */
2642
+ declare const discoverCrons: (project: Project, lunoraDirectory: string, workflows?: ReadonlyArray<WorkflowIR>, agents?: ReadonlyArray<AgentIR>) => CronJobIR[];
2643
+ /** The only file a feature-flag provider may be declared in — mirrors `lunora/queues.ts`. */
2644
+ declare const FLAGS_FILENAME = "flags.ts";
2645
+ /**
2646
+ * Discover the feature-flag provider a project declares in `lunora/flags.ts`.
2647
+ * Returns `undefined` when the file doesn't exist (the app has no flags). The
2648
+ * read is metadata-only and lenient: codegen wires `ctx.flags` purely from the
2649
+ * file's *existence* (`run-codegen.ts`) and imports the real module for the
2650
+ * provider value — this IR exists solely so the config layer can reconcile the
2651
+ * wrangler `flagship` binding for the Flagship binding-mode provider. Anything
2652
+ * it can't read statically degrades to a `custom` provider (no binding), never
2653
+ * a thrown error.
2654
+ */
2655
+ declare const discoverFlags: (project: Project, lunoraDirectory: string) => FlagsIR | undefined;
2656
+ /**
2657
+ * Scan all .ts files under `lunoraDir` (skipping `_generated/` and `schema.ts`)
2658
+ * for top-level `export const x = query/mutation/action({...})` registrations.
2659
+ */
2660
+ declare const discoverFunctions: (project: Project, lunoraDirectory: string) => FunctionIR[];
2661
+ /**
2662
+ * Scan all `.ts` files under `lunoraDir` (skipping `_generated/` and `schema.ts`)
2663
+ * for `export const x = httpRoute.<verb>(...)…handler(...)` typed REST routes.
2664
+ * These are the headline OpenAPI target: each becomes a real `paths` entry.
2665
+ */
2666
+ declare const discoverHttpRoutes: (project: Project, lunoraDirectory: string) => HttpRouteIR[];
2667
+ /**
2668
+ * Discover `ctx.db.insert("table", …)` writes under the lunora source directory
2669
+ * and attribute each to the exported function (and file) performing it. Calls
2670
+ * with a non-literal table argument, or outside an exported declaration, are
2671
+ * dropped (`table === ""` / no enclosing export).
2672
+ */
2673
+ declare const discoverInserts: (project: Project, lunoraDirectory: string) => InsertWriteIR[];
2674
+ /**
2675
+ * Discover masking usage for every exported Lunora procedure under the lunora
2676
+ * source directory — the column-level twin of `discoverRlsProcedures`. For each
2677
+ * procedure, records whether its builder chain includes `.use(mask(...))`, which
2678
+ * `(table, column)` pairs that mask declares, and which tables it reads/writes
2679
+ * through `ctx.db`. Feeds the `mask_uncovered_pii_column` advisor lint.
2680
+ */
2681
+ declare const discoverMaskProcedures: (project: Project, lunoraDirectory: string) => MaskProcedureIR[];
2682
+ /**
2683
+ * Scan all `.ts` files under `lunoraDir` for top-level
2684
+ * `export const x = defineMigration({...})` declarations and lift them into
2685
+ * {@link MigrationIR}. `id` must be a static string literal (it's the registry
2686
+ * key); `table` is best-effort and left `""` when not a literal.
2687
+ */
2688
+ declare const discoverMigrations: (project: Project, lunoraDirectory: string) => MigrationIR[];
2689
+ /** The only file custom mutators may be declared in — mirrors `lunora/queues.ts`. */
2690
+ declare const MUTATORS_FILENAME = "mutators.ts";
2691
+ /**
2692
+ * Discover every custom mutator the project declares: exported
2693
+ * `defineMutator()` calls in `lunora/mutators.ts`. Returns `[]` when the file
2694
+ * doesn't exist. The export binding plus the declared `args` / `server` return
2695
+ * type are lifted — enough to emit a typed `api.mutators.<name>` reference —
2696
+ * while the runtime object still carries the authoritative `server` impl +
2697
+ * `handler`, so codegen never evaluates the body. The client `client` impl is
2698
+ * split into the browser bundle separately.
2699
+ */
2700
+ declare const discoverMutators: (project: Project, lunoraDirectory: string) => MutatorIR[];
2701
+ /**
2702
+ * Discover non-deterministic API calls (`Date.now`, `new Date()`, `Date()`,
2703
+ * `Math.random`, `crypto.randomUUID`, `crypto.getRandomValues` — including
2704
+ * `globalThis`/`self`/`window`-prefixed receivers — and `fetch`) lexically inside
2705
+ * the handler body of every exported `query(...)` / `mutation(...)` registration
2706
+ * under the lunora source directory — the `nondeterministic_query_mutation` lint
2707
+ * input. `action(...)` (and `stream(...)`) registrations are intentionally
2708
+ * skipped: actions run exactly once and may use ambient APIs freely.
2709
+ *
2710
+ * Traversal is scoped to the handler node (not the whole declaration), mirroring
2711
+ * how the auth-api / insert feeders attribute calls — so a call in a sibling
2712
+ * helper outside the handler, or in a nested `action(...)` passed elsewhere, is
2713
+ * not attributed to the query/mutation. One {@link NondeterministicCallIR} is
2714
+ * produced per call site.
2715
+ */
2716
+ declare const discoverNondeterministicCalls: (project: Project, lunoraDirectory: string) => NondeterministicCallIR[];
2717
+ /** The only file a `@lunora/notify` provider may be declared in — mirrors `lunora/flags.ts`. */
2718
+ declare const NOTIFY_FILENAME = "notify.ts";
2719
+ /**
2720
+ * Discover `ctx.notify` / `ctx.push` sends lexically inside the handler body of
2721
+ * every exported `query(...)` / `mutation(...)` registration under the lunora
2722
+ * source directory — the `notify_send_outside_action` lint input. `action(...)`
2723
+ * (and `stream(...)`) registrations are intentionally skipped: a notification
2724
+ * send is external I/O that belongs in actions. One {@link AdvisorNotifyCall} is
2725
+ * produced per send site.
2726
+ */
2727
+ declare const discoverNotifyCalls: (project: Project, lunoraDirectory: string) => AdvisorNotifyCall[];
2728
+ /**
2729
+ * Discover which push channels the project's `lunora/notify.ts` default export
2730
+ * (`defineNotify({...})`) wires plus whether any handler sends a push — the
2731
+ * `notify_missing_push_config` lint input. Returns `undefined` when the file is
2732
+ * absent (the app declares no notify config). The read is metadata-only and
2733
+ * lenient (like `discoverFlags`): a `webPush`/`fcm` property's mere presence
2734
+ * counts as the channel being wired; a non-literal config degrades to "unwired"
2735
+ * rather than throwing.
2736
+ */
2737
+ declare const discoverNotifyConfig: (project: Project, lunoraDirectory: string) => AdvisorNotifyConfig | undefined;
2738
+ /**
2739
+ * Every package name the project declares a dependency on, read from the
2740
+ * `package.json` at the project root across all four dependency fields
2741
+ * (`dependencies`, `devDependencies`, `peerDependencies`,
2742
+ * `optionalDependencies`).
2743
+ *
2744
+ * Returns `undefined` — NOT an empty set — when the manifest is absent or
2745
+ * unparseable, which is the whole reason this exists. Studio nav gating can
2746
+ * treat "declares nothing" and "cannot tell" the same way, but a check that
2747
+ * errors on a missing package cannot: conflating them makes it fire on every
2748
+ * project without a root `package.json` (the codegen fixtures, an embedded
2749
+ * schema, a tool driving `runCodegen` directly).
2750
+ */
2751
+ declare const readPackageDependencies: (projectRoot: string) => Set<string> | undefined;
2752
+ /**
2753
+ * Discover every `ctx.db.query("table")…` read under the lunora source directory
2754
+ * and reduce each to a {@link QueryReadIR}.
2755
+ *
2756
+ * Reads without a `.filter()` are kept too. They are never
2757
+ * `filter_without_index` candidates (that lint gates on `hasFilter`), but an
2758
+ * unfiltered, unindexed `.collect()` is the read `unbounded_collect` exists for
2759
+ * — and dropping it here is precisely why nothing could see it.
2760
+ */
2761
+ declare const discoverQueries: (project: Project, lunoraDirectory: string) => QueryReadIR[];
2762
+ /** The only file queues may be declared in — mirrors `lunora/workflows.ts`. */
2763
+ declare const QUEUES_FILENAME = "queues.ts";
2764
+ /**
2765
+ * Discover every queue the project declares: exported `defineQueue()` calls in
2766
+ * `lunora/queues.ts`. Returns `[]` when the file doesn't exist. Only the
2767
+ * wrangler-relevant literals (`name`/`mode`/batch tuning) are read; the handler
2768
+ * body is runtime-only, so codegen never evaluates it.
2769
+ */
2770
+ declare const discoverQueues: (project: Project, lunoraDirectory: string) => QueueIR[];
2771
+ /**
2772
+ * Discover `ctx.r2sql` accesses lexically inside the handler body of every
2773
+ * exported `query(...)` / `mutation(...)` registration under the lunora source
2774
+ * directory — the `r2sql_outside_action` lint input. `action(...)` (and
2775
+ * `stream(...)`) registrations are intentionally skipped: R2 SQL is the
2776
+ * external, non-reactive surface that belongs in actions.
2777
+ *
2778
+ * Traversal is scoped to the handler node (not the whole declaration), mirroring
2779
+ * `discoverNondeterministicCalls` — so a `ctx.r2sql` touch in a sibling helper
2780
+ * outside the handler is not attributed to the query/mutation. One
2781
+ * {@link R2sqlCallIR} is produced per access site.
2782
+ */
2783
+ declare const discoverR2sqlCalls: (project: Project, lunoraDirectory: string) => R2sqlCallIR[];
907
2784
  declare const discoverRlsProcedures: (project: Project, lunoraDirectory: string) => RlsProcedureIR[];
908
2785
  /**
909
- * Aggregate the schema-wide RLS metadata the studio's read-only inspector reads:
910
- * every statically-discovered `(table, on, procedure)` policy entry plus every
911
- * role declared via `rls(policies, { roles })`. Walks the same builder chains as
912
- * {@link discoverRlsProcedures} but extracts the richer `{ on }` operation +
913
- * role/permission shape rather than the lint's table-name set.
914
- *
915
- * Only the **builder** form (`c.use(rls(...)).query(...)`) can declare policies,
916
- * so bare-factory procedures contribute nothing. The `when` predicate is never
917
- * read — it's an opaque JS closure whose logic belongs in code, not the UI.
918
- * Roles are deduped by name (first declaration wins) so a role registered on
919
- * several procedures lists once.
920
- */
2786
+ * Aggregate the schema-wide RLS metadata the studio's read-only inspector reads:
2787
+ * every statically-discovered `(table, on, procedure)` policy entry plus every
2788
+ * role declared via `rls(policies, { roles })`. Walks the same builder chains as
2789
+ * {@link discoverRlsProcedures} but extracts the richer `{ on }` operation +
2790
+ * role/permission shape rather than the lint's table-name set.
2791
+ *
2792
+ * Only the **builder** form (`c.use(rls(...)).query(...)`) can declare policies,
2793
+ * so bare-factory procedures contribute nothing. The `when` predicate is never
2794
+ * read — it's an opaque JS closure whose logic belongs in code, not the UI.
2795
+ * Roles are deduped by name (first declaration wins) so a role registered on
2796
+ * several procedures lists once.
2797
+ */
921
2798
  declare const discoverRlsMetadata: (project: Project, lunoraDirectory: string) => RlsMetadataIR;
922
2799
  /**
923
- * Load `&lt;projectRoot>/lunora/schema.ts`, find `defineSchema({...})`, and
924
- * return a structural IR. Throws if the file or call cannot be found.
925
- */
926
- declare const discoverSchema: (project: Project, schemaPath: string, projectRoot?: string) => SchemaIR;
2800
+ * Which sandbox tools a project imports from `@lunora/agent` (main entry or the
2801
+ * `/sandbox` subpath), detected by NAMED value import. Drives two things:
2802
+ * registering the `sandbox:invoke` dispatcher (either tool) and provisioning the
2803
+ * `BROWSER` wrangler binding (`browserTool` the browser op runs on
2804
+ * `ctx.browser` inside the dispatcher).
2805
+ */
2806
+ interface SandboxUsage {
2807
+ /** `import { browserTool } from "@lunora/agent"` (or `/sandbox`) appears in `lunora/`. */
2808
+ usesSandboxBrowser: boolean;
2809
+ /** `import { containerTool } from "@lunora/agent"` (or `/sandbox`) appears in `lunora/`. */
2810
+ usesSandboxContainer: boolean;
2811
+ }
2812
+ declare const discoverSandboxUsage: (project: Project, lunoraDirectory: string) => SandboxUsage;
927
2813
  /**
928
- * Aggregate the schema-wide storage-rule metadata the studio's inspector reads:
929
- * every statically-discovered `(bucket, on, prefix, procedure)` entry across all
930
- * `.use(storageRules(...))` chains. Only the builder form can declare rules, so
931
- * bare-factory procedures contribute nothing.
932
- */
2814
+ * Load `<projectRoot>/lunora/schema.ts`, find `defineSchema({...})`, and
2815
+ * return a structural IR. Throws if the file or call cannot be found.
2816
+ */
2817
+ declare const discoverSchema: (project: Project, schemaPath: string, projectRoot?: string) => SchemaIR;
2818
+ /** The only file shapes may be declared in — mirrors `lunora/queues.ts`. */
2819
+ declare const SHAPES_FILENAME = "shapes.ts";
2820
+ /**
2821
+ * Discover every replication shape the project declares: exported
2822
+ * `defineShape()` calls in `lunora/shapes.ts`. Returns `[]` when the file
2823
+ * doesn't exist. Only the export binding is lifted — the runtime object carries
2824
+ * the authoritative `table`/`columns`/`compileWhere`, so codegen never
2825
+ * evaluates the predicate.
2826
+ */
2827
+ declare const discoverShapes: (project: Project, lunoraDirectory: string) => ShapeIR[];
2828
+ /**
2829
+ * Aggregate the schema-wide storage-rule metadata the studio's inspector reads:
2830
+ * every statically-discovered `(bucket, on, prefix, procedure)` entry across all
2831
+ * `.use(storageRules(...))` chains. Only the builder form can declare rules, so
2832
+ * bare-factory procedures contribute nothing.
2833
+ */
933
2834
  declare const discoverStorageRulesMetadata: (project: Project, lunoraDirectory: string) => StorageRulesMetadataIR;
934
2835
  /** The only file workflows may be declared in — mirrors `lunora/containers.ts`. */
935
2836
  declare const WORKFLOWS_FILENAME = "workflows.ts";
936
2837
  /**
937
- * Discover every workflow the project declares: exported `defineWorkflow()`
938
- * calls in `lunora/workflows.ts`. Returns `[]` when the file doesn't exist. The
939
- * only wrangler-relevant literal is the optional `name` override; the workflow
940
- * body is runtime-only, so codegen never evaluates it.
941
- */
2838
+ * Discover every workflow the project declares: exported `defineWorkflow()`
2839
+ * calls in `lunora/workflows.ts`. Returns `[]` when the file doesn't exist. The
2840
+ * only wrangler-relevant literal is the optional `name` override; the workflow
2841
+ * body is runtime-only, so codegen never evaluates it.
2842
+ */
942
2843
  declare const discoverWorkflows: (project: Project, lunoraDirectory: string) => WorkflowIR[];
943
2844
  declare const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
944
- /** Emit `_generated/dataModel.ts` — `Doc&lt;"name">` + `Id&lt;"name">` for every table. */
945
- declare const emitDataModel: (schema: SchemaIR, useUmbrella?: boolean) => string;
946
- /**
947
- * Emit `_generated/api.ts` — the typed `api.*` registry (public functions), the
948
- * `internal.*` registry, and (when the project declares workflows) the typed
949
- * `workflows.*` reference object. `api`/`internal` are the same `anyApi` proxy
950
- * at runtime (the `__lunoraRef` is identical); visibility is enforced
951
- * server-side at dispatch, not in the reference. Splitting the *types* keeps
952
- * internal functions off the client-facing `api` surface.
953
- */
954
- declare const emitApi: (functions: ReadonlyArray<FunctionIR>, workflows?: ReadonlyArray<WorkflowIR>, useUmbrella?: boolean) => string;
955
- /**
956
- * Emit `_generated/seed.ts` — a project-bound `createSeedClient` with this
957
- * schema's `InsertModel` and runtime schema pre-applied, so a test or script
958
- * calls `createSeedClient({ seed: 1 }).users(5)` with full column types and no
959
- * manual wiring. The runtime schema is the default export of `lunora/schema.ts`
960
- * (the same import the generated ShardDO uses).
961
- *
962
- * Returns `""` when `@lunora/seed` is not a declared dependency, so projects
963
- * that don't use it keep a clean `_generated/` and never import the package.
964
- */
2845
+ /** Emit `_generated/dataModel.ts` — `Doc<"name">` + `Id<"name">` for every table. */
2846
+ declare const emitDataModel: (schema: SchemaIR) => string;
2847
+ /**
2848
+ * Emit `_generated/api.ts` — the typed `api.*` registry (public functions), the
2849
+ * `internal.*` registry, and (when the project declares them) the typed
2850
+ * `workflows.*` / `agents.*` scheduler-target reference objects. `api`/`internal` are the same `anyApi` proxy
2851
+ * at runtime (the `__lunoraRef` is identical); visibility is enforced
2852
+ * server-side at dispatch, not in the reference. Splitting the *types* keeps
2853
+ * internal functions off the client-facing `api` surface.
2854
+ */
2855
+ interface EmitApiOptions {
2856
+ agents?: ReadonlyArray<AgentIR>;
2857
+ functions: ReadonlyArray<FunctionIR>;
2858
+ /** Typed REST routes; only `.stream()` (SSE) routes emit a `httpStreams.*` reference. */
2859
+ httpRoutes?: ReadonlyArray<HttpRouteIR>;
2860
+ /** Custom mutators (`lunora/mutators.ts`) emitted as `api.mutators.*` so a client `serverRef` is compile-checked. */
2861
+ mutators?: ReadonlyArray<MutatorIR>;
2862
+ useUmbrella?: boolean;
2863
+ workflows?: ReadonlyArray<WorkflowIR>;
2864
+ }
2865
+ declare const emitApi: (options: EmitApiOptions) => string;
2866
+ /**
2867
+ * Emit `_generated/collections.ts` — a typed TanStack DB binding per `defineShape`
2868
+ * in `lunora/shapes.ts` (the local-first partial-replication surface).
2869
+ *
2870
+ * Each shape emits **two** entry points.
2871
+ *
2872
+ * `<shape>CollectionOptions(options)` is the composable form: it returns the full
2873
+ * `LunoraCollectionOptions` — `config` for `createCollection`, plus `checkpoints`
2874
+ * (which `bindMutators` gates optimistic overlays on) and `scope`. This is what an app
2875
+ * with custom mutators needs, and what the old single-factory form made impossible: it
2876
+ * built the collection internally and dropped `checkpoints` on the floor, so there was
2877
+ * no way to wire mutators to the collection codegen produced.
2878
+ *
2879
+ * `<shape>Collection(options)` is the convenience form for a read-only collection: it
2880
+ * returns `{ checkpoints, collection, scope }` rather than a bare `Collection`, so the
2881
+ * sync controls stay reachable even from the short path.
2882
+ *
2883
+ * Both are typed: `args` comes from the shape's own validators (a parameterless
2884
+ * shape takes none), rows resolve to `Doc<"table">` when the shape names its table
2885
+ * with a literal, and `shardKey` / `getKey` / `load` / `onError` / `checkpoints` are
2886
+ * all threadable — a sharded table needs `shardKey` for its watermark to land in the
2887
+ * right bucket, and a server-minted `_id` that differs from the app's natural key
2888
+ * needs `getKey`.
2889
+ *
2890
+ * Returns `""` (so `writeIfPresent` skips the file) unless the project both
2891
+ * declares shapes AND installs `@lunora/db` — the add-on that ships
2892
+ * `lunoraCollectionOptions`. `@lunora/db` stays a scoped install even under the
2893
+ * `lunorash` umbrella (an opt-in add-on, like `@lunora/auth`), so its import is
2894
+ * always `@lunora/db/collections`; only the in-umbrella `@lunora/client` import
2895
+ * is remapped to `lunorash/client`.
2896
+ */
2897
+ declare const emitCollections: (shapes: ReadonlyArray<ShapeIR>, hasDatabase: boolean, useUmbrella?: boolean) => string;
965
2898
  interface EmitServerOptions {
2899
+ /** Agents declared via `defineAgent` exports — wires the typed `ctx.agents` producers onto Mutation/Action contexts. */
2900
+ agents?: ReadonlyArray<AgentIR>;
966
2901
  containers?: ReadonlyArray<ContainerIR>;
2902
+ /**
2903
+ * The single `defineEnv(...)` contract declared in `lunora/env.ts`. When
2904
+ * present, `ctx.env` is typed as the validated `InferEnv` shape (recovered
2905
+ * via `ReturnType` over the accessor's `typeof`). `undefined` leaves `ctx.env`
2906
+ * the base optional binding record — byte-identical to today.
2907
+ */
2908
+ env?: EnvIR;
2909
+ /**
2910
+ * A `lunora/` source reads `ctx.access` — wires the verified Cloudflare Access
2911
+ * facade (`@lunora/cloudflare-access/context`) onto every ctx. Distinct from
2912
+ * `emitApp`'s `hasAccess` (which gates the worker's `.access()` resolveIdentity
2913
+ * method); this one gates the per-request `ctx.access` read surface.
2914
+ */
2915
+ hasAccessFacade?: boolean;
967
2916
  hasAi?: boolean;
968
2917
  /** A `lunora/` source uses `@lunora/bindings/analytics` / `ctx.analytics` — wires the write helper onto every ctx. */
969
2918
  hasAnalytics?: boolean;
970
2919
  /** A `lunora/` source uses `@lunora/browser` / `ctx.browser` — wires `ctx.browser` onto ActionCtx only. */
971
2920
  hasBrowser?: boolean;
2921
+ /** The project declares `lunora/flags.ts` — wires `ctx.flags` (OpenFeature) onto every ctx. */
2922
+ hasFlags?: boolean;
972
2923
  /** A `lunora/` source uses `@lunora/hyperdrive` / `ctx.sql` — wires `ctx.sql` onto ActionCtx only. */
973
2924
  hasHyperdrive?: boolean;
974
2925
  /** A `lunora/` source uses `@lunora/bindings/images` / `ctx.images` — wires `ctx.images` onto ActionCtx only. */
975
2926
  hasImages?: boolean;
976
2927
  /** A `lunora/` source uses `@lunora/bindings/kv` / `ctx.kv` — wires `ctx.kv` onto every ctx. */
977
2928
  hasKv?: boolean;
2929
+ /** The project declares `lunora/notify.ts` — wires `ctx.notify` + its `ctx.push` alias (`@lunora/notify`) onto every ctx. */
2930
+ hasNotify?: boolean;
978
2931
  hasPayments?: boolean;
979
2932
  /** A `lunora/` source uses `@lunora/bindings/pipelines` / `ctx.pipelines` — wires `ctx.pipelines` onto ActionCtx only. */
980
2933
  hasPipelines?: boolean;
981
2934
  /** A `lunora/` source uses `@lunora/bindings/r2sql` / `ctx.r2sql` — wires `ctx.r2sql` onto ActionCtx only. */
982
2935
  hasR2sql?: boolean;
2936
+ /** A `lunora/` source uses `@lunora/x402/pay` / `ctx.x402` — wires the agent-wallet pay rail onto ActionCtx only. */
2937
+ hasX402?: boolean;
2938
+ /**
2939
+ * The single `defineIdentity(...)` claim contract declared in
2940
+ * `lunora/identity.ts` (Plan 080). When present, `ctx.auth.getIdentity()`,
2941
+ * the RLS policy `ctx.auth.identity`, and the shard-authorization hooks
2942
+ * narrow to the declared shape (recovered via `InferIdentity` over the
2943
+ * contract's `typeof`). `undefined` keeps the identity an untyped bag —
2944
+ * byte-identical to today.
2945
+ */
2946
+ identity?: IdentityIR;
983
2947
  /** Queues declared via `defineQueue` exports — wires the typed `ctx.queues` producers onto Mutation/Action contexts. */
984
2948
  queues?: ReadonlyArray<QueueIR>;
985
2949
  schema?: SchemaIR;
@@ -988,169 +2952,179 @@ interface EmitServerOptions {
988
2952
  useUmbrella?: boolean;
989
2953
  workflows?: ReadonlyArray<WorkflowIR>;
990
2954
  }
991
- declare const emitServer: ({
992
- containers,
993
- hasAi,
994
- hasAnalytics,
995
- hasBrowser,
996
- hasHyperdrive,
997
- hasImages,
998
- hasKv,
999
- hasPayments,
1000
- hasPipelines,
1001
- hasR2sql,
1002
- queues,
1003
- schema,
1004
- storageRuleBuckets,
1005
- useUmbrella,
1006
- workflows
1007
- }?: EmitServerOptions) => string;
1008
- declare const emitFunctions: (functions: ReadonlyArray<FunctionIR>, migrations?: ReadonlyArray<MigrationIR>, useUmbrella?: boolean) => string;
1009
- /**
1010
- * Storage-column map per table for the file browser: `{ table: [field, …] }` for
1011
- * every field declared `v.storage(...)` (unwrapping `v.optional(...)`). The
1012
- * generated shard hands this to the base `storageColumns` hook so the admin
1013
- * `storageReferences` read can join R2 objects back to the rows that own them
1014
- * (and flag orphans — objects no row references). Only scalar storage columns
1015
- * are emitted; an array-of-storage field can't be matched by an equality scan,
1016
- * so it is skipped here.
1017
- */
1018
- /**
1019
- * Emit `_generated/containers.ts` — one container-enabled Durable Object class
1020
- * per `defineContainer` export, each a thin subclass of `LunoraContainer`
1021
- * (`@lunora/container/do`) constructed with the user's definition object. The
1022
- * worker entry must re-export these classes: wrangler requires every
1023
- * `containers[].class_name` to be exported by the deployed worker. Returns ""
1024
- * when the project declares no containers (the file is not written then).
1025
- */
2955
+ declare const emitServer: ({ agents, containers, env, hasAccessFacade, hasAi, hasAnalytics, hasBrowser, hasFlags, hasHyperdrive, hasImages, hasKv, hasNotify, hasPayments, hasPipelines, hasR2sql, hasX402, identity, queues, schema, storageRuleBuckets, useUmbrella, workflows }?: EmitServerOptions) => string;
2956
+ interface EmitFunctionsOptions {
2957
+ agents?: ReadonlyArray<AgentIR>;
2958
+ functions: ReadonlyArray<FunctionIR>;
2959
+ migrations?: ReadonlyArray<MigrationIR>;
2960
+ mutators?: ReadonlyArray<MutatorIR>;
2961
+ shapes?: ReadonlyArray<ShapeIR>;
2962
+ /** Import of a sandbox tool (`browserTool`/`containerTool`) auto-registers the `sandbox:invoke` action. */
2963
+ usesSandbox?: boolean;
2964
+ useUmbrella?: boolean;
2965
+ }
2966
+ declare const emitFunctions: (options: EmitFunctionsOptions) => string;
2967
+ /**
2968
+ * Emit `_generated/containers.ts` — one container-enabled Durable Object class
2969
+ * per `defineContainer` export, each a thin subclass of `LunoraContainer`
2970
+ * (`@lunora/container/do`) constructed with the user's definition object. The
2971
+ * worker entry must re-export these classes: wrangler requires every
2972
+ * `containers[].class_name` to be exported by the deployed worker. Returns ""
2973
+ * when the project declares no containers (the file is not written then).
2974
+ */
1026
2975
  declare const emitContainers: (containers: ReadonlyArray<ContainerIR>, jurisdiction?: JurisdictionIR) => string;
1027
2976
  /**
1028
- * Emit `_generated/workflows.ts` — one `WorkflowEntrypoint` class per
1029
- * `defineWorkflow` export, each a thin subclass of `LunoraWorkflow`
1030
- * (`@lunora/workflow/do`) constructed with the user's definition object. The
1031
- * worker entry must re-export these classes: wrangler requires every
1032
- * `workflows[].class_name` to be exported by the deployed worker. Returns ""
1033
- * when the project declares no workflows (the file is not written then).
1034
- */
2977
+ * Emit `_generated/workflows.ts` — one `WorkflowEntrypoint` class per
2978
+ * `defineWorkflow` export, each a thin subclass of `LunoraWorkflow`
2979
+ * (`@lunora/workflow/do`) constructed with the user's definition object. The
2980
+ * worker entry must re-export these classes: wrangler requires every
2981
+ * `workflows[].class_name` to be exported by the deployed worker. Returns ""
2982
+ * when the project declares no workflows (the file is not written then).
2983
+ */
1035
2984
  declare const emitWorkflows: (workflows: ReadonlyArray<WorkflowIR>) => string;
1036
2985
  /**
1037
- * Emit `_generated/queues.ts` — the push-consumer registry the worker `queue()`
1038
- * handler dispatches through. Maps each push queue's stable wrangler name (which
1039
- * `batch.queue` carries) to its `defineQueue` definition + export name. Pull
1040
- * queues are consumed by an external worker, so they carry no handler and are
1041
- * omitted here. Returns "" (and the file is not written) when no push queues are
1042
- * declared a pull-only or queue-free app keeps a clean `_generated/`.
1043
- */
2986
+ * Emit `_generated/agents.ts` — one `WorkflowEntrypoint` class per `defineAgent`
2987
+ * export, each a thin subclass of `LunoraWorkflow` (`@lunora/workflow/do`)
2988
+ * constructed with the compiled agent tool-loop (`compileAgentWorkflow`). Like
2989
+ * `_generated/workflows.ts`, the worker entry must re-export these classes:
2990
+ * wrangler requires every `workflows[].class_name` to be exported by the
2991
+ * deployed worker. Returns "" when the project declares no agents (the file is
2992
+ * not written then).
2993
+ */
2994
+ declare const emitAgents: (agents: ReadonlyArray<AgentIR>) => string;
1044
2995
  interface EmitShardOptions {
1045
2996
  advisories?: ReadonlyArray<Finding>;
2997
+ /** Every declared procedure — the health map's denominator, served via `getAdvisorProcedures`. */
2998
+ advisorProcedures?: ReadonlyArray<AdvisorProcedureProtection>;
2999
+ /** Agents declared via `defineAgent` exports in `lunora/agents.ts` — wires the typed `ctx.agents` producers. */
3000
+ agents?: ReadonlyArray<AgentIR>;
1046
3001
  containers?: ReadonlyArray<ContainerIR>;
3002
+ /** The single `defineEnv(...)` contract declared in `lunora/env.ts` — applies the accessor to the worker `env` to populate `ctx.env`. */
3003
+ env?: EnvIR;
3004
+ /** Statically-discovered `ctx.flags.<type>("key")` reads — the studio Flags page + reactive evaluation iterate these. */
3005
+ flagKeys?: ReadonlyArray<{
3006
+ key: string;
3007
+ type: "boolean" | "number" | "object" | "string";
3008
+ }>;
3009
+ /** A `lunora/` source reads `ctx.access` — wires the verified Cloudflare Access facade onto every ctx. */
3010
+ hasAccessFacade?: boolean;
1047
3011
  hasAi?: boolean;
1048
3012
  /** A `lunora/` source reads `ctx.analytics` — wires the Analytics Engine write helper onto every ctx. */
1049
3013
  hasAnalytics?: boolean;
1050
3014
  /** A `lunora/` source reads `ctx.browser` — wires `ctx.browser` onto the ActionCtx only. */
1051
3015
  hasBrowser?: boolean;
3016
+ /** The project declares `lunora/flags.ts` — wires `ctx.flags` (OpenFeature) onto every ctx. */
3017
+ hasFlags?: boolean;
1052
3018
  /** A `lunora/` source reads `ctx.sql` (Hyperdrive) — wires `ctx.sql` onto the ActionCtx only. */
1053
3019
  hasHyperdrive?: boolean;
1054
3020
  /** A `lunora/` source reads `ctx.images` — wires `ctx.images` onto the ActionCtx only. */
1055
3021
  hasImages?: boolean;
1056
3022
  /** A `lunora/` source reads `ctx.kv` — wires `ctx.kv` onto every ctx. */
1057
3023
  hasKv?: boolean;
3024
+ /** The project declares `lunora/notify.ts` — wires `ctx.notify` + its `ctx.push` alias (`@lunora/notify`) onto every ctx. */
3025
+ hasNotify?: boolean;
1058
3026
  hasPayments?: boolean;
1059
3027
  /** A `lunora/` source reads `ctx.pipelines` — wires `ctx.pipelines` onto the ActionCtx only. */
1060
3028
  hasPipelines?: boolean;
1061
3029
  /** A `lunora/` source reads `ctx.r2sql` (R2 SQL) — wires `ctx.r2sql` onto the ActionCtx only. */
1062
3030
  hasR2sql?: boolean;
3031
+ /** A `lunora/` source reads `ctx.x402` — wires the agent-wallet pay rail onto the ActionCtx only. */
3032
+ hasX402?: boolean;
1063
3033
  maskMetadata?: MaskMetadataIR;
3034
+ /** Custom mutators declared via `defineMutator` in `lunora/mutators.ts` — wires the `isCustomMutator` push-protocol override. */
3035
+ mutators?: ReadonlyArray<MutatorIR>;
1064
3036
  /** Queues declared via `defineQueue` exports in `lunora/queues.ts` — wires the typed `ctx.queues` producers. */
1065
3037
  queues?: ReadonlyArray<QueueIR>;
1066
3038
  rlsMetadata?: RlsMetadataIR;
1067
3039
  schema: SchemaIR;
3040
+ /**
3041
+ * The structural snapshot the pre-deploy drift gate diffs against, threaded
3042
+ * in so the emitted DO records it in `__lunora_schema_history` on cold start
3043
+ * (plan 200 — the Studio's schema-version timeline). Optional so an emitter
3044
+ * caller that has no snapshot (tests, fixtures) emits the pre-ledger shape
3045
+ * unchanged.
3046
+ */
3047
+ schemaSnapshot?: SchemaSnapshot;
3048
+ /** Replication shapes declared via `defineShape` in `lunora/shapes.ts` — wires the `resolveShape` subscription override. */
3049
+ shapes?: ReadonlyArray<ShapeIR>;
1068
3050
  storageRules?: StorageRulesMetadataIR;
1069
3051
  studioFeatures?: StudioFeaturesResult;
1070
3052
  /** The project depends on the `lunora` umbrella — import base packages via its subpaths. */
1071
3053
  useUmbrella?: boolean;
1072
3054
  workflows?: ReadonlyArray<WorkflowIR>;
1073
3055
  }
1074
- declare const emitShard: ({
1075
- advisories,
1076
- containers,
1077
- hasAi,
1078
- hasAnalytics,
1079
- hasBrowser,
1080
- hasHyperdrive,
1081
- hasImages,
1082
- hasKv,
1083
- hasPayments,
1084
- hasPipelines,
1085
- hasR2sql,
1086
- maskMetadata,
1087
- queues,
1088
- rlsMetadata,
1089
- schema,
1090
- storageRules,
1091
- studioFeatures,
1092
- useUmbrella,
1093
- workflows
1094
- }: EmitShardOptions) => string;
1095
- /**
1096
- * Emit drizzle `sqliteTable` definitions for the project schema, split into
1097
- * `global` (D1-backed) and `shard` (DO-SQLite-backed) buckets. Tables marked
1098
- * `.global()` go in the global file; everything else (default root + `.shardBy()`)
1099
- * goes in the shard file.
1100
- *
1101
- * `searchIndexes` are intentionally not emitted — drizzle has no `sqliteTable`
1102
- * abstraction for FTS5 virtual tables. FTS plumbing is handled by the runtime
1103
- * outside of drizzle.
1104
- */
3056
+ declare const emitShard: ({ advisories, advisorProcedures, agents, containers, env, flagKeys, hasAccessFacade, hasAi, hasAnalytics, hasBrowser, hasFlags, hasHyperdrive, hasImages, hasKv, hasNotify, hasPayments, hasPipelines, hasR2sql, hasX402, maskMetadata, mutators, queues, rlsMetadata, schema, schemaSnapshot, shapes, storageRules, studioFeatures, useUmbrella, workflows }: EmitShardOptions) => string;
3057
+ /**
3058
+ * Emit drizzle `sqliteTable` definitions for the project schema, split into
3059
+ * `global` (D1-backed) and `shard` (DO-SQLite-backed) buckets. Tables marked
3060
+ * `.global()` go in the global file; everything else (default root + `.shardBy()`)
3061
+ * goes in the shard file.
3062
+ *
3063
+ * `searchIndexes` are intentionally not emitted — drizzle has no `sqliteTable`
3064
+ * abstraction for FTS5 virtual tables. FTS plumbing is handled by the runtime
3065
+ * outside of drizzle.
3066
+ */
1105
3067
  declare const emitDrizzleSchema: (schema: SchemaIR, useUmbrella?: boolean) => {
1106
3068
  global: string;
1107
3069
  shard: string;
1108
3070
  };
1109
3071
  /**
1110
- * Emit `_generated/crons.ts` from the discovered cron jobs.
1111
- *
1112
- * `LUNORA_CRON_TRIGGERS` is the deduplicated schedule array — what lands in
1113
- * wrangler's `triggers.crons` (the vite plugin reconciles it into
1114
- * `wrangler.jsonc`, and {@link emitWranglerCronTriggers} renders the same list
1115
- * for the CLI / docs).
1116
- *
1117
- * `LUNORA_CRONS` is the dispatcher map keyed by cron expression, each value a
1118
- * list of `{ name, functionPath, args }`. Cloudflare's `scheduled()` handler
1119
- * receives only the cron string, so multiple jobs sharing one expression must
1120
- * all fire — hence a list per key rather than a single entry.
1121
- *
1122
- * Jobs arrive pre-sorted by name (deterministic output); the trigger array
1123
- * preserves first-seen order of distinct expressions.
1124
- *
1125
- * Cloudflare caps a Worker at **3 Cron Triggers** (i.e. 3 distinct cron
1126
- * expressions). Because the dispatcher fires every job sharing an expression,
1127
- * many jobs can ride a single trigger — only the count of *distinct* schedules
1128
- * matters. `lunora codegen` warns when that count exceeds the limit; for
1129
- * finer-grained scheduling use Durable Object alarms (`@lunora/scheduler`),
1130
- * which have no such cap.
1131
- */
3072
+ * Emit `_generated/crons.ts` from the discovered cron jobs.
3073
+ *
3074
+ * `LUNORA_CRON_TRIGGERS` is the deduplicated schedule array — what lands in
3075
+ * wrangler's `triggers.crons` (the vite plugin reconciles it into
3076
+ * `wrangler.jsonc`, and {@link emitWranglerCronTriggers} renders the same list
3077
+ * for the CLI / docs).
3078
+ *
3079
+ * `LUNORA_CRONS` is the dispatcher map keyed by cron expression, each value a
3080
+ * list of `{ name, functionPath, args }`. Cloudflare's `scheduled()` handler
3081
+ * receives only the cron string, so multiple jobs sharing one expression must
3082
+ * all fire — hence a list per key rather than a single entry.
3083
+ *
3084
+ * Jobs arrive pre-sorted by name (deterministic output); the trigger array
3085
+ * preserves first-seen order of distinct expressions.
3086
+ *
3087
+ * Cloudflare caps a Worker at **3 Cron Triggers** (i.e. 3 distinct cron
3088
+ * expressions). Because the dispatcher fires every job sharing an expression,
3089
+ * many jobs can ride a single trigger — only the count of *distinct* schedules
3090
+ * matters. `lunora codegen` warns when that count exceeds the limit; for
3091
+ * finer-grained scheduling use Durable Object alarms (`@lunora/scheduler`),
3092
+ * which have no such cap.
3093
+ */
1132
3094
  declare const emitCrons: (crons: ReadonlyArray<CronJobIR>) => string;
1133
3095
  /**
1134
- * Render `_generated/vectors.ts` — the static registry of every vector index
1135
- * declared in `schema.ts` (inline `.vectorize()` columns + standalone
1136
- * `defineVectorIndex()` definitions).
1137
- *
1138
- * Cloudflare Vectorize exposes no way to enumerate an account's indexes at
1139
- * runtime — a binding can `describe()` itself but the worker can't ask "which
1140
- * indexes exist". So this generated array is the source of truth the studio's
1141
- * vector browser lists, and the worker's admin route pairs each entry with its
1142
- * live `describe()` stats. Entries are sorted by name for deterministic output.
1143
- */
3096
+ * Render `_generated/vectors.ts` — the static registry of every vector index
3097
+ * declared in `schema.ts` (inline `.vectorize()` columns + standalone
3098
+ * `defineVectorIndex()` definitions).
3099
+ *
3100
+ * Cloudflare Vectorize exposes no way to enumerate an account's indexes at
3101
+ * runtime — a binding can `describe()` itself but the worker can't ask "which
3102
+ * indexes exist". So this generated array is the source of truth the studio's
3103
+ * vector browser lists, and the worker's admin route pairs each entry with its
3104
+ * live `describe()` stats. Entries are sorted by name for deterministic output.
3105
+ */
1144
3106
  declare const emitVectors: (vectorIndexes: ReadonlyArray<VectorIndexIR>) => string;
1145
3107
  /**
1146
- * Render the deduplicated cron schedules as a JSON fragment suitable for
1147
- * splicing into `wrangler.jsonc`'s `triggers.crons`. The vite plugin uses this
1148
- * (plus the parsed wrangler config) to reconcile generated triggers without the
1149
- * user hand-editing the file.
1150
- */
3108
+ * Render the deduplicated cron schedules as a JSON fragment suitable for
3109
+ * splicing into `wrangler.jsonc`'s `triggers.crons`. The vite plugin uses this
3110
+ * (plus the parsed wrangler config) to reconcile generated triggers without the
3111
+ * user hand-editing the file.
3112
+ */
1151
3113
  declare const emitWranglerCronTriggers: (crons: ReadonlyArray<CronJobIR>) => string[];
1152
3114
  /** Which capability methods the generated `defineApp` builder exposes — one flag per package-backed feature the app actually uses. */
1153
3115
  interface EmitAppOptions {
3116
+ /**
3117
+ * Inbound-email agents (`defineAgent({ onEmail })`) → wire the worker's
3118
+ * top-level `email()` handler to `dispatchAgentEmail(...)` (from
3119
+ * `@lunora/agent/inbound`), so received mail starts a durable run. Empty/absent
3120
+ * ⇒ no wiring, byte-identical output for email-free (and agent-free) projects.
3121
+ */
3122
+ emailAgents?: ReadonlyArray<{
3123
+ bindingName: string;
3124
+ exportName: string;
3125
+ }>;
3126
+ /** App depends on `@lunora/cloudflare-access` → emit `.access()` (wire the Cloudflare Access `resolveIdentity`, composed ahead of `@lunora/auth` when both are present). */
3127
+ hasAccess: boolean;
1154
3128
  /** App uses `@lunora/ai` / `ctx.ai` → emit `.ai()` (override the Workers AI binding backing `ctx.ai`). */
1155
3129
  hasAi: boolean;
1156
3130
  /** App uses `@lunora/bindings/analytics` / `ctx.analytics` → emit `.analytics()` (override the dataset backing `ctx.analytics`). */
@@ -1171,6 +3145,8 @@ interface EmitAppOptions {
1171
3145
  hasImages: boolean;
1172
3146
  /** App uses `@lunora/bindings/kv` / `ctx.kv` → emit `.kv()`. */
1173
3147
  hasKv: boolean;
3148
+ /** App declares `lunora/notify.ts` (`@lunora/notify`) → wire `options.notifySubscriptionStore` so the studio Notifications page can read registered devices. */
3149
+ hasNotify: boolean;
1174
3150
  /** App uses `@lunora/payment` / `ctx.payments` → emit `.payment()`. */
1175
3151
  hasPayments: boolean;
1176
3152
  /** App declares push queues (`defineQueue`) → wire `LUNORA_QUEUE_REGISTRY` into the worker's `queue()` consumer entry. */
@@ -1185,30 +3161,45 @@ interface EmitAppOptions {
1185
3161
  hasVectors: boolean;
1186
3162
  /** App declares Cloudflare Workflows (`defineWorkflow`) → wire `options.workflowsClient` so the studio's workflow-instance proxy can reach the CF REST API. */
1187
3163
  hasWorkflow: boolean;
3164
+ /** App uses `@lunora/x402/pay` / `ctx.x402` → emit `.x402()` (wire the agent-wallet pay rail). */
3165
+ hasX402: boolean;
3166
+ /** The single `defineIdentity(...)` contract in `lunora/identity.ts` (Plan 080) → import it as a VALUE and wire `options.identity`, so the runtime trust boundary validates every resolved identity before it becomes `ctx.auth`. `undefined` ⇒ no wiring, byte-identical output. */
3167
+ identity?: IdentityIR;
1188
3168
  /** Schema declares `.jurisdiction("…")` → pin every DO the worker reaches (shards, fan-out, scheduler, containers) to the Cloudflare data-residency jurisdiction. */
1189
3169
  jurisdiction?: JurisdictionIR;
1190
3170
  /** Project depends on the unscoped `lunorash` umbrella → import the runtime via `lunorash/runtime` instead of `@lunora/runtime`. */
1191
3171
  useUmbrella: boolean;
3172
+ /**
3173
+ * Voice-enabled agents (`defineAgent({ voice: … })`) → wire
3174
+ * `options.voiceAgents`, mapping each agent's export name to its `VOICE_*`
3175
+ * Durable Object namespace binding so the runtime exposes
3176
+ * `/_lunora/voice/<exportName>`. Empty/absent ⇒ no wiring, byte-identical
3177
+ * output for voice-free (and agent-free) projects.
3178
+ */
3179
+ voiceAgents?: ReadonlyArray<{
3180
+ bindingName: string;
3181
+ exportName: string;
3182
+ }>;
1192
3183
  /** An OpenAPI spec is emitted (`openapi.ts`) → wire `openApiSpec` into the worker. */
1193
3184
  wantsOpenApi: boolean;
1194
3185
  /** An OpenRPC spec is emitted (`openrpc.ts`) → wire `openRpcSpec` into the worker. */
1195
3186
  wantsOpenRpc: boolean;
1196
3187
  }
1197
3188
  /**
1198
- * Emit `_generated/app.ts` — a fluent, feature-specialized worker-composition
1199
- * builder. Only the methods for capabilities THIS app uses are emitted, so the
1200
- * builder's type surface (IntelliSense) lists exactly what can be configured.
1201
- *
1202
- * Each capability declaration is fanned into BOTH runtime surfaces: the DO-side
1203
- * `createShardDO(...)` factory that backs `ctx.*`, and the worker-side
1204
- * `createWorker(...)` options that back the studio/admin endpoints — so storage
1205
- * / scheduler / global are declared once instead of twice. The builder is pure
1206
- * sugar over the public `createWorker` / `createShardDO`; both stay usable.
1207
- *
1208
- * Lives in generated code (not `@lunora/runtime`, which is dependency-free) so
1209
- * it can import the add-on packages the app installed (`@lunora/auth`,
1210
- * `@lunora/storage`, …) directly.
1211
- */
3189
+ * Emit `_generated/app.ts` — a fluent, feature-specialized worker-composition
3190
+ * builder. Only the methods for capabilities THIS app uses are emitted, so the
3191
+ * builder's type surface (IntelliSense) lists exactly what can be configured.
3192
+ *
3193
+ * Each capability declaration is fanned into BOTH runtime surfaces: the DO-side
3194
+ * `createShardDO(...)` factory that backs `ctx.*`, and the worker-side
3195
+ * `createWorker(...)` options that back the studio/admin endpoints — so storage
3196
+ * / scheduler / global are declared once instead of twice. The builder is pure
3197
+ * sugar over the public `createWorker` / `createShardDO`; both stay usable.
3198
+ *
3199
+ * Lives in generated code (not `@lunora/runtime`, which is dependency-free) so
3200
+ * it can import the add-on packages the app installed (`@lunora/auth`,
3201
+ * `@lunora/storage`, …) directly.
3202
+ */
1212
3203
  declare const emitApp: (options: EmitAppOptions) => string;
1213
3204
  /** Inputs the OpenAPI emitter needs from a codegen run. */
1214
3205
  interface OpenApiEmitInput {
@@ -1218,40 +3209,40 @@ interface OpenApiEmitInput {
1218
3209
  version?: string;
1219
3210
  }
1220
3211
  /**
1221
- * Emit an OpenAPI 3.1.0 document covering both Lunora function surfaces.
1222
- *
1223
- * `httpRouter()` typed REST routes become real `paths` keyed by their method +
1224
- * URL, with query/path parameters and JSON request bodies derived from their
1225
- * `v.*` validators, and a response schema from `.output()` when declared.
1226
- *
1227
- * RPC `query`/`mutation`/`action` functions become one operation each on
1228
- * `POST /_lunora/rpc` (disambiguated by a `#functionPath` path fragment), with a
1229
- * requestBody pinning `functionPath` + typed `args`. `internal`/`stream`
1230
- * functions are excluded (unreachable / not invocable on the external RPC path).
1231
- *
1232
- * Operations are grouped into `tags` by file namespace, and every operation
1233
- * references a reusable `LunoraError` error-response component enumerating the
1234
- * standard error codes. Borrows oRPC's per-procedure-operation + tag-grouping +
1235
- * internal-filtering structure; the JSON Schema dialect matches `@lunora/values`
1236
- * (Draft 2020-12). Returns the document as a plain object (the single source of
1237
- * truth `emitOpenApi` stringifies and `emitOpenApiModule` inlines, so the
1238
- * `.json` and `.ts` artifacts can never drift).
1239
- */
3212
+ * Emit an OpenAPI 3.1.0 document covering both Lunora function surfaces.
3213
+ *
3214
+ * `httpRouter()` typed REST routes become real `paths` keyed by their method +
3215
+ * URL, with query/path parameters and JSON request bodies derived from their
3216
+ * `v.*` validators, and a response schema from `.output()` when declared.
3217
+ *
3218
+ * RPC `query`/`mutation`/`action` functions become one operation each on
3219
+ * `POST /_lunora/rpc` (disambiguated by a `#functionPath` path fragment), with a
3220
+ * requestBody pinning `functionPath` + typed `args`. `internal`/`stream`
3221
+ * functions are excluded (unreachable / not invocable on the external RPC path).
3222
+ *
3223
+ * Operations are grouped into `tags` by file namespace, and every operation
3224
+ * references a reusable `LunoraError` error-response component enumerating the
3225
+ * standard error codes. Borrows oRPC's per-procedure-operation + tag-grouping +
3226
+ * internal-filtering structure; the JSON Schema dialect matches `@lunora/values`
3227
+ * (Draft 2020-12). Returns the document as a plain object (the single source of
3228
+ * truth `emitOpenApi` stringifies and `emitOpenApiModule` inlines, so the
3229
+ * `.json` and `.ts` artifacts can never drift).
3230
+ */
1240
3231
  declare const buildOpenApiDocument: (input: OpenApiEmitInput) => Record<string, unknown>;
1241
3232
  /**
1242
- * Emit the OpenAPI 3.1 document as a pretty-printed JSON string
1243
- * (`_generated/openapi.json`) — the portable artifact for external tooling.
1244
- */
3233
+ * Emit the OpenAPI 3.1 document as a pretty-printed JSON string
3234
+ * (`_generated/openapi.json`) — the portable artifact for external tooling.
3235
+ */
1245
3236
  declare const emitOpenApi: (input: OpenApiEmitInput) => string;
1246
3237
  /**
1247
- * Emit the OpenAPI document as an importable TS module
1248
- * (`_generated/openapi.ts`) the worker entry imports and passes to
1249
- * `createWorker({ openApiSpec })`. The document object literal is inlined
1250
- * verbatim (same `JSON.stringify` form the `.json` uses), so the `.ts` and
1251
- * `.json` are byte-identical content and regenerate together — closing the gap
1252
- * where a Worker cannot read the JSON file at runtime. `document_` is the object
1253
- * returned by {@link buildOpenApiDocument} (reused, never recomputed).
1254
- */
3238
+ * Emit the OpenAPI document as an importable TS module
3239
+ * (`_generated/openapi.ts`) the worker entry imports and passes to
3240
+ * `createWorker({ openApiSpec })`. The document object literal is inlined
3241
+ * verbatim (same `JSON.stringify` form the `.json` uses), so the `.ts` and
3242
+ * `.json` are byte-identical content and regenerate together — closing the gap
3243
+ * where a Worker cannot read the JSON file at runtime. `document_` is the object
3244
+ * returned by {@link buildOpenApiDocument} (reused, never recomputed).
3245
+ */
1255
3246
  declare const emitOpenApiModule: (document_: Record<string, unknown>) => string;
1256
3247
  /** The OpenRPC dialect version this emitter targets. */
1257
3248
  declare const OPENRPC_VERSION = "1.3.2";
@@ -1262,140 +3253,71 @@ interface OpenRpcEmitInput {
1262
3253
  version?: string;
1263
3254
  }
1264
3255
  /**
1265
- * Emit an OpenRPC 1.x document describing Lunora's JSON-RPC surface.
1266
- *
1267
- * Only the RPC `query`/`mutation`/`action` functions become `methods` — one per
1268
- * function, `name` = `file:fn`. `internal` (off the external RPC path) and
1269
- * `stream` (not invocable over the RPC envelope) are excluded, the same filter
1270
- * the OpenAPI emitter applies. Each method's single `args` param is typed from
1271
- * the function's `v.*` validators (`argsObjectSchema`); `result` is the
1272
- * `.output()` schema when declared, else a best-effort inferred schema. The
1273
- * standard `LunoraError` codes ride along under each method's `errors`.
1274
- *
1275
- * `httpRouter()` typed REST routes are deliberately omitted — OpenRPC is
1276
- * RPC-only and cannot represent REST paths; the OpenAPI document is the spec
1277
- * that covers the REST surface. Methods are sorted by name for stable output.
1278
- * Returns the document as a plain object (the single source of truth
1279
- * `emitOpenRpc` stringifies and `emitOpenRpcModule` inlines, so the `.json` and
1280
- * `.ts` artifacts can never drift).
1281
- */
3256
+ * Emit an OpenRPC 1.x document describing Lunora's JSON-RPC surface.
3257
+ *
3258
+ * Only the RPC `query`/`mutation`/`action` functions become `methods` — one per
3259
+ * function, `name` = `file:fn`. `internal` (off the external RPC path) and
3260
+ * `stream` (not invocable over the RPC envelope) are excluded, the same filter
3261
+ * the OpenAPI emitter applies. Each method's single `args` param is typed from
3262
+ * the function's `v.*` validators (`argsObjectSchema`); `result` is the
3263
+ * `.output()` schema when declared, else a best-effort inferred schema. The
3264
+ * standard `LunoraError` codes ride along under each method's `errors`.
3265
+ *
3266
+ * `httpRouter()` typed REST routes are deliberately omitted — OpenRPC is
3267
+ * RPC-only and cannot represent REST paths; the OpenAPI document is the spec
3268
+ * that covers the REST surface. Methods are sorted by name for stable output.
3269
+ * Returns the document as a plain object (the single source of truth
3270
+ * `emitOpenRpc` stringifies and `emitOpenRpcModule` inlines, so the `.json` and
3271
+ * `.ts` artifacts can never drift).
3272
+ */
1282
3273
  declare const buildOpenRpcDocument: (input: OpenRpcEmitInput) => Record<string, unknown>;
1283
3274
  /**
1284
- * Emit the OpenRPC 1.x document as a pretty-printed JSON string
1285
- * (`_generated/openrpc.json`) — the portable artifact for external tooling.
1286
- */
3275
+ * Emit the OpenRPC 1.x document as a pretty-printed JSON string
3276
+ * (`_generated/openrpc.json`) — the portable artifact for external tooling.
3277
+ */
1287
3278
  declare const emitOpenRpc: (input: OpenRpcEmitInput) => string;
1288
3279
  /**
1289
- * Emit the OpenRPC document as an importable TS module
1290
- * (`_generated/openrpc.ts`) the worker entry imports and passes to
1291
- * `createWorker({ openRpcSpec })`. The document object literal is inlined
1292
- * verbatim (same `JSON.stringify` form the `.json` uses), so the `.ts` and
1293
- * `.json` are byte-identical content and regenerate together. `document_` is
1294
- * the object returned by {@link buildOpenRpcDocument} (reused, never recomputed).
1295
- */
3280
+ * Emit the OpenRPC document as an importable TS module
3281
+ * (`_generated/openrpc.ts`) the worker entry imports and passes to
3282
+ * `createWorker({ openRpcSpec })`. The document object literal is inlined
3283
+ * verbatim (same `JSON.stringify` form the `.json` uses), so the `.ts` and
3284
+ * `.json` are byte-identical content and regenerate together. `document_` is
3285
+ * the object returned by {@link buildOpenRpcDocument} (reused, never recomputed).
3286
+ */
1296
3287
  declare const emitOpenRpcModule: (document_: Record<string, unknown>) => string;
1297
- /** Current snapshot format version. Bumped if the structural shape below changes. */
1298
- declare const SCHEMA_SNAPSHOT_VERSION: 1;
1299
- /** A single field's structural shape: its value kind and whether it is optional. */
1300
- interface FieldSnapshot {
1301
- /** The validator kind (`string`, `number`, `id`, `object`, …) after unwrapping `v.optional`. */
1302
- kind: string;
1303
- /** True when declared `v.optional(...)` — accepts `undefined` / absent on insert. */
1304
- optional: boolean;
1305
- }
1306
- /** A single secondary index's structural shape. */
1307
- interface IndexSnapshot {
1308
- fields: ReadonlyArray<string>;
1309
- unique: boolean;
1310
- }
1311
- /** A single relation's structural shape. */
1312
- interface RelationSnapshot {
1313
- field: string;
1314
- kind: "many" | "one";
1315
- table: string;
1316
- }
1317
- /** Structural snapshot of one table. */
1318
- interface TableSnapshot {
1319
- /** Field name → {@link FieldSnapshot}, in declared order. */
1320
- fields: Record<string, FieldSnapshot>;
1321
- /** Index name → {@link IndexSnapshot}. */
1322
- indexes: Record<string, IndexSnapshot>;
1323
- /** Relation accessor name → {@link RelationSnapshot}. */
1324
- relations: Record<string, RelationSnapshot>;
1325
- /**
1326
- * `"root"` (default single-DO), `"global"` (D1-replicated), or
1327
- * `"shardBy:&lt;field>"` (partitioned). Encoded as a string so the snapshot
1328
- * stays a plain JSON-stable value.
1329
- */
1330
- shardMode: string;
1331
- }
1332
- /** The committed baseline — a deterministic structural view of the whole schema. */
1333
- interface SchemaSnapshot {
1334
- /**
1335
- * Cloudflare DO data-residency jurisdiction declared via `.jurisdiction("…")`,
1336
- * or absent. Tracked because changing it strands all existing Durable Object
1337
- * data (a DO name maps to a different ID per jurisdiction). Optional, so old
1338
- * baselines written before this field parse cleanly (absent ⇒ undefined).
1339
- *
1340
- * Typed as a plain `string` (not the authoring union) on purpose: this is
1341
- * STORED data that a newer Lunora may have written with a jurisdiction this
1342
- * version doesn't yet know. Preserving the raw value keeps the breaking
1343
- * `changedJurisdiction` diff correct under a downgrade — coercing an unknown
1344
- * value to `undefined` would fail OPEN and hide the most destructive change.
1345
- */
1346
- jurisdiction?: string;
1347
- /** Sorted list of every declared `defineMigration` id at capture time. */
1348
- migrationIds: ReadonlyArray<string>;
1349
- /** Table name → {@link TableSnapshot}, keys sorted for stable serialization. */
1350
- tables: Record<string, TableSnapshot>;
1351
- version: typeof SCHEMA_SNAPSHOT_VERSION;
1352
- }
1353
- /** One classified structural change between the baseline and the current schema. */
1354
- interface DriftChange {
1355
- /** `"breaking"` changes need a data migration; `"safe"` changes are additive. */
1356
- severity: "breaking" | "safe";
1357
- /** Human-readable, actionable description (used in the gate message). */
1358
- summary: string;
1359
- /** A machine-readable change discriminator. */
1360
- type: "addedIndex" | "addedOptionalField" | "addedRelation" | "addedRequiredField" | "addedTable" | "changedJurisdiction" | "changedFieldKind" | "changedIndex" | "changedShardMode" | "fieldOptionalToRequired" | "fieldRequiredToOptional" | "removedField" | "removedIndex" | "removedRelation" | "removedTable";
1361
- }
1362
- /** The result of diffing two snapshots: every classified change. */
1363
- interface SchemaDrift {
1364
- /** Every classified change, in a stable order (added/changed per table, then removals). */
1365
- changes: ReadonlyArray<DriftChange>;
1366
- }
1367
3288
  /**
1368
- * Build a {@link SchemaSnapshot} from a parsed {@link SchemaIR} and the set of
1369
- * declared migration ids. Tables and migration ids are sorted so the emitted
1370
- * JSON is byte-stable across runs (no spurious diffs / churn).
1371
- */
3289
+ * Build a {@link SchemaSnapshot} from a parsed {@link SchemaIR} and the set of
3290
+ * declared migration ids. Tables and migration ids are sorted so the emitted
3291
+ * JSON is byte-stable across runs AND across machines (no spurious diffs /
3292
+ * churn) — see `sortKeys` in `shared/schema-snapshot.ts` for why that ordering
3293
+ * must not be locale-aware.
3294
+ *
3295
+ * Field / index / relation keys are deliberately NOT sorted: they are emitted in
3296
+ * declaration order from the schema source, which is already deterministic for a
3297
+ * given source file and keeps the snapshot readable next to the schema it mirrors.
3298
+ */
1372
3299
  declare const buildSchemaSnapshot: (schema: SchemaIR, migrationIds: ReadonlyArray<string>) => SchemaSnapshot;
1373
- /** Serialize a snapshot to the exact bytes written to `lunora/.lunora-schema.json` (trailing newline). */
1374
- declare const serializeSchemaSnapshot: (snapshot: SchemaSnapshot) => string;
1375
3300
  /**
1376
- * Thrown by {@link parseSchemaSnapshot} when the baseline file exists but is
1377
- * malformed (bad JSON / wrong version / invalid table shape). Lets the CLI gate
1378
- * treat a corrupt baseline as a hard error rather than silently degrading to a
1379
- * "first capture" that would mask drift and then overwrite the bad file.
1380
- */
1381
- declare class SchemaSnapshotParseError extends Error {
1382
- override readonly name = "SchemaSnapshotParseError";
1383
- }
1384
- /**
1385
- * Parse a committed snapshot file. Returns `undefined` ONLY when the content is
1386
- * absent/empty; throws {@link SchemaSnapshotParseError} when content is present
1387
- * but malformed (bad JSON, wrong version, or structurally-invalid tables) so the
1388
- * caller can distinguish "no baseline yet" (a legitimate first capture) from "a
1389
- * corrupt baseline" (which must not be silently treated as a first capture).
1390
- */
1391
- declare const parseSchemaSnapshot: (content: string | undefined) => SchemaSnapshot | undefined;
3301
+ * Thrown by {@link parseSchemaSnapshot} when the baseline file exists but is
3302
+ * malformed (bad JSON / wrong version / invalid table shape). Lets the CLI gate
3303
+ * treat a corrupt baseline as a hard error rather than silently degrading to a
3304
+ * "first capture" that would mask drift and then overwrite the bad file.
3305
+ */
3306
+ declare class SchemaSnapshotParseError extends LunoraError {
3307
+ constructor(message: string);
3308
+ }
1392
3309
  /**
1393
- * Diff the current snapshot against a committed baseline and classify every
1394
- * structural change. Pure no I/O. When `baseline` is `undefined` (no committed
1395
- * snapshot yet) there is no drift to report: every table is treated as a fresh
1396
- * additive `addedTable`, so a first deploy is never blocked.
1397
- */
1398
- declare const diffSchemaSnapshots: (baseline: SchemaSnapshot | undefined, current: SchemaSnapshot) => SchemaDrift;
3310
+ * Parse a committed snapshot file. Returns `undefined` ONLY when the content is
3311
+ * absent/empty; throws {@link SchemaSnapshotParseError} when content is present
3312
+ * but malformed (bad JSON, wrong version, or structurally-invalid tables) so the
3313
+ * caller can distinguish "no baseline yet" (a legitimate first capture) from "a
3314
+ * corrupt baseline" (which must not be silently treated as a first capture).
3315
+ *
3316
+ * The parsing itself lives in `shared/schema-snapshot.ts` (the Studio reads the
3317
+ * same JSON out of the DO ledger); this wrapper only applies the CLI's policy of
3318
+ * treating a malformed baseline as fatal.
3319
+ */
3320
+ declare const parseSchemaSnapshot: (content: string | undefined) => SchemaSnapshot | undefined;
1399
3321
  /** The decision the pre-deploy gate returns. */
1400
3322
  interface SchemaDriftDecision {
1401
3323
  /** True when the deploy must be blocked (breaking drift with no new migration, and no override). */
@@ -1405,226 +3327,339 @@ interface SchemaDriftDecision {
1405
3327
  /** Migration ids declared now but absent from the baseline — proof a migration was added. */
1406
3328
  newMigrationIds: ReadonlyArray<string>;
1407
3329
  /**
1408
- * A multi-line, actionable explanation. Always present; empty string when
1409
- * there is no drift at all. Mirrors the D1-placeholder guard's message style.
1410
- */
3330
+ * A multi-line, actionable explanation. Always present; empty string when
3331
+ * there is no drift at all. Mirrors the D1-placeholder guard's message style.
3332
+ */
1411
3333
  reason: string;
1412
3334
  }
1413
3335
  /**
1414
- * Decide whether breaking schema drift should block a deploy.
1415
- *
1416
- * Blocks only when the baseline exists (a first-ever capture is never blocking),
1417
- * there is at least one `breaking` change, no NEW migration id was added since
1418
- * the baseline, and the `allowDrift` override is not set. Safe-only drift (or
1419
- * breaking drift accompanied by a new migration id) passes.
1420
- */
3336
+ * Decide whether breaking schema drift should block a deploy.
3337
+ *
3338
+ * Blocks only when the baseline exists (a first-ever capture is never blocking),
3339
+ * there is at least one `breaking` change, no NEW migration id was added since
3340
+ * the baseline, and the `allowDrift` override is not set. Safe-only drift (or
3341
+ * breaking drift accompanied by a new migration id) passes.
3342
+ */
1421
3343
  declare const evaluateSchemaDrift: (options: {
1422
3344
  allowDrift?: boolean;
1423
3345
  baseline: SchemaSnapshot | undefined;
3346
+ /** The command printing the remediation, so it names only flags that command accepts. */
3347
+ command?: string;
1424
3348
  current: SchemaSnapshot;
1425
3349
  }) => SchemaDriftDecision;
1426
3350
  /**
1427
- * Committed, tracked baseline file holding the blessed structural schema
1428
- * snapshot the pre-deploy drift gate diffs against. Lives in `lunora/` (NOT the
1429
- * gitignored `_generated/`) so it is committed alongside `schema.ts`. Leading
1430
- * dot keeps it tucked away next to the schema it describes.
1431
- */
1432
- declare const SCHEMA_SNAPSHOT_FILENAME = ".lunora-schema.json";
3351
+ * Convert a {@link SchemaIR} into a synthetic runtime {@link Schema} carrying just
3352
+ * the `tables[name].shape` surface `@lunora/seed` introspects. System columns
3353
+ * (`_id`, `_creationTime`) are absent from the IR shape, exactly as the seed
3354
+ * engine expects (it assigns `_id` itself).
3355
+ */
3356
+ declare const schemaFromIr: (ir: SchemaIR) => Schema;
1433
3357
  /**
1434
- * Construct the ts-morph `Project` codegen discovers over. Prefers the user's
1435
- * `tsconfig.json` (when one is found walking up from `lunoraDirectory`) so
1436
- * cross-file type resolution and path aliases work; falls back to an isolated
1437
- * project otherwise. This is the exact construction {@link runCodegen} uses
1438
- * when no `project` is injected exported so a long-lived caller (the Vite
1439
- * dev-loop) can build one once and reuse it across runs via
1440
- * {@link refreshCodegenProject} instead of re-parsing the user's whole TS
1441
- * program on every save.
1442
- */
1443
- declare const createCodegenProject: (lunoraDirectory: string) => Project;
3358
+ * Convert a codegen {@link ValidatorIR} into a JSON Schema node. A thin wrapper
3359
+ * over the shared {@link jsonSchemaFromNode} core (from `@lunora/values`) with the
3360
+ * IR-backed {@link irReader}, so the kind→schema mapping is the *same* algorithm
3361
+ * `@lunora/values`' `toJsonSchema` runs codegen never instantiates the runtime
3362
+ * `v.*` objects, it only holds the reflected IR. Shared by the OpenAPI and
3363
+ * OpenRPC emitters so both surfaces speak one JSON Schema dialect.
3364
+ */
3365
+ declare const validatorIrToJsonSchema: (validator: ValidatorIR) => JsonSchema;
1444
3366
  /**
1445
- * Synchronise a reused {@link createCodegenProject} Project with the current
1446
- * on-disk state of `lunoraDirectory`, so the next {@link runCodegen} sees the
1447
- * same files a freshly-constructed Project would — without re-parsing the whole
1448
- * TS program. Adds any on-disk source file the Project doesn't yet have, and
1449
- * `refreshFromFileSystemSync()`es the ones it does (picking up edits); then
1450
- * removes Project source files under `lunoraDirectory` that no longer exist on
1451
- * disk (the classic stale-deleted-file cache bug).
1452
- *
1453
- * Files outside `lunoraDirectory` (e.g. those pulled in by the user's tsconfig)
1454
- * are left untouched — they back type resolution and rarely change in the
1455
- * dev-loop; a tsconfig change invalidates the whole cached Project upstream.
1456
- */
1457
- declare const refreshCodegenProject: (project: Project, lunoraDirectory: string) => void;
3367
+ * The machine-readable `LunoraError` codes Lunora emits on the RPC + REST
3368
+ * surfaces, enumerated from `@lunora/server`'s `CODE_STATUS` map plus the
3369
+ * runtime/DO dispatch codes (`FUNCTION_NOT_FOUND`, `PAYLOAD_TOO_LARGE`,
3370
+ * `METHOD_NOT_ALLOWED`, the `*_NOT_CONFIGURED` admin gates, …). The list documents
3371
+ * the contract; clients switch on `error.code`. Kept sorted for stable output.
3372
+ */
3373
+ declare const LUNORA_ERROR_CODES: ReadonlyArray<string>;
1458
3374
  /**
1459
- * Top-level codegen entry. Parses `&lt;projectRoot>/lunora/schema.ts` and every
1460
- * function file under `&lt;projectRoot>/lunora/`, then writes
1461
- * `_generated/{api,server,dataModel}.ts` next to them.
1462
- *
1463
- * When `LUNORA_CODEGEN_TIMING` is set (truthy), a single diagnostic summary
1464
- * line is written to stderr with the total wall time and the discovery-vs-emit
1465
- * split opt-in instrumentation that is otherwise zero-cost and side-effect-free
1466
- * on the returned {@link CodegenResult}.
1467
- */
1468
- declare const runCodegen: (options: CodegenOptions) => CodegenResult;
1469
- interface CodegenOptions {
1470
- /**
1471
- * Which machine-readable API spec(s) to emit into `_generated/`.
1472
- *
1473
- * `"openapi"` (the default) writes only `openapi.json` (OpenAPI 3.1; covers
1474
- * both the RPC functions and `httpRouter()` REST routes). `"openrpc"` writes
1475
- * only `openrpc.json` (OpenRPC 1.x; the RPC functions only OpenRPC cannot
1476
- * represent REST routes). `"both"` writes both files; `"none"` writes neither.
1477
- *
1478
- * Regardless of the choice, `CodegenResult.generated.openApi` and `.openRpc`
1479
- * always carry the rendered string (computation is cheap and pure); only the
1480
- * on-disk write is gated by this option.
1481
- */
1482
- apiSpec?: "both" | "none" | "openapi" | "openrpc";
3375
+ * Language-agnostic half of SDK generation: turn an OpenRPC document
3376
+ * (`_generated/openrpc.json`, see {@link file://../openrpc.ts}) into the parsed
3377
+ * shape every per-language target renders from.
3378
+ *
3379
+ * Nothing here knows about a target language. A target supplies its own member
3380
+ * naming and templates (see {@link file://./targets}); everything that would
3381
+ * otherwise be re-derived per language how a `functionPath` splits, which
3382
+ * runtime verb a kind maps to, whether a schema is real or the untyped
3383
+ * placeholder, how namespaces group and sort — lives here exactly once.
3384
+ *
3385
+ * That single-source rule is not stylistic. `paths.ts` documents the same
3386
+ * discipline for namespaces ("if these ever disagree, runtime dispatch silently
3387
+ * misses functions"), and the failure mode here is the same in a new costume: a
3388
+ * target that re-derives a model name renders an import pointing at a class
3389
+ * quicktype never emitted.
3390
+ *
3391
+ * Deriving names in one place is necessary but NOT sufficient, because only
3392
+ * half the decision is ours: quicktype chooses whether a predicted name becomes
3393
+ * a declared type, and different backends answer differently for the same
3394
+ * schema. {@link withDeclaredModels} reconciles the two halves before a target
3395
+ * renders anything.
3396
+ */
3397
+ /** One OpenRPC method as {@link file://../openrpc.ts} emits it. */
3398
+ interface OpenRpcMethod {
3399
+ name: string;
3400
+ params?: ReadonlyArray<{
3401
+ name: string;
3402
+ schema?: Record<string, unknown>;
3403
+ }>;
3404
+ result?: {
3405
+ name: string;
3406
+ schema?: Record<string, unknown>;
3407
+ };
3408
+ summary?: string;
3409
+ "x-lunora-function-kind"?: string;
3410
+ }
3411
+ /** The `_generated/openrpc.json` document. */
3412
+ interface OpenRpcDocument {
3413
+ info?: {
3414
+ title?: string;
3415
+ version?: string;
3416
+ };
3417
+ methods: ReadonlyArray<OpenRpcMethod>;
3418
+ }
3419
+ /** The runtime verbs a generated SDK can call. Mirrors the client transports. */
3420
+ type RuntimeVerb = "action" | "mutation" | "query";
3421
+ /** One RPC function, parsed and language-neutral. */
3422
+ interface SdkMethod {
1483
3423
  /**
1484
- * When true, run discovery + emit (so any schema/function parse error
1485
- * surfaces) but skip writing files to `_generated/`. The returned
1486
- * `outputDirectory` is still the path that *would* have been written.
1487
- */
1488
- dryRun?: boolean;
3424
+ * Where this function's ARGUMENT nulls mean "unset" and where they mean
3425
+ * "null" see {@link ModelNullPaths}.
3426
+ *
3427
+ * On the method rather than on a shared render input because it is a
3428
+ * per-method fact and the parser already holds the schema it comes from. Read
3429
+ * by the three targets whose rendered models cannot tell the two apart
3430
+ * (ruby, rust, swift); the other five have a marker of their own and ignore
3431
+ * it.
3432
+ */
3433
+ argsNullPaths: ModelNullPaths;
1489
3434
  /**
1490
- * Run the static schema advisor (unindexed FKs, …) during codegen.
1491
- * Defaults to `true`. When `false`, `CodegenResult.advisories` is empty.
1492
- * Computed regardless of `dryRun`; codegen never prints them — see
1493
- * {@link CodegenResult.advisories}.
1494
- */
1495
- lint?: boolean;
1496
- /** Override the lunora subdirectory name. Defaults to `"lunora"`. */
1497
- lunoraDirectory?: string;
3435
+ * Generated args model name, or `undefined` when NO model could be named for
3436
+ * this function's arguments.
3437
+ *
3438
+ * `undefined` does NOT mean "takes no arguments" — see {@link SdkMethod.takesArgs}.
3439
+ * A schema carrying a `v.bigint()` or `v.bytes()` gets no model deliberately
3440
+ * (`hasUnrepresentableWireType`), and a backend that cannot name a shape leaves
3441
+ * it undeclared. Both still take arguments, just untyped ones.
3442
+ */
3443
+ argsType: string | undefined;
3444
+ /** Raw exported function name (`"list"`), before any naming convention. */
3445
+ functionName: string;
3446
+ /** The wire identifier (`"messages:list"`), emitted verbatim into calls. */
3447
+ functionPath: string;
3448
+ /** Raw file namespace (`"messages"`), before any naming convention. */
3449
+ namespace: string;
3450
+ /** Generated result model name, or `undefined` while the result is untyped. */
3451
+ resultType: string | undefined;
3452
+ /** Human summary for the doc comment. */
3453
+ summary: string;
1498
3454
  /**
1499
- * Reuse a previously-constructed ts-morph {@link Project} instead of building
1500
- * a fresh one each run. The caller owns refreshing its source files from disk
1501
- * (see {@link refreshCodegenProject}) — codegen does not re-read changed files
1502
- * off an injected Project. Built via {@link createCodegenProject} when absent.
1503
- * Used by the Vite dev-loop to avoid re-parsing the whole TS program on every
1504
- * save; omit it (CLI one-shot path) to get the default fresh-Project behaviour.
1505
- */
1506
- project?: Project;
1507
- /** Project root containing the `lunora/` directory. */
1508
- projectRoot: string;
3455
+ * Whether this function declares arguments at all, independent of whether a
3456
+ * model could be named for them.
3457
+ *
3458
+ * A target emits three shapes from this: a TYPED parameter when `argsType` is
3459
+ * set, an UNTYPED wire-shaped parameter when it is not but this is true, and no
3460
+ * parameter at all when this is false. Collapsing the middle case into the last
3461
+ * is what made `v.bigint()` functions uncallable with arguments.
3462
+ */
3463
+ takesArgs: boolean;
3464
+ /** Which runtime verb this dispatches to. */
3465
+ verb: RuntimeVerb;
3466
+ }
3467
+ /** One namespace's functions, sorted. */
3468
+ interface SdkNamespace {
3469
+ methods: ReadonlyArray<SdkMethod>;
3470
+ /** Raw namespace (`"messages"`); a target applies its own casing. */
3471
+ name: string;
3472
+ }
3473
+ /**
3474
+ * True when a schema actually describes a shape.
3475
+ *
3476
+ * `openrpc.ts` emits a description-only placeholder for any function without a
3477
+ * declared `.output()` (the return type is TS-inferred and absent from the IR).
3478
+ * A placeholder must never become a generated model: quicktype would render an
3479
+ * empty type, and the surface would decode every response into it — silently
3480
+ * discarding the real payload rather than leaving it untyped.
3481
+ */
3482
+ declare const isTypedSchema: (schema: Record<string, unknown> | undefined) => boolean;
3483
+ /**
3484
+ * A path from a model's root to one property. `*` stands for every element of an
3485
+ * array or every value of a record, neither of which has named positions.
3486
+ */
3487
+ type SchemaPath = ReadonlyArray<string>;
3488
+ /**
3489
+ * Where a model's nulls mean different things — the one fact a generated model
3490
+ * flattens away, and the reason three ports could not send a `v.nullable()`
3491
+ * argument at all.
3492
+ *
3493
+ * An unset `v.optional()` and a `v.nullable()` set to null are the SAME value in
3494
+ * every generated model (a nil field), and opposite things on the wire: the
3495
+ * validator rejects an explicit null for the first and requires the key present
3496
+ * for the second. Neither Ruby, Rust nor Swift renders a marker telling them
3497
+ * apart, so the distinction is computed here, from the schema, where `required`
3498
+ * still exists — and handed to the targets that need it.
3499
+ *
3500
+ * Two lists rather than one because the ports need opposite operations. Ruby and
3501
+ * Rust project a whole value tree and prune nulls, so they prune at
3502
+ * {@link ModelNullPaths.optional} and nowhere else — which also stops them
3503
+ * dropping a legitimate null inside a record or an array, as a blanket prune
3504
+ * does. Swift's `JSONEncoder` has already dropped every struct-property nil
3505
+ * before the transport sees a tree, so it restores nulls at
3506
+ * {@link ModelNullPaths.nullable} instead: an absent key at a required path can
3507
+ * only have been a nil, so putting the null back is exact.
3508
+ *
3509
+ * A `$ref` is NOT resolved. `openrpc.ts` inlines everything it emits, so this
3510
+ * never comes up for a generated document — but `--spec` accepts a hand-written
3511
+ * one, and there a `$ref`'d sub-object contributes no paths at all: the ports
3512
+ * that prune would send its unset optionals as null, and Swift would not restore
3513
+ * its nullables. Inline the schema, or teach this to follow the pointer.
3514
+ *
3515
+ * Both lists name PROPERTIES only. A record's values and an array's elements can
3516
+ * be null too, but no port drops one: the pruning ports prune at `optional`
3517
+ * paths, which a `*` position can never be, and `JSONEncoder` drops a nil only
3518
+ * from a struct property — a nil inside a dictionary or array encodes as null.
3519
+ * Listing a `*` leaf would also make Swift's restore INVENT record keys that
3520
+ * were never there, which is why the walk records the path it descends through
3521
+ * but never the `*` position itself.
3522
+ */
3523
+ interface ModelNullPaths {
3524
+ /** Required properties that permit null — a null there is a VALUE and must survive. */
3525
+ nullable: ReadonlyArray<SchemaPath>;
3526
+ /** Properties absent from their object's `required` — a null there means UNSET. */
3527
+ optional: ReadonlyArray<SchemaPath>;
3528
+ }
3529
+ /** What a target renders from. */
3530
+ interface SdkRenderInput {
1509
3531
  /**
1510
- * Re-bless the committed schema-drift baseline (`lunora/.lunora-schema.json`)
1511
- * with the current structural snapshot. The baseline is ALWAYS written on
1512
- * first capture (when the file is absent); set this to overwrite an existing
1513
- * one — e.g. after the developer has added the data migration that justifies
1514
- * a breaking change. Ignored when `dryRun` is true.
1515
- */
1516
- updateSchemaBaseline?: boolean;
3532
+ * The rendered model source, to be written as the target's model file.
3533
+ *
3534
+ * Empty for a target that emits its own model FILES via
3535
+ * {@link SdkTarget.renderModels} those are already written, and this string
3536
+ * exists only so the declared-model reconciliation reads one shape.
3537
+ */
3538
+ models: string;
3539
+ /** Namespaces and their functions, already sorted. */
3540
+ namespaces: ReadonlyArray<SdkNamespace>;
1517
3541
  }
1518
- interface CodegenResult {
3542
+ /**
3543
+ * One directory or file of the hand-written transport, and where it lands in the
3544
+ * output.
3545
+ *
3546
+ * `from` is relative to `sdks/<target id>/` and `to` is relative to `--out`. The
3547
+ * two differ because a repo layout and a consumable layout are not the same
3548
+ * shape: the Ruby transport lives under `lib/` so `ruby -Ilib` works in the
3549
+ * repo, while a vendored copy has no `lib` to point at, and the Rust transport
3550
+ * is the repo's root crate but a nested one in the output.
3551
+ *
3552
+ * Only the runtime is listed. A transport's own conformance suite and its
3553
+ * `generated_check/` sample are deliberately absent — they assert against
3554
+ * `protocol/fixtures/`, which is not copied, so vendoring them would ship a user
3555
+ * a test suite that cannot run.
3556
+ */
3557
+ interface SdkVendorEntry {
3558
+ /** Path under `sdks/<id>/`. A directory is copied recursively. */
3559
+ from: string;
3560
+ /** Destination path under `--out`. */
3561
+ to: string;
3562
+ }
3563
+ /** A language target. One per `--lang` value. */
3564
+ interface SdkTarget {
3565
+ /** The `--lang` value (`"python"`, `"go"`, …). */
3566
+ id: string;
1519
3567
  /**
1520
- * Static schema advisor findings (e.g. unindexed foreign keys) produced
1521
- * this run. Empty when `lint` is `false` or the schema is clean. Codegen
1522
- * does not print these itself — each caller presents them through its own
1523
- * channel (the CLI logger, the vite overlay, the studio Advisors table).
1524
- * `formatAdvisories` is exported for a plain multi-line rendering.
1525
- */
1526
- advisories: ReadonlyArray<Finding>;
3568
+ * The quicktype backend + renderer options that produce this target's
3569
+ * models, or absent when the target emits its own (see
3570
+ * {@link SdkTarget.renderModels}) or none at all.
3571
+ *
3572
+ * `LanguageName` is quicktype's own union, so a target naming a backend
3573
+ * quicktype does not ship fails to compile rather than at run time.
3574
+ */
3575
+ quicktype?: {
3576
+ lang: LanguageName;
3577
+ rendererOptions?: Record<string, string>;
3578
+ };
1527
3579
  /**
1528
- * Containers discovered from `defineContainer` exports in
1529
- * `lunora/containers.ts` the list the config layer reconciles into
1530
- * wrangler's `containers[]`, `CONTAINER_*` Durable Object bindings, and
1531
- * migration classes. Empty when the project declares no containers.
1532
- */
1533
- containers: ReadonlyArray<ContainerIR>;
3580
+ * Render the SDK. Returns file contents keyed by path relative to the
3581
+ * output directory (nested paths are created as needed).
3582
+ *
3583
+ * This includes the BUILD MANIFEST the layout needs `go.mod`, `Cargo.toml`,
3584
+ * `Package.swift`, a crate root — because those name the vendored transport
3585
+ * and are therefore part of "how this language resolves the copy", not
3586
+ * something a consumer should have to write. Languages that resolve by
3587
+ * directory (Python, Ruby, Java, Kotlin) emit no manifest.
3588
+ */
3589
+ render: (input: SdkRenderInput) => Record<string, string>;
1534
3590
  /**
1535
- * Deduplicated cron schedules discovered from `cronJobs()` definitions
1536
- * the array the vite plugin reconciles into `wrangler.jsonc`'s
1537
- * `triggers.crons`. Empty when the project declares no crons.
1538
- */
1539
- cronTriggers: ReadonlyArray<string>;
1540
- generated: {
1541
- api: string; /** Fluent worker-composition builder (`_generated/app.ts`)`defineApp()`. Always written. */
1542
- app: string;
1543
- /** Container DO classes (`_generated/containers.ts`); `""` (and not written) when no containers are declared. */
1544
- containers: string;
1545
- crons: string;
1546
- dataModel: string;
1547
- drizzleGlobal: string;
1548
- drizzleShard: string;
1549
- functions: string; /** OpenAPI 3.1.0 document (`_generated/openapi.json`), pretty-printed JSON. */
1550
- openApi: string;
1551
- /**
1552
- * OpenAPI document as an importable TS module (`_generated/openapi.ts`) —
1553
- * `export const openApiSpec`, the worker imports it for
1554
- * `createWorker({ openApiSpec })`. Same document as `openApi`. Written
1555
- * alongside `openapi.json` whenever `apiSpec` includes `openapi`.
1556
- */
1557
- openApiModule: string;
1558
- /** OpenRPC 1.x document (`_generated/openrpc.json`), pretty-printed JSON. Always computed; written only when `apiSpec` includes `openrpc`. */
1559
- openRpc: string;
1560
- /**
1561
- * OpenRPC document as an importable TS module (`_generated/openrpc.ts`) —
1562
- * `export const openRpcSpec`, for `createWorker({ openRpcSpec })`. Same
1563
- * document as `openRpc`. Written alongside `openrpc.json` whenever
1564
- * `apiSpec` includes `openrpc`.
1565
- */
1566
- openRpcModule: string;
1567
- /** Push-consumer queue registry (`_generated/queues.ts`); `""` (and not written) when no push queues are declared. */
1568
- queues: string;
1569
- /** Project-bound seed client (`_generated/seed.ts`); `""` (and not written) when `@lunora/seed` is not a declared dependency. */
1570
- seed: string;
1571
- server: string;
1572
- shard: string; /** Static vector-index registry (`_generated/vectors.ts`) — `LUNORA_VECTOR_INDEXES`. Empty array body when the schema declares none. */
1573
- vectors: string;
1574
- /** WorkflowEntrypoint classes (`_generated/workflows.ts`); `""` (and not written) when no workflows are declared. */
1575
- workflows: string;
1576
- };
1577
- outputDirectory: string;
3591
+ * Emit this target's models from the schema directly, INSTEAD of quicktype,
3592
+ * as file contents keyed by path relative to the output directory.
3593
+ *
3594
+ * Present only for the two JVM targets, and the exception is earned rather
3595
+ * than a preference: quicktype's Java and Kotlin backends rename properties
3596
+ * and, under `just-types`, emit no mapping metadata, so a model they render
3597
+ * cannot be projected back onto the wire and the only complete mapping
3598
+ * they offer requires a Jackson / Klaxon / kotlinx dependency, which is the
3599
+ * one thing these JDK-only transports are defined not to have.
3600
+ * `targets/java.ts` records every option that was measured.
3601
+ *
3602
+ * A MAP rather than the single string quicktype returns, because Java takes
3603
+ * one file per class: its single-file render is not compilable Java at all.
3604
+ * The values are still joined for {@link SdkRenderInput.models}, so the
3605
+ * declared-model reconciliation is the same code for every target.
3606
+ */
3607
+ renderModels?: (document: OpenRpcDocument) => Record<string, string>;
1578
3608
  /**
1579
- * Queues discovered from `defineQueue` exports in `lunora/queues.ts` the
1580
- * list the config layer reconciles into wrangler's `queues.producers[]` /
1581
- * `queues.consumers[]`. Queues are NOT Durable Objects, so this adds no
1582
- * binding or migration. Empty when the project declares no queues.
1583
- */
1584
- queues: ReadonlyArray<QueueIR>;
3609
+ * THIRD-PARTY packages a consuming project must still install, reported by
3610
+ * the CLI. Empty for six of the eight the transport is vendored and those
3611
+ * six reach the wire with only their standard library.
3612
+ *
3613
+ * A list, and not derivable from the transport, because a target's MODELS can
3614
+ * carry a dependency the transport does not: quicktype's Ruby backend emits
3615
+ * `Dry::Struct` types with no renderer option to avoid them, so a Ruby SDK
3616
+ * needs the gems even though `sdks/ruby` itself is dependency-free.
3617
+ */
3618
+ requires: ReadonlyArray<string>;
1585
3619
  /**
1586
- * The CURRENT structural schema snapshot computed this run (tables + field
1587
- * kinds/optionality + indexes/relations/shard mode + declared migration ids).
1588
- * The pre-deploy drift gate diffs this against the committed baseline read
1589
- * from {@link CodegenResult.schemaSnapshotPath}. Always present, even on a
1590
- * `dryRun`.
1591
- */
1592
- schemaSnapshot: SchemaSnapshot;
1593
- /** Absolute path of the committed baseline file (`lunora/.lunora-schema.json`). */
1594
- schemaSnapshotPath: string;
3620
+ * Which parts of `sdks/<id>/` are the transport, and where they land under
3621
+ * `--out`. See {@link SdkVendorEntry}.
3622
+ */
3623
+ vendor: ReadonlyArray<SdkVendorEntry>;
3624
+ }
3625
+ /** Every language `lunora sdk generate --lang` accepts, keyed by id. */
3626
+ declare const SDK_TARGETS: Readonly<Record<string, SdkTarget>>;
3627
+ /** The accepted `--lang` values, sorted for help text and error messages. */
3628
+ declare const SDK_LANGUAGES: ReadonlyArray<string>;
3629
+ /** The files a generation run writes, keyed by path relative to the output directory. */
3630
+ type SdkFiles = Record<string, string>;
3631
+ /** What a generation run produced, plus what it had to weaken and why. */
3632
+ interface SdkResult {
3633
+ files: SdkFiles;
1595
3634
  /**
1596
- * Workflows discovered from `defineWorkflow` exports in
1597
- * `lunora/workflows.ts` the list the config layer reconciles into
1598
- * wrangler's `workflows[]` array. Workflows are NOT Durable Objects, so this
1599
- * adds no binding or migration. Empty when the project declares no workflows.
1600
- */
1601
- workflows: ReadonlyArray<WorkflowIR>;
3635
+ * Model names predicted from the schema that the chosen backend did not
3636
+ * declare, so their call sites fell back to an untyped return. Surfaced
3637
+ * rather than swallowed silently weaker types are how an SDK looks
3638
+ * finished while returning `Any` everywhere.
3639
+ */
3640
+ undeclared: ReadonlyArray<string>;
3641
+ /**
3642
+ * Functions whose args or result carry a `v.bigint()` or `v.bytes()`. No
3643
+ * typed model can represent those — the wire needs a tagged value that no
3644
+ * generated field produces — so their parameters stay untyped and the
3645
+ * caller passes wire values directly.
3646
+ */
3647
+ unrepresentable: ReadonlyArray<string>;
1602
3648
  }
1603
3649
  /**
1604
- * Convert a {@link SchemaIR} into a synthetic runtime {@link Schema} carrying just
1605
- * the `tables[name].shape` surface `@lunora/seed` introspects. System columns
1606
- * (`_id`, `_creationTime`) are absent from the IR shape, exactly as the seed
1607
- * engine expects (it assigns `_id` itself).
1608
- */
1609
- declare const schemaFromIr: (ir: SchemaIR) => Schema;
1610
- /**
1611
- * Convert a codegen {@link ValidatorIR} into a JSON Schema node. A thin wrapper
1612
- * over the shared {@link jsonSchemaFromNode} core (from `@lunora/values`) with the
1613
- * IR-backed {@link irReader}, so the kind→schema mapping is the *same* algorithm
1614
- * `@lunora/values`' `toJsonSchema` runs codegen never instantiates the runtime
1615
- * `v.*` objects, it only holds the reflected IR. Shared by the OpenAPI and
1616
- * OpenRPC emitters so both surfaces speak one JSON Schema dialect.
1617
- */
1618
- declare const validatorIrToJsonSchema: (validator: ValidatorIR) => JsonSchema;
1619
- /** Build `{ type: "object", properties, required }` from an IR shape (mirrors `@lunora/values`' object mapping). */
1620
-
1621
- /**
1622
- * The machine-readable `LunoraError` codes Lunora emits on the RPC + REST
1623
- * surfaces, enumerated from `@lunora/server`'s `CODE_STATUS` map plus the
1624
- * runtime/DO dispatch codes (`FUNCTION_NOT_FOUND`, `PAYLOAD_TOO_LARGE`,
1625
- * `METHOD_NOT_ALLOWED`, the `*_NOT_CONFIGURED` admin gates, …). The list documents
1626
- * the contract; clients switch on `error.code`. Kept sorted for stable output.
1627
- */
1628
- declare const LUNORA_ERROR_CODES: ReadonlyArray<string>;
3650
+ * Generate the SDK for `document` in `target`'s language.
3651
+ *
3652
+ * Async only because the model layer is: quicktype's renderer is promise-based.
3653
+ * The surface itself is pure, so a target's `render` stays synchronous and
3654
+ * unit-testable without touching quicktype.
3655
+ *
3656
+ * Models are rendered BEFORE the surface because the surface's model references
3657
+ * depend on what the backend actually declared see {@link withDeclaredModels}.
3658
+ */
3659
+ declare const generateSdk: (document: OpenRpcDocument, target: SdkTarget) => Promise<SdkResult>;
3660
+ /** The matching secret rule's `kind` for a string value, or `undefined` when none matches. */
3661
+ declare const secretKindOf: (value: string) => string | undefined;
3662
+ /** A redacted preview of a secret value first 4 chars plus its length, never the full value. */
3663
+ declare const redact: (value: string) => string;
1629
3664
  declare const VERSION = "0.0.0";
1630
- export { type AuthApiCallIR, CONTAINERS_FILENAME, CodegenDiagnosticError, type CodegenOptions, type CodegenResult, type ContainerIR, type CronJobIR, type DriftChange, type EmitAppOptions, type FieldSnapshot, type FunctionIR, GENERATED_HEADER, type HttpRouteIR, type IndexIR, type IndexSnapshot, type InsertWriteIR, LUNORA_ERROR_CODES, type MaskProcedureIR, type MigrationIR, OPENRPC_VERSION, type OpenApiEmitInput, type OpenRpcEmitInput, type ProjectIR, QUEUES_FILENAME, type QueryReadIR, type QueueIR, type R2sqlCallIR, type RelationSnapshot, type RlsMetadataIR, type RlsPolicyIR, type RlsProcedureIR, type RlsRoleIR, SCHEMA_SNAPSHOT_FILENAME, SCHEMA_SNAPSHOT_VERSION, type SchemaDrift, type SchemaDriftDecision, type SchemaIR, type SchemaSnapshot, SchemaSnapshotParseError, type StorageRuleIR, type StorageRulesMetadataIR, type TableIR, type TableSnapshot, VERSION, type ValidatorIR, type VectorIndexIR, WORKFLOWS_FILENAME, type WorkflowIR, buildOpenApiDocument, buildOpenRpcDocument, buildSchemaSnapshot, createCodegenProject, diagnosticAt, diffSchemaSnapshots, discoverAuthApiCalls, discoverContainers, discoverCrons, discoverFunctions, discoverHttpRoutes, discoverInserts, discoverMaskProcedures, discoverMigrations, discoverNondeterministicCalls, discoverQueries, discoverQueues, discoverR2sqlCalls, discoverRlsMetadata, discoverRlsProcedures, discoverSchema, discoverStorageRulesMetadata, discoverWorkflows, emitApi, emitApp, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitOpenApi, emitOpenApiModule, emitOpenRpc, emitOpenRpcModule, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers, evaluateSchemaDrift, formatAdvisories, lintSchema, parseSchemaSnapshot, refreshCodegenProject, runCodegen, schemaFromIr, serializeSchemaSnapshot, validatorIrToJsonSchema };
3665
+ export { AGENTS_FILENAME, type AgentIR, type AuthApiCallIR, CONTAINERS_FILENAME, CodegenDiagnosticError, type CodegenOptions, type CodegenResult, type ContainerIR, type CronJobIR, DEFAULT_TARGET, type DriftChange, type DriftScope, type EmitAppOptions, FLAGS_FILENAME, type FieldSnapshot, type FlagsIR, type FunctionIR, GENERATED_HEADER, type HttpRouteIR, type IndexIR, type IndexSnapshot, type InsertWriteIR, LUNORA_ERROR_CODES, type LintSchemaOptions, MUTATORS_FILENAME, type MaskProcedureIR, type MigrationIR, type MutatorIR, NOTIFY_FILENAME, OPENRPC_VERSION, type OpenApiEmitInput, type OpenRpcDocument, type OpenRpcEmitInput, type OpenRpcMethod, type PlatformDiagnostic, type ProjectIR, QUEUES_FILENAME, type QueryReadIR, type QueueIR, type R2sqlCallIR, type RelationSnapshot, type RlsMetadataIR, type RlsPolicyIR, type RlsProcedureIR, type RlsRoleIR, type RuntimeVerb, SCHEMA_SNAPSHOT_FILENAME, SCHEMA_SNAPSHOT_VERSION, SDK_LANGUAGES, SDK_TARGETS, SHAPES_FILENAME, type SandboxUsage, type SchemaDrift, type SchemaDriftDecision, type SchemaIR, type SchemaSnapshot, SchemaSnapshotParseError, type SdkFiles, type SdkMethod, type SdkNamespace, type SdkRenderInput, type SdkResult, type SdkTarget, type ShapeIR, type StorageRuleIR, type StorageRulesMetadataIR, type TableIR, type TableSnapshot, VERSION, type ValidatorIR, type VectorIndexIR, WORKFLOWS_FILENAME, type WorkflowIR, type WranglerVariableIR, buildOpenApiDocument, buildOpenRpcDocument, buildSchemaSnapshot, createCodegenProject, describeErrorLevelFindings, diagnosticAt, diffSchemaSnapshots, discoverAgents, discoverAuthApiCalls, discoverContainers, discoverCrons, discoverFlags, discoverFunctions, discoverHttpRoutes, discoverInserts, discoverMaskProcedures, discoverMigrations, discoverMutators, discoverNondeterministicCalls, discoverNotifyCalls, discoverNotifyConfig, discoverQueries, discoverQueues, discoverR2sqlCalls, discoverRlsMetadata, discoverRlsProcedures, discoverSandboxUsage, discoverSchema, discoverShapes, discoverStorageRulesMetadata, discoverWorkflows, emitAgents, emitApi, emitApp, emitCollections, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitOpenApi, emitOpenApiModule, emitOpenRpc, emitOpenRpcModule, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers, errorAdvisoryNames, errorPlatformDiagnosticNames, evaluateSchemaDrift, findTsconfig, formatAdvisories, generateSdk, isTypedSchema, lintSchema, parseSchemaSnapshot, platformMatrixIds, readPackageDependencies, readProjectTarget, redact, refreshCodegenProject, resolveCodegenTarget, runCodegen, schemaFromIr, secretKindOf, serializeSchemaSnapshot, toAdvisorContext, validatorIrToJsonSchema };