@lunora/advisor 1.0.0-alpha.1 → 1.0.0-alpha.2

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.
package/dist/index.d.mts CHANGED
@@ -285,6 +285,33 @@ interface AdvisorQueryRead {
285
285
  table: string;
286
286
  }
287
287
  /**
288
+ * One `ctx.r2sql` access discovered lexically inside a `query(...)` or
289
+ * `mutation(...)` handler body — the input the `r2sql_outside_action` lint
290
+ * consumes. Produced by the codegen feeder, which walks each exported function's
291
+ * handler with ts-morph and records reads of the R2 SQL `ctx.r2sql` surface
292
+ * (`ctx.r2sql.query(...)`, `ctx.r2sql.from(...)`, …).
293
+ *
294
+ * R2 SQL queries Apache Iceberg tables over an **external** REST endpoint Lunora
295
+ * does not own (there is no Workers binding): a `ctx.r2sql` call is a network
296
+ * round-trip with a mutable result (non-deterministic, like `fetch`) and its
297
+ * reads are invisible to Lunora live queries. It therefore belongs **only** in
298
+ * `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
299
+ * recorded — actions are the escape hatch. Runtime callers don't supply this, so
300
+ * the lint finds nothing there.
301
+ */
302
+ interface AdvisorR2sqlCall {
303
+ /** The accessed `ctx.r2sql` surface, e.g. `ctx.r2sql.query` / `ctx.r2sql.from`. */
304
+ callee: string;
305
+ /** The exported function performing the access (e.g. `topPerRegion`). */
306
+ exportName: string;
307
+ /** Source file the access appears in (relative to the lunora dir, no extension). */
308
+ file: string;
309
+ /** Which procedure kind the access lives in — only `query`/`mutation` are flagged; actions are exempt. */
310
+ kind: "mutation" | "query";
311
+ /** 1-based line of the access, or `0` when unknown. */
312
+ line: number;
313
+ }
314
+ /**
288
315
  * One procedure (query / mutation / action) discovered in the lunora source,
289
316
  * reduced to the facts the `rls_uncovered_table` lint needs: whether the
290
317
  * procedure's builder chain includes `.use(rls(...))`, and which tables the
@@ -657,6 +684,13 @@ interface LintContext {
657
684
  */
658
685
  queries?: ReadonlyArray<AdvisorQueryRead>;
659
686
  /**
687
+ * R2 SQL `ctx.r2sql` accesses discovered lexically inside `query`/`mutation`
688
+ * handler bodies — the `r2sql_outside_action` input. Supplied by the codegen
689
+ * feeder, which omits `action` handlers (where `ctx.r2sql` is the typed,
690
+ * intended surface); absent for runtime callers, where the lint finds nothing.
691
+ */
692
+ r2sqlCalls?: ReadonlyArray<AdvisorR2sqlCall>;
693
+ /**
660
694
  * Per-procedure RLS usage discovered in function bodies (the
661
695
  * `rls_uncovered_table` input). Carries whether each procedure's builder chain
662
696
  * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
@@ -1172,6 +1206,31 @@ declare const publicArgumentUsesAny: Lint;
1172
1206
  */
1173
1207
  declare const publicMutationWithoutRatelimit: Lint;
1174
1208
  /**
1209
+ * Flags an R2 SQL `ctx.r2sql` access inside a `query(...)` or `mutation(...)`
1210
+ * handler body.
1211
+ *
1212
+ * R2 SQL (`@lunora/r2sql`) queries Apache Iceberg tables over an **external**
1213
+ * REST endpoint Lunora does not own — there is no Workers binding, every query
1214
+ * is an HTTPS round-trip. A `ctx.r2sql` query is therefore non-deterministic
1215
+ * (exactly like `fetch`), which breaks the determinism the coordinator relies on
1216
+ * when it re-runs a query on subscription re-evaluation or a mutation on OCC
1217
+ * retry. And R2 SQL reads are invisible to the DO/SQLite change-feed, so a
1218
+ * subscription will never re-fire on them. `ctx.r2sql` is therefore wired onto
1219
+ * `ActionCtx` **only** and belongs exclusively in `action(...)` handlers; using
1220
+ * it in a query/mutation is the same class of bug as `fetch`/`Date.now`.
1221
+ *
1222
+ * This mirrors `hyperdrive_outside_action` — the action-only enforcement teeth
1223
+ * for external, non-reactive I/O. Runtime enforcement is still absent (see
1224
+ * `MEMORY.md` "Query/mutation determinism not enforced"), so the lint is the
1225
+ * guardrail.
1226
+ *
1227
+ * This lint runs when the codegen feeder has supplied access evidence
1228
+ * (`context.r2sqlCalls` present); a runtime caller with no evidence flags nothing
1229
+ * rather than raising false alarms. The feeder records accesses only inside
1230
+ * `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1231
+ */
1232
+ declare const r2sqlOutsideAction: Lint;
1233
+ /**
1175
1234
  * A correctness lint covering the columns a relation wires together: the FK
1176
1235
  * `field` and the `references` column must each exist on their respective
1177
1236
  * tables, or the join can never resolve. Caught here at codegen time rather
@@ -1389,4 +1448,4 @@ interface RunAdvisorOptions {
1389
1448
  * `static` lints at build time and defer `runtime` lints to a live shard.
1390
1449
  */
