@lunora/codegen 1.0.0-alpha.8 → 1.0.0-alpha.80

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