@lunora/advisor 1.0.0-alpha.6 → 1.0.0-alpha.8

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
@@ -214,6 +214,28 @@ interface AdvisorMaskProcedure {
214
214
  visibility: "internal" | "public";
215
215
  }
216
216
  /**
217
+ * One whole-row `ctx.db.replace(id, document)` write discovered inside a custom
218
+ * mutator's authoritative `server` impl (`lunora/mutators.ts`) — the input the
219
+ * `mutator_full_row_replace` lint consumes.
220
+ *
221
+ * In the local-first sync engine a `replace` overwrites the entire row, so a
222
+ * concurrent edit to a *different* column (committed between this mutator's read
223
+ * and its write) is silently clobbered. `patch(id, { onlyTheField })` merges at
224
+ * the column level instead, letting independent field edits coexist — the
225
+ * blessed pattern for mutators on a synced (poke-live) table. Produced by the
226
+ * codegen feeder, which attributes each `replace` to the mutator export
227
+ * performing it; runtime callers don't supply it, so the lint finds nothing
228
+ * there.
229
+ */
230
+ interface AdvisorMutatorWrite {
231
+ /** The mutator export whose `server` impl performs the replace (e.g. `renameChannel`). */
232
+ exportName: string;
233
+ /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
234
+ file: string;
235
+ /** 1-based line of the `replace(...)` call, or `0` when unknown. */
236
+ line: number;
237
+ }
238
+ /**
217
239
  * One non-deterministic API call discovered lexically inside a `query(...)` or
218
240
  * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
219
241
  * lint consumes. Produced by the codegen feeder, which walks each exported
@@ -384,6 +406,16 @@ interface AdvisorTable {
384
406
  optionalFields?: ReadonlySet<string>;
385
407
  /** Declared relations (`.relations((r) => …)`). */
386
408
  relations: ReadonlyArray<AdvisorRelation>;
409
+ /**
410
+ * Storage tier the table is declared in: `"global"` (a `.global()` table,
411
+ * lives in D1 — the cross-shard tier), `"shardBy"` (partitioned across
412
+ * shard DOs by a key), or `"root"` (the default single-DO table). Read by
413
+ * the `shape_*` lints to flag replication shapes targeting a `.global()`
414
+ * table (poll-refreshed/latency-tiered, not poke-live). Optional — the
415
+ * codegen feeder always supplies it, the runtime feeder derives it; a feeder
416
+ * that omits it leaves tier-sensitive lints to treat the table as local.
417
+ */
418
+ shardKind?: "global" | "root" | "shardBy";
387
419
  }
388
420
  /**
389
421
  * One declared index, flattened across Lunora's index kinds so a single lint can
@@ -440,6 +472,28 @@ interface AdvisorSecretLiteral {
440
472
  preview: string;
441
473
  }
442
474
  /**
475
+ * A replication shape declared via `defineShape({ table, where, columns? })` in
476
+ * `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
477
+ * The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
478
+ * against the declared schema to flag a shape targeting an unknown table or a
479
+ * `.global()` table (which replicates through the latency-tiered D1 poll path,
480
+ * not the poke-live op-log). Supplied by the codegen feeder, which lifts only
481
+ * the export name + the static `table` literal; absent for runtime callers,
482
+ * where the shape lints find nothing.
483
+ */
484
+ interface AdvisorShape {
485
+ /** Export binding name — the shape's registry key (e.g. `channelMessages`). */
486
+ exportName: string;
487
+ /** File the shape is declared in (relative, for the operator to open). */
488
+ file: string;
489
+ /**
490
+ * The `table` string literal the shape replicates from, or `undefined` when
491
+ * the feeder could not read it as a plain string literal — tier-sensitive
492
+ * lints skip a shape with no resolvable table rather than guessing.
493
+ */
494
+ table?: string;
495
+ }
496
+ /**
443
497
  * One shard's observed traffic share — the input the `hot_shard` runtime lint
444
498
  * consumes. Produced by the studio backend, which fans out over a sharded
445
499
  * function's shards and reads each shard's recorded request volume from the
@@ -660,6 +714,14 @@ interface LintContext {
660
714
  */