1391
1450
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
1392
- export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
1451
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
package/dist/index.d.ts CHANGED
@@ -285,6 +285,33 @@ interface AdvisorQueryRead {
285
285
  table: string;
286
286
  }
287
287
  /**
288
+ * One `ctx.r2sql` access discovered lexically inside a `query(...)` or
289
+ * `mutation(...)` handler body — the input the `r2sql_outside_action` lint
290
+ * consumes. Produced by the codegen feeder, which walks each exported function's
291
+ * handler with ts-morph and records reads of the R2 SQL `ctx.r2sql` surface
292
+ * (`ctx.r2sql.query(...)`, `ctx.r2sql.from(...)`, …).
293
+ *
294
+ * R2 SQL queries Apache Iceberg tables over an **external** REST endpoint Lunora
295
+ * does not own (there is no Workers binding): a `ctx.r2sql` call is a network
296
+ * round-trip with a mutable result (non-deterministic, like `fetch`) and its
297
+ * reads are invisible to Lunora live queries. It therefore belongs **only** in
298
+ * `action(...)` handlers. Calls inside `action(...)` are intentionally **not**
299
+ * recorded — actions are the escape hatch. Runtime callers don't supply this, so
300
+ * the lint finds nothing there.
301
+ */
302
+ interface AdvisorR2sqlCall {
303
+ /** The accessed `ctx.r2sql` surface, e.g. `ctx.r2sql.query` / `ctx.r2sql.from`. */
304
+ callee: string;
305
+ /** The exported function performing the access (e.g. `topPerRegion`). */
306
+ exportName: string;
307
+ /** Source file the access appears in (relative to the lunora dir, no extension). */
308
+ file: string;
309
+ /** Which procedure kind the access lives in — only `query`/`mutation` are flagged; actions are exempt. */
310
+ kind: "mutation" | "query";
311
+ /** 1-based line of the access, or `0` when unknown. */
312
+ line: number;
313
+ }
314
+ /**
288
315
  * One procedure (query / mutation / action) discovered in the lunora source,
289
316
  * reduced to the facts the `rls_uncovered_table` lint needs: whether the
290
317
  * procedure's builder chain includes `.use(rls(...))`, and which tables the
@@ -657,6 +684,13 @@ interface LintContext {
657
684
  */
658
685
  queries?: ReadonlyArray<AdvisorQueryRead>;
659
686
  /**
687
+ * R2 SQL `ctx.r2sql` accesses discovered lexically inside `query`/`mutation`
688
+ * handler bodies — the `r2sql_outside_action` input. Supplied by the codegen
689
+ * feeder, which omits `action` handlers (where `ctx.r2sql` is the typed,
690
+ * intended surface); absent for runtime callers, where the lint finds nothing.
691
+ */
692
+ r2sqlCalls?: ReadonlyArray<AdvisorR2sqlCall>;
693
+ /**
660
694
  * Per-procedure RLS usage discovered in function bodies (the
661
695
  * `rls_uncovered_table` input). Carries whether each procedure's builder chain
662
696
  * includes `.use(rls(...))`, which tables the procedure reads/writes, and which
@@ -1172,6 +1206,31 @@ declare const publicArgumentUsesAny: Lint;
1172
1206
  */
1173
1207
  declare const publicMutationWithoutRatelimit: Lint;
1174
1208
  /**
1209
+ * Flags an R2 SQL `ctx.r2sql` access inside a `query(...)` or `mutation(...)`
1210
+ * handler body.
1211
+ *
1212
+ * R2 SQL (`@lunora/r2sql`) queries Apache Iceberg tables over an **external**
1213
+ * REST endpoint Lunora does not own — there is no Workers binding, every query
1214
+ * is an HTTPS round-trip. A `ctx.r2sql` query is therefore non-deterministic
1215
+ * (exactly like `fetch`), which breaks the determinism the coordinator relies on
1216
+ * when it re-runs a query on subscription re-evaluation or a mutation on OCC
1217
+ * retry. And R2 SQL reads are invisible to the DO/SQLite change-feed, so a
1218
+ * subscription will never re-fire on them. `ctx.r2sql` is therefore wired onto
1219
+ * `ActionCtx` **only** and belongs exclusively in `action(...)` handlers; using
1220
+ * it in a query/mutation is the same class of bug as `fetch`/`Date.now`.
1221
+ *
1222
+ * This mirrors `hyperdrive_outside_action` — the action-only enforcement teeth
1223
+ * for external, non-reactive I/O. Runtime enforcement is still absent (see
1224
+ * `MEMORY.md` "Query/mutation determinism not enforced"), so the lint is the
1225
+ * guardrail.
1226
+ *
1227
+ * This lint runs when the codegen feeder has supplied access evidence
1228
+ * (`context.r2sqlCalls` present); a runtime caller with no evidence flags nothing
1229
+ * rather than raising false alarms. The feeder records accesses only inside
1230
+ * `query`/`mutation` handlers, so `action(...)` bodies never reach here.
1231
+ */
1232
+ declare const r2sqlOutsideAction: Lint;
1233
+ /**
1175
1234
  * A correctness lint covering the columns a relation wires together: the FK
1176
1235
  * `field` and the `references` column must each exist on their respective
1177
1236
  * tables, or the join can never resolve. Caught here at codegen time rather
@@ -1389,4 +1448,4 @@ interface RunAdvisorOptions {
1389
1448
  * `static` lints at build time and defer `runtime` lints to a live shard.
1390
1449
  */
