@lunora/codegen 1.0.0-alpha.15 → 1.0.0-alpha.151

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