661
715
  maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
662
716
  /**
717
+ * Whole-row `ctx.db.replace(id, document)` writes lifted from custom
718
+ * mutators' authoritative `server` impls (the `mutator_full_row_replace`
719
+ * input). Each `replace` overwrites the entire row, clobbering a concurrent
720
+ * edit to a different column on a synced table. Supplied by the codegen
721
+ * feeder; absent for runtime callers, where the lint finds nothing.
722
+ */
723
+ mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
724
+ /**
663
725
  * Non-deterministic API calls (`Date.now`, `Math.random`,
664
726
  * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
665
727
  * inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
@@ -708,6 +770,14 @@ interface LintContext {
708
770
  */
709
771
  secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
710
772
  /**
773
+ * Replication shapes declared via `defineShape` in `lunora/shapes.ts` — the
774
+ * `shape_unknown_table` and `shape_targets_global_table` lint input. Each
775
+ * carries the export name and its static `table` literal, cross-referenced
776
+ * against {@link LintContext.schema}. Supplied by the codegen feeder; absent
777
+ * for runtime callers, where the shape lints find nothing.
778
+ */
779
+ shapes?: ReadonlyArray<AdvisorShape>;
780
+ /**
711
781
  * Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
712
782
  * studio backend, which fans out over a sharded function's shards and reads
713
783
  * each shard's recorded request volume from the durable `__lunora_metrics`
@@ -1133,6 +1203,30 @@ declare const indexReferencesUnknownField: Lint;
1133
1203
  */
1134
1204
  declare const maskUncoveredPiiColumn: Lint;
1135
1205
  /**
1206
+ * Flags a custom mutator whose authoritative `server` impl writes a row with
1207
+ * `ctx.db.replace(id, document)` — a whole-document overwrite.
1208
+ *
1209
+ * The local-first sync engine serializes mutators in the shard DO, so two
1210
+ * mutators that touch the *same row* but *different columns* both run to
1211
+ * completion — but only if each writes its own column. A `replace` overwrites
1212
+ * the entire row from the document the mutator assembled, so a concurrent edit
1213
+ * to another column (committed between this mutator's read and its write, or
1214
+ * carried as a pending optimistic overlay on a client) is silently clobbered:
1215
+ * the kind of "two offline edits to different fields fight each other" data loss
1216
+ * a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
1217
+ * merges at the column level instead, so independent field edits coexist.
1218
+ *
1219
+ * `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
1220
+ * the whole row (a full-form save, a state-machine transition that rewrites
1221
+ * every field). The lint just surfaces the column-clobber risk so a developer
1222
+ * reaches for `patch` by default on a synced table.
1223
+ *
1224
+ * **Evidence supply**: runs only when the codegen feeder supplies
1225
+ * `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
1226
+ * `server` body); absent for runtime callers, where the lint finds nothing.
1227
+ */
1228
+ declare const mutatorFullRowReplace: Lint;
1229
+ /**
1136
1230
  * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
1137
1231
  * handler body.
1138
1232
  *
@@ -1285,6 +1379,48 @@ declare const relationReferencesUnknownTable: Lint;
1285
1379
  */
1286
1380
  declare const rlsUncoveredTable: Lint;
1287
1381
  /**
1382
+ * Flags a replication shape whose `table` is a `.global()` table.
1383
+ *
1384
+ * Poke-live replication is a per-shard-DO property: the shard owns its SQLite
1385
+ * and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
1386
+ * every subscriber at the next flush. A `.global()` table lives outside the
1387
+ * shard DO's SQLite op-log (in a global backend — D1, or Hyperdrive-fronted
1388
+ * Postgres/MySQL) — so a shape over a global table cannot be poke-live. It is
1389
+ * served through the cross-shard tier: **coordinator/poll-refreshed, latency-
1390
+ * tiered**, not live. That is a real and supported tier (it is the recommended
1391
+ * answer for cross-shard reads — denormalize, or move the joined table to
1392
+ * `.global()` and read through the global backend), but its freshness semantics
1393
+ * differ from a sharded shape's, so the boundary is surfaced rather than hidden.
1394
+ *
1395
+ * `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
1396
+ * accept the poll-refresh latency; the lint just makes the tier explicit so a
1397
+ * developer does not assume poke-live freshness.
1398
+ *
1399
+ * **Evidence supply**: runs only when the codegen feeder supplies
1400
+ * `context.shapes`; the table's tier comes from the schema's `shardKind`. A
1401
+ * shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
1402
+ * the feeder didn't supply is skipped.
1403
+ */
1404
+ declare const shapeTargetsGlobalTable: Lint;
1405
+ /**
1406
+ * Flags a replication shape whose `table` names a table that does not exist in
1407
+ * the schema.
1408
+ *
1409
+ * `defineShape({ table: "messages", … })` binds a shape to a table by a plain
1410
+ * string. A live `subscribeShape("…")` resolves that shape server-side and runs
1411
+ * its membership query against the named table — so a typo, a stale name after a
1412
+ * rename, or a copy-paste mistake produces a shape that can never resolve a
1413
+ * rowset: the subscription seeds empty and then errors at the first flush
1414
+ * (`no such table`). This is a definite, build-time-detectable break, so it is
1415
+ * an `ERROR` — surfaced before the broken shape ever ships.
1416
+ *
1417
+ * **Evidence supply**: runs only when the codegen feeder supplies
1418
+ * `context.shapes`. A shape whose `table` wasn't a static string literal (no
1419
+ * resolvable name) is skipped rather than guessed at, so the lint under-reports
1420
+ * rather than raising false alarms.
1421
+ */
1422
+ declare const shapeUnknownTable: Lint;
1423
+ /**
1288
1424
  * Flags a `ctx.sql` tagged-template that splices an unparameterized
1289
1425
  * string-building expression into the query.
1290
1426
  *
@@ -1448,4 +1584,4 @@ interface RunAdvisorOptions {
1448
1584
  * `static` lints at build time and defer `runtime` lints to a live shard.
1449
1585
  */
1450
1586
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
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 };
1587
+ 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 AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, 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, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
package/dist/index.d.ts CHANGED
@@ -214,6 +214,28 @@ interface AdvisorMaskProcedure {
214
214
  visibility: "internal" | "public";
215
215
  }
216
216
  /**
217
+ * One whole-row `ctx.db.replace(id, document)` write discovered inside a custom
218
+ * mutator's authoritative `server` impl (`lunora/mutators.ts`) — the input the
219
+ * `mutator_full_row_replace` lint consumes.
220
+ *
221
+ * In the local-first sync engine a `replace` overwrites the entire row, so a
222
+ * concurrent edit to a *different* column (committed between this mutator's read
223
+ * and its write) is silently clobbered. `patch(id, { onlyTheField })` merges at
224
+ * the column level instead, letting independent field edits coexist — the
225
+ * blessed pattern for mutators on a synced (poke-live) table. Produced by the
226
+ * codegen feeder, which attributes each `replace` to the mutator export
227
+ * performing it; runtime callers don't supply it, so the lint finds nothing
228
+ * there.
229
+ */
230
+ interface AdvisorMutatorWrite {
231
+ /** The mutator export whose `server` impl performs the replace (e.g. `renameChannel`). */
232
+ exportName: string;
233
+ /** Openable source path the replace appears in — always `lunora/mutators.ts`. */
234
+ file: string;
235
+ /** 1-based line of the `replace(...)` call, or `0` when unknown. */
236
+ line: number;
237
+ }
238
+ /**
217
239
  * One non-deterministic API call discovered lexically inside a `query(...)` or
218
240
  * `mutation(...)` handler body — the input the `nondeterministic_query_mutation`
219
241
  * lint consumes. Produced by the codegen feeder, which walks each exported
@@ -384,6 +406,16 @@ interface AdvisorTable {
384
406
  optionalFields?: ReadonlySet<string>;
385
407
  /** Declared relations (`.relations((r) => …)`). */
386
408
  relations: ReadonlyArray<AdvisorRelation>;
409
+ /**
410
+ * Storage tier the table is declared in: `"global"` (a `.global()` table,
411
+ * lives in D1 — the cross-shard tier), `"shardBy"` (partitioned across
412
+ * shard DOs by a key), or `"root"` (the default single-DO table). Read by
413
+ * the `shape_*` lints to flag replication shapes targeting a `.global()`
414
+ * table (poll-refreshed/latency-tiered, not poke-live). Optional — the
415
+ * codegen feeder always supplies it, the runtime feeder derives it; a feeder
416
+ * that omits it leaves tier-sensitive lints to treat the table as local.
417
+ */
418
+ shardKind?: "global" | "root" | "shardBy";
387
419
  }
388
420
  /**
389
421
  * One declared index, flattened across Lunora's index kinds so a single lint can
@@ -440,6 +472,28 @@ interface AdvisorSecretLiteral {
440
472
  preview: string;
441
473
  }
442
474
  /**
475
+ * A replication shape declared via `defineShape({ table, where, columns? })` in
476
+ * `lunora/shapes.ts` (the local-first sync engine's partial-replication unit).
477
+ * The `shape_*` lints cross-reference each shape's {@link AdvisorShape.table}
478
+ * against the declared schema to flag a shape targeting an unknown table or a
479
+ * `.global()` table (which replicates through the latency-tiered D1 poll path,
480
+ * not the poke-live op-log). Supplied by the codegen feeder, which lifts only
481
+ * the export name + the static `table` literal; absent for runtime callers,
482
+ * where the shape lints find nothing.
483
+ */
484
+ interface AdvisorShape {
485
+ /** Export binding name — the shape's registry key (e.g. `channelMessages`). */
486
+ exportName: string;
487
+ /** File the shape is declared in (relative, for the operator to open). */
488
+ file: string;
489
+ /**
490
+ * The `table` string literal the shape replicates from, or `undefined` when
491
+ * the feeder could not read it as a plain string literal — tier-sensitive
492
+ * lints skip a shape with no resolvable table rather than guessing.
493
+ */
494
+ table?: string;
495
+ }
496
+ /**
443
497
  * One shard's observed traffic share — the input the `hot_shard` runtime lint
444
498
  * consumes. Produced by the studio backend, which fans out over a sharded
445
499
  * function's shards and reads each shard's recorded request volume from the
@@ -660,6 +714,14 @@ interface LintContext {
660
714
  */
661
715
  maskProcedures?: ReadonlyArray<AdvisorMaskProcedure>;
662
716
  /**
717
+ * Whole-row `ctx.db.replace(id, document)` writes lifted from custom
718
+ * mutators' authoritative `server` impls (the `mutator_full_row_replace`
719
+ * input). Each `replace` overwrites the entire row, clobbering a concurrent
720
+ * edit to a different column on a synced table. Supplied by the codegen
721
+ * feeder; absent for runtime callers, where the lint finds nothing.
722
+ */
723
+ mutatorWrites?: ReadonlyArray<AdvisorMutatorWrite>;
724
+ /**
663
725
  * Non-deterministic API calls (`Date.now`, `Math.random`,
664
726
  * `crypto.randomUUID`, `crypto.getRandomValues`, `fetch`) discovered lexically
665
727
  * inside `query`/`mutation` handler bodies — the `nondeterministic_query_mutation`
@@ -708,6 +770,14 @@ interface LintContext {
708
770
  */
709
771
  secretLiterals?: ReadonlyArray<AdvisorSecretLiteral>;
710
772
  /**
773
+ * Replication shapes declared via `defineShape` in `lunora/shapes.ts` — the
774
+ * `shape_unknown_table` and `shape_targets_global_table` lint input. Each
775
+ * carries the export name and its static `table` literal, cross-referenced
776
+ * against {@link LintContext.schema}. Supplied by the codegen feeder; absent
777
+ * for runtime callers, where the shape lints find nothing.
778
+ */
779
+ shapes?: ReadonlyArray<AdvisorShape>;
780
+ /**
711
781
  * Per-shard observed traffic — the `hot_shard` lint input. Supplied by the
712
782
  * studio backend, which fans out over a sharded function's shards and reads
713
783
  * each shard's recorded request volume from the durable `__lunora_metrics`
@@ -1133,6 +1203,30 @@ declare const indexReferencesUnknownField: Lint;
1133
1203
  */
1134
1204
  declare const maskUncoveredPiiColumn: Lint;
1135
1205
  /**
1206
+ * Flags a custom mutator whose authoritative `server` impl writes a row with
1207
+ * `ctx.db.replace(id, document)` — a whole-document overwrite.
1208
+ *
1209
+ * The local-first sync engine serializes mutators in the shard DO, so two
1210
+ * mutators that touch the *same row* but *different columns* both run to
1211
+ * completion — but only if each writes its own column. A `replace` overwrites
1212
+ * the entire row from the document the mutator assembled, so a concurrent edit
1213
+ * to another column (committed between this mutator's read and its write, or
1214
+ * carried as a pending optimistic overlay on a client) is silently clobbered:
1215
+ * the kind of "two offline edits to different fields fight each other" data loss
1216
+ * a column-level merge avoids. `ctx.db.patch(id, { onlyTheChangedField })`
1217
+ * merges at the column level instead, so independent field edits coexist.
1218
+ *
1219
+ * `WARN`, not `ERROR`: `replace` is legitimate when the mutator genuinely owns
1220
+ * the whole row (a full-form save, a state-machine transition that rewrites
1221
+ * every field). The lint just surfaces the column-clobber risk so a developer
1222
+ * reaches for `patch` by default on a synced table.
1223
+ *
1224
+ * **Evidence supply**: runs only when the codegen feeder supplies
1225
+ * `context.mutatorWrites` (each a `replace` call lifted from a mutator's inline
1226
+ * `server` body); absent for runtime callers, where the lint finds nothing.
1227
+ */
1228
+ declare const mutatorFullRowReplace: Lint;
1229
+ /**
1136
1230
  * Flags a non-deterministic API call inside a `query(...)` or `mutation(...)`
1137
1231
  * handler body.
1138
1232
  *
@@ -1285,6 +1379,48 @@ declare const relationReferencesUnknownTable: Lint;
1285
1379
  */
1286
1380
  declare const rlsUncoveredTable: Lint;
1287
1381
  /**
1382
+ * Flags a replication shape whose `table` is a `.global()` table.
1383
+ *
1384
+ * Poke-live replication is a per-shard-DO property: the shard owns its SQLite
1385
+ * and a monotonic `__cdc_log`, so a write produces an ordered op the DO pokes to
1386
+ * every subscriber at the next flush. A `.global()` table lives outside the
1387
+ * shard DO's SQLite op-log (in a global backend — D1, or Hyperdrive-fronted
1388
+ * Postgres/MySQL) — so a shape over a global table cannot be poke-live. It is
1389
+ * served through the cross-shard tier: **coordinator/poll-refreshed, latency-
1390
+ * tiered**, not live. That is a real and supported tier (it is the recommended
1391
+ * answer for cross-shard reads — denormalize, or move the joined table to
1392
+ * `.global()` and read through the global backend), but its freshness semantics
1393
+ * differ from a sharded shape's, so the boundary is surfaced rather than hidden.
1394
+ *
1395
+ * `WARN`, not `ERROR`: a global-table shape is a legitimate design once you
1396
+ * accept the poll-refresh latency; the lint just makes the tier explicit so a
1397
+ * developer does not assume poke-live freshness.
1398
+ *
1399
+ * **Evidence supply**: runs only when the codegen feeder supplies
1400
+ * `context.shapes`; the table's tier comes from the schema's `shardKind`. A
1401
+ * shape whose table is unknown (caught by `shape_unknown_table`) or whose tier
1402
+ * the feeder didn't supply is skipped.
1403
+ */
1404
+ declare const shapeTargetsGlobalTable: Lint;
1405
+ /**
1406
+ * Flags a replication shape whose `table` names a table that does not exist in
1407
+ * the schema.
1408
+ *
1409
+ * `defineShape({ table: "messages", … })` binds a shape to a table by a plain
1410
+ * string. A live `subscribeShape("…")` resolves that shape server-side and runs
1411
+ * its membership query against the named table — so a typo, a stale name after a
1412
+ * rename, or a copy-paste mistake produces a shape that can never resolve a
1413
+ * rowset: the subscription seeds empty and then errors at the first flush
1414
+ * (`no such table`). This is a definite, build-time-detectable break, so it is
1415
+ * an `ERROR` — surfaced before the broken shape ever ships.
1416
+ *
1417
+ * **Evidence supply**: runs only when the codegen feeder supplies
1418
+ * `context.shapes`. A shape whose `table` wasn't a static string literal (no
1419
+ * resolvable name) is skipped rather than guessed at, so the lint under-reports
1420
+ * rather than raising false alarms.
1421
+ */
1422
+ declare const shapeUnknownTable: Lint;
1423
+ /**
1288
1424
  * Flags a `ctx.sql` tagged-template that splices an unparameterized
1289
1425
  * string-building expression into the query.
1290
1426
  *
@@ -1448,4 +1584,4 @@ interface RunAdvisorOptions {
1448
1584
  * `static` lints at build time and defer `runtime` lints to a live shard.
1449
1585
  */