1391
1450
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
1392
- export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
1451
+ export { AE_METRIC_EVENTS, ALL_LINTS, type AdvisorAdminRoute, type AdvisorArgumentValidator, type AdvisorAuthApiCall, type AdvisorContainer, type AdvisorHyperdriveCall, type AdvisorIndex, type AdvisorIndexHit, type AdvisorInsertWrite, type AdvisorMaskProcedure, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShardTraffic, type AdvisorSqlInterpolation, type AdvisorTable, type AdvisorTableSample, type AdvisorTableScan, type AdvisorWorkflow, type AdvisorWorkflowCall, type AnalyticsMetricsOptions, type AnalyticsMetricsSource, type AnalyticsRuntimeMetrics, type Category, type Facing, type Finding, type Level, type Lint, type LintContext, type LintSource, RUNTIME_LINTS, RunAdvisorOptions, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, fromServerSchema, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, loadAnalyticsRuntimeMetrics, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
package/dist/index.mjs CHANGED
@@ -17,6 +17,7 @@ import nondeterministicQueryMutation from './packem_shared/nondeterministicQuery
17
17
  import policyReferencesUnknownTable from './packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs';
18
18
  import publicArgumentUsesAny from './packem_shared/publicArgumentUsesAny-C71b2NCf.mjs';
19
19
  import publicMutationWithoutRatelimit from './packem_shared/publicMutationWithoutRatelimit-xBpJ6GWK.mjs';
20
+ import r2sqlOutsideAction from './packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs';
20
21
  import relationReferencesUnknownField from './packem_shared/relationReferencesUnknownField-YznyXt_7.mjs';
21
22
  import relationReferencesUnknownTable from './packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs';
22
23
  import rlsUncoveredTable from './packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs';
@@ -46,6 +47,7 @@ const STATIC_LINTS = [
46
47
  filterWithoutIndex,
47
48
  nondeterministicQueryMutation,
48
49
  hyperdriveOutsideAction,
50
+ r2sqlOutsideAction,
49
51
  authApiCallWithoutHeaders,
50
52
  policyReferencesUnknownTable,
51
53
  rlsUncoveredTable,
@@ -74,4 +76,4 @@ const runAdvisor = (context, options = {}) => {
74
76
  return findings;
75
77
  };
76
78
 
77
- export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
79
+ export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, maskUncoveredPiiColumn, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
@@ -0,0 +1,30 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const r2sqlOutsideAction = {
4
+ categories: ["SCHEMA"],
5
+ description: "A `query`/`mutation` handler accesses R2 SQL via `ctx.r2sql`. R2 SQL queries external Apache Iceberg tables over REST (no Workers binding): queries are non-deterministic (like `fetch`) and the reads are invisible to live queries. `ctx.r2sql` is available on `ActionCtx` only and must be confined to `action` handlers.",
6
+ facing: "EXTERNAL",
7
+ level: "WARN",
8
+ name: "r2sql_outside_action",
9
+ remediation: "Move the `ctx.r2sql` access into an `action(...)` (the only context where it is typed), where external I/O is allowed. If a query/mutation needs the data, have the action read it via `ctx.r2sql` and write a projection into a `defineSchema` DO/D1 table — that write is tracked by live queries, whereas R2 SQL is not.",
10
+ run: (context) => {
11
+ if (context.r2sqlCalls === void 0) {
12
+ return [];
13
+ }
14
+ const findings = [];
15
+ for (const call of context.r2sqlCalls) {
16
+ findings.push(
17
+ emit(r2sqlOutsideAction, {
18
+ cacheKey: `r2sql_outside_action:${call.file}:${call.line.toString()}:${call.callee}`,
19
+ detail: `\`${call.callee}(…)\` in ${call.exportName} (${call.file}:${call.line.toString()}) runs inside a ${call.kind} handler — R2 SQL's \`ctx.r2sql\` is non-deterministic and non-reactive, so it is available only in actions. Move the external query into an \`action\` and project the result into a Lunora table if a query/mutation needs it.`,
20
+ metadata: { callee: call.callee, exportName: call.exportName, file: call.file, kind: call.kind, line: call.line }
21
+ })
22
+ );
23
+ }
24
+ return findings;
25
+ },
26
+ source: "static",
27
+ title: "R2 SQL `ctx.r2sql` used outside an action"
28
+ };
29
+
30
+ export { r2sqlOutsideAction as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/advisor",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.2",
4
4
  "description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
5
5
  "keywords": [
6
6
  "advisor",