1450
1586
  declare const runAdvisor: (context: LintContext, options?: RunAdvisorOptions) => Finding[];
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 };
1587
+ 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 AdvisorMutatorWrite, type AdvisorNondeterministicCall, type AdvisorProcedureProtection, type AdvisorQueryRead, type AdvisorR2sqlCall, type AdvisorRelation, type AdvisorRlsProcedure, type AdvisorSchema, type AdvisorSecretLiteral, type AdvisorShape, 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, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
package/dist/index.mjs CHANGED
@@ -13,6 +13,7 @@ import hardcodedSecret from './packem_shared/hardcodedSecret-W2pz1UZB.mjs';
13
13
  import hyperdriveOutsideAction from './packem_shared/hyperdriveOutsideAction-BgZqX7Xg.mjs';
14
14
  import indexReferencesUnknownField from './packem_shared/indexReferencesUnknownField-DH0_dbUY.mjs';
15
15
  import maskUncoveredPiiColumn from './packem_shared/maskUncoveredPiiColumn-DjGIPG6M.mjs';
16
+ import mutatorFullRowReplace from './packem_shared/mutatorFullRowReplace-BJnNDaIV.mjs';
16
17
  import nondeterministicQueryMutation from './packem_shared/nondeterministicQueryMutation-GXES1fLp.mjs';
17
18
  import policyReferencesUnknownTable from './packem_shared/policyReferencesUnknownTable-DtaIEovd.mjs';
18
19
  import publicArgumentUsesAny from './packem_shared/publicArgumentUsesAny-C71b2NCf.mjs';
@@ -21,6 +22,8 @@ import r2sqlOutsideAction from './packem_shared/r2sqlOutsideAction-CtqxvMuV.mjs'
21
22
  import relationReferencesUnknownField from './packem_shared/relationReferencesUnknownField-YznyXt_7.mjs';
22
23
  import relationReferencesUnknownTable from './packem_shared/relationReferencesUnknownTable-DrorpKYe.mjs';
23
24
  import rlsUncoveredTable from './packem_shared/rlsUncoveredTable-CxEfZ5eZ.mjs';
25
+ import shapeTargetsGlobalTable from './packem_shared/shapeTargetsGlobalTable-DHrf4Koi.mjs';
26
+ import shapeUnknownTable from './packem_shared/shapeUnknownTable-C8aDWFoe.mjs';
24
27
  import sqlInjectionRisk from './packem_shared/sqlInjectionRisk-zwytYGLt.mjs';
25
28
  import tableWithoutInsert from './packem_shared/tableWithoutInsert-CbbaYIP4.mjs';
26
29
  import unboundedStringArgument from './packem_shared/unboundedStringArgument-DThg2-wt.mjs';
@@ -30,13 +33,14 @@ import userCreatingMutationWithoutCaptcha from './packem_shared/userCreatingMuta
30
33
  import workflowUnknownTarget from './packem_shared/workflowUnknownTarget-Cdd7WhKQ.mjs';
31
34
  import workflowUnused from './packem_shared/workflowUnused-D0jHxdz9.mjs';
32
35
  export { AE_METRIC_EVENTS, loadAnalyticsRuntimeMetrics } from './packem_shared/AE_METRIC_EVENTS-DexctYv6.mjs';
33
- export { fromServerSchema } from './packem_shared/fromServerSchema-DinF1nph.mjs';
36
+ export { fromServerSchema } from './packem_shared/fromServerSchema-TWb7dTHC.mjs';
34
37
 
35
38
  const STATIC_LINTS = [
36
39
  indexReferencesUnknownField,
37
40
  relationReferencesUnknownTable,
38
41
  relationReferencesUnknownField,
39
42
  workflowUnknownTarget,
43
+ shapeUnknownTable,
40
44
  emptyIndex,
41
45
  circularFk,
42
46
  unindexedForeignKey,
@@ -45,6 +49,8 @@ const STATIC_LINTS = [
45
49
  tableWithoutInsert,
46
50
  workflowUnused,
47
51
  filterWithoutIndex,
52
+ shapeTargetsGlobalTable,
53
+ mutatorFullRowReplace,
48
54
  nondeterministicQueryMutation,
49
55
  hyperdriveOutsideAction,
50
56
  r2sqlOutsideAction,
@@ -76,4 +82,4 @@ const runAdvisor = (context, options = {}) => {
76
82
  return findings;
77
83
  };
78
84
 
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 };
85
+ export { ALL_LINTS, RUNTIME_LINTS, STATIC_LINTS, adminRouteWithoutGuard, authApiCallWithoutHeaders, circularFk, constraintValidator, containerOversizedInstance, containerPublicInternet, duplicateIndex, emptyIndex, filterWithoutIndex, hardcodedSecret, hotShard, hyperdriveOutsideAction, indexReferencesUnknownField, indexUtilization, maskUncoveredPiiColumn, mutatorFullRowReplace, nondeterministicQueryMutation, policyReferencesUnknownTable, publicArgumentUsesAny, publicMutationWithoutRatelimit, r2sqlOutsideAction, relationReferencesUnknownField, relationReferencesUnknownTable, rlsUncoveredTable, runAdvisor, shapeTargetsGlobalTable, shapeUnknownTable, sqlInjectionRisk, tableWithoutInsert, unboundedStringArgument, unindexedForeignKey, unindexedRelationTarget, userCreatingMutationWithoutCaptcha, workflowUnknownTarget, workflowUnused };
@@ -32,6 +32,7 @@ const fromServerSchema = (schema) => {
32
32
  indexes,
33
33
  name,
34
34
  optionalFields,
35
+ shardKind: table.shardMode.kind,
35
36
  relations: Object.entries(table.relationMap).map(([accessor, relation]) => {
36
37
  return {
37
38
  field: relation.field,
@@ -0,0 +1,26 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const mutatorFullRowReplace = {
4
+ categories: ["SCHEMA"],
5
+ description: "A custom mutator's `server` impl writes with `ctx.db.replace(id, document)` — a whole-row overwrite. On a synced table this clobbers a concurrent edit to a different column; `ctx.db.patch(id, { field })` merges at the column level so independent field edits coexist.",
6
+ facing: "EXTERNAL",
7
+ level: "WARN",
8
+ name: "mutator_full_row_replace",
9
+ remediation: "Prefer `ctx.db.patch(id, { onlyTheChangedField })` so a concurrent edit to another column isn't lost. Keep `replace` only when the mutator genuinely owns the entire row (e.g. a full-form save).",
10
+ run: (context) => {
11
+ if (context.mutatorWrites === void 0) {
12
+ return [];
13
+ }
14
+ return context.mutatorWrites.map(
15
+ (write) => emit(mutatorFullRowReplace, {
16
+ cacheKey: `mutator_full_row_replace:${write.exportName}:${String(write.line)}`,
17
+ detail: `Mutator \`${write.exportName}\` (${write.file}:${String(write.line)}) writes with \`ctx.db.replace(...)\`, overwriting the whole row. A concurrent edit to a different column is clobbered — use \`ctx.db.patch(id, { field })\` to merge at the column level.`,
18
+ metadata: { exportName: write.exportName, file: write.file, line: write.line }
19
+ })
20
+ );
21
+ },
22
+ source: "static",
23
+ title: "Mutator overwrites the whole row (clobbers concurrent column edits)"
24
+ };
25
+
26
+ export { mutatorFullRowReplace as default };
@@ -0,0 +1,34 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const shapeTargetsGlobalTable = {
4
+ categories: ["PERFORMANCE"],
5
+ description: "A `defineShape` replicates from a `.global()` table. Global tables live outside the shard DO's SQLite op-log, so the shape is served through the cross-shard tier — coordinator/poll-refreshed and latency-tiered, not poke-live like a sharded shape.",
6
+ facing: "EXTERNAL",
7
+ level: "WARN",
8
+ name: "shape_targets_global_table",
9
+ remediation: "Expected if you want the cross-shard tier — just don't assume poke-live freshness; global shapes refresh on a poll. For live updates, replicate from a sharded table instead (denormalize the columns you need into the shard).",
10
+ run: (context) => {
11
+ if (context.shapes === void 0) {
12
+ return [];
13
+ }
14
+ const globalTables = new Set(context.schema.tables.filter((table) => table.shardKind === "global").map((table) => table.name));
15
+ const findings = [];
16
+ for (const shape of context.shapes) {
17
+ if (shape.table === void 0 || !globalTables.has(shape.table)) {
18
+ continue;
19
+ }
20
+ findings.push(
21
+ emit(shapeTargetsGlobalTable, {
22
+ cacheKey: `shape_targets_global_table:${shape.exportName}`,
23
+ detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from the \`.global()\` table \`${shape.table}\`. It is served through the cross-shard global tier — poll-refreshed and latency-tiered, not poke-live.`,
24
+ metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
25
+ })
26
+ );
27
+ }
28
+ return findings;
29
+ },
30
+ source: "static",
31
+ title: "Shape replicates from a global (cross-shard) table"
32
+ };
33
+
34
+ export { shapeTargetsGlobalTable as default };
@@ -0,0 +1,34 @@
1
+ import { e as emit } from './finding-Dm_zvzS1.mjs';
2
+
3
+ const shapeUnknownTable = {
4
+ categories: ["SCHEMA"],
5
+ description: "A `defineShape` is bound to a `table` name that does not exist in the schema. The shape can never resolve a rowset — its subscription seeds empty and errors at the first flush.",
6
+ facing: "INTERNAL",
7
+ level: "ERROR",
8
+ name: "shape_unknown_table",
9
+ remediation: "Fix the shape's `table` to a real table name (check for a typo or a table that was renamed/removed).",
10
+ run: (context) => {
11
+ if (context.shapes === void 0) {
12
+ return [];
13
+ }
14
+ const knownTables = new Set(context.schema.tables.map((table) => table.name));
15
+ const findings = [];
16
+ for (const shape of context.shapes) {
17
+ if (shape.table === void 0 || knownTables.has(shape.table)) {
18
+ continue;
19
+ }
20
+ findings.push(
21
+ emit(shapeUnknownTable, {
22
+ cacheKey: `shape_unknown_table:${shape.exportName}`,
23
+ detail: `Shape \`${shape.exportName}\` (${shape.file}) replicates from table \`${shape.table}\`, which is not declared in the schema. The shape can never resolve a rowset.`,
24
+ metadata: { exportName: shape.exportName, file: shape.file, table: shape.table }
25
+ })
26
+ );
27
+ }
28
+ return findings;
29
+ },
30
+ source: "static",
31
+ title: "Shape bound to an unknown table"
32
+ };
33
+
34
+ export { shapeUnknownTable as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/advisor",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.8",
4
4
  "description": "Schema & query lints (splinter-style advisors) for Lunora, feeding the Studio Advisors view",
5
5
  "keywords": [
6
6
  "advisor",
@@ -46,7 +46,7 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@lunora/server": "1.0.0-alpha.5"
49
+ "@lunora/server": "1.0.0-alpha.7"
50
50
  },
51
51
  "engines": {
52
52
  "node": "^22.15.0 || >=24.11.0"