@camstack/system 1.2.133 → 1.2.135

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 (56) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +53 -6
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +53 -6
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.js +1 -1
  24. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  25. package/dist/builtins/loki-logging/index.js +1 -1
  26. package/dist/builtins/loki-logging/index.mjs +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  29. package/dist/builtins/platform-probe/index.js +1 -1
  30. package/dist/builtins/platform-probe/index.mjs +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  33. package/dist/builtins/snapshot/index.js +1 -1
  34. package/dist/builtins/snapshot/index.mjs +1 -1
  35. package/dist/builtins/sqlite-storage/analyze-stats.d.ts +41 -0
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  37. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  38. package/dist/builtins/sqlite-storage/query-profiler.d.ts +74 -1
  39. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +67 -1
  40. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  41. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  42. package/dist/builtins/sqlite-storage/statement-shape.d.ts +64 -0
  43. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  45. package/dist/builtins/system-config/system-config.addon.js +1 -1
  46. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  47. package/dist/builtins/winston-logging/index.js +1 -1
  48. package/dist/builtins/winston-logging/index.mjs +1 -1
  49. package/dist/{dist-BUEU7B1V.mjs → dist-Bh25kEUH.mjs} +62 -32
  50. package/dist/{dist-DqcaE-k1.js → dist-D-R0SuP-.js} +62 -32
  51. package/dist/index.js +18 -1
  52. package/dist/index.mjs +18 -1
  53. package/dist/kernel/config-manager.d.ts +4 -0
  54. package/dist/{retired-settings-keys-BhPleRL7.mjs → retired-settings-keys-BZbOXqch.mjs} +1 -1
  55. package/dist/{retired-settings-keys-KZnmtfsh.js → retired-settings-keys-CqD6rSQx.js} +1 -1
  56. package/package.json +1 -1
@@ -35,6 +35,23 @@ import { IScopedLogger } from '@camstack/types';
35
35
  * statement that really does take seconds. Both are needed; neither is
36
36
  * sufficient.
37
37
  *
38
+ * ## Why the SQL shape, and why the plan
39
+ *
40
+ * `(op, collection)` names a TABLE, not a call site. On 2026-08-25 the busiest
41
+ * line of a 90 s operator stall was `query pipeline-analytics:tracks — 1005
42
+ * calls, max 1615 ms` and eleven different reads in one addon are spelled
43
+ * exactly that way; the aggregate accused all of them and cleared none. So the
44
+ * shape of the prepared statement (`statement-shape.ts`) is part of the
45
+ * aggregate KEY, and the report prints it.
46
+ *
47
+ * A shape says WHICH query. It does not say why it is slow — "1.6 s for 501
48
+ * rows" is a full scan and "1.6 s for 501 rows" is also a cold disk, and those
49
+ * are the same line. `EXPLAIN QUERY PLAN` is the only thing that separates
50
+ * them, and it is attached to the slow-call WARN because that is where the
51
+ * question is asked. It is asked ONCE PER SHAPE PER WINDOW: a scan that fires
52
+ * the WARN fires it repeatedly, and a plan re-printed forty times is how the
53
+ * one line that mattered stops being read.
54
+ *
38
55
  * ## Cost
39
56
  *
40
57
  * Two `Date.now()` calls and one `Map` lookup per statement. At the measured
@@ -64,17 +81,37 @@ export interface SqliteOpSample {
64
81
  readonly deviceId?: number;
65
82
  /** See {@link SqliteOpLabel.owner}. */
66
83
  readonly owner?: string;
84
+ /** See {@link SqliteOpLabel.sql}. */
85
+ readonly sql?: string;
86
+ /** See {@link SqliteOpLabel.params}. */
87
+ readonly params?: readonly unknown[];
67
88
  }
68
- /** One `(op, collection, owner)` triple's totals over a report window. */
89
+ /** One `(op, collection, owner, shape)` tuple's totals over a report window. */
69
90
  export interface SqliteOpStat {
70
91
  readonly op: string;
71
92
  readonly collection: string;
72
93
  readonly owner?: string;
94
+ /** `statement-shape.ts` fingerprint; absent for a fixed-shape statement the
95
+ * engine did not hand to the profiler. */
96
+ readonly shape?: string;
73
97
  readonly calls: number;
74
98
  readonly totalMs: number;
75
99
  readonly maxMs: number;
76
100
  readonly rows: number;
77
101
  }
102
+ /**
103
+ * Runs `EXPLAIN QUERY PLAN` for a statement on the connection that executed it.
104
+ *
105
+ * Injected rather than reached for: the profiler must not own a database
106
+ * handle, and the plan is only trustworthy from the SAME connection — a second
107
+ * handle can differ in `sqlite_stat1` visibility and in temp-schema state, and
108
+ * a plan taken from a connection that is not the one that stalled describes a
109
+ * query nobody ran.
110
+ *
111
+ * Returns one string per plan row (SQLite's `detail` column). Must not throw —
112
+ * an unexplainable statement yields `[]`.
113
+ */
114
+ export type SqlExplainer = (sql: string, params: readonly unknown[]) => readonly string[];
78
115
  /**
79
116
  * Duration at which one statement is worth a WARN on its own, in ms.
80
117
  *
@@ -104,6 +141,8 @@ export interface SqliteOpProfilerOptions {
104
141
  readonly reportIntervalMs?: number;
105
142
  readonly reportFloorMs?: number;
106
143
  readonly topN?: number;
144
+ /** Absent ⇒ slow-call WARNs carry no plan. See {@link SqlExplainer}. */
145
+ readonly explain?: SqlExplainer;
107
146
  }
108
147
  /** Identity of a measured call, without the numbers `measure` supplies itself. */
109
148
  export interface SqliteOpLabel {
@@ -117,6 +156,27 @@ export interface SqliteOpLabel {
117
156
  * exactly where the 67 660-call storm of 2026-08-25 stopped.
118
157
  */
119
158
  readonly owner?: string;
159
+ /**
160
+ * The statement the engine prepared, verbatim.
161
+ *
162
+ * Supplied only by the ops that BUILD their SQL from a filter — those are the
163
+ * ones whose cost varies by call site. A fixed-shape statement (`get`, `set`)
164
+ * is already fully described by `(op, collection)`.
165
+ *
166
+ * Used for two things and nothing else: the shape that becomes part of the
167
+ * aggregate key, and the `EXPLAIN QUERY PLAN` on a slow call. Never logged
168
+ * raw — see `statement-shape.ts` for what is dropped.
169
+ */
170
+ readonly sql?: string;
171
+ /**
172
+ * The values bound to {@link sql}.
173
+ *
174
+ * Needed because `EXPLAIN QUERY PLAN` on a statement with unbound parameters
175
+ * is not the plan that ran: SQLite's planner reads bound values for `LIKE`
176
+ * prefixes and for index-selection on `IN`. Never logged, in whole or in
177
+ * part — they carry device ids, keys and user data.
178
+ */
179
+ readonly params?: readonly unknown[];
120
180
  }
121
181
  export declare class SqliteOpProfiler {
122
182
  private readonly logger;
@@ -125,7 +185,11 @@ export declare class SqliteOpProfiler {
125
185
  private readonly reportIntervalMs;
126
186
  private readonly reportFloorMs;
127
187
  private readonly topN;
188
+ private readonly explain;
128
189
  private readonly stats;
190
+ /** Shapes already explained in this window — see the class doc on why the
191
+ * plan is printed once and not on every WARN. Cleared by `drain`. */
192
+ private readonly explainedShapes;
129
193
  private windowStartedAt;
130
194
  private timer;
131
195
  constructor(options: SqliteOpProfilerOptions);
@@ -139,6 +203,15 @@ export declare class SqliteOpProfiler {
139
203
  */
140
204
  measure<T>(label: SqliteOpLabel, run: () => T): T;
141
205
  record(sample: SqliteOpSample): void;
206
+ /**
207
+ * `EXPLAIN QUERY PLAN` for a statement that just held the loop, or nothing.
208
+ *
209
+ * Nothing when: no explainer was injected, the op carries no SQL, or this
210
+ * shape has already been explained in this window. The explainer is
211
+ * contracted not to throw and is guarded anyway — a profiler that can turn a
212
+ * slow query into a crash is worse than one that cannot explain it.
213
+ */
214
+ private planFor;
142
215
  /** Totals for the window, busiest first, and reset. */
143
216
  drain(): readonly SqliteOpStat[];
144
217
  /** Emit one aggregate line for the window, unless the engine was idle. */
@@ -45,6 +45,22 @@ export interface DroppedTable {
45
45
  export declare function scopeOwnerOf(filter: QueryFilter | undefined): {
46
46
  owner?: string;
47
47
  };
48
+ /**
49
+ * Stable identity of a declared schema — every property that changes the DDL
50
+ * or the set of columns a filter can name.
51
+ *
52
+ * Deliberately covers `indexes` too: an added index is as invisible to a
53
+ * name-only memo as an added column, and `CREATE INDEX IF NOT EXISTS` is
54
+ * cheap enough that re-running it costs nothing.
55
+ */
56
+ /**
57
+ * A `query` asked to read a column the collection cannot serve. Distinct type
58
+ * so a caller can map it to a 400 — it is always a programming error in the
59
+ * caller, never a condition of the data.
60
+ */
61
+ export declare class UnprojectableColumnError extends Error {
62
+ constructor(message: string);
63
+ }
48
64
  /**
49
65
  * SQLite implementation of ISettingsBackend.
50
66
  *
@@ -130,7 +146,7 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
130
146
  getEngineInfo(): Promise<DataStoreEngineInfo>;
131
147
  get({ namespace, collection, key }: SettingsGetInput): Promise<unknown>;
132
148
  set({ namespace, collection, key, value }: SettingsSetInput): Promise<void>;
133
- query<T extends object = Record<string, unknown>>({ namespace, collection, filter, }: SettingsQueryInput): Promise<readonly SettingsRecord<T>[]>;
149
+ query<T extends object = Record<string, unknown>>({ namespace, collection, filter, columns, }: SettingsQueryInput): Promise<readonly SettingsRecord<T>[]>;
134
150
  insert<T extends object = Record<string, unknown>>({ namespace, collection, record, }: SettingsInsertInput<T>): Promise<void>;
135
151
  update({ namespace, collection, id, data }: SettingsUpdateInput): Promise<void>;
136
152
  delete({ namespace, collection, key }: SettingsDeleteInput): Promise<void>;
@@ -151,6 +167,28 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
151
167
  * uncapped `tableUpdate` would have done.
152
168
  */
153
169
  updateWhere({ namespace, collection, filter, data, }: SettingsUpdateWhereInput): Promise<SettingsUpdateWhereResult>;
170
+ /**
171
+ * How many rows match — about the SAME rows {@link query} would return.
172
+ *
173
+ * That agreement is the entire contract, and until now it did not hold. This
174
+ * was the FOURTH hand-rolled copy of the filter logic (after `query`, the
175
+ * bulk mutations and `histogram`) and it compiled only `where`:
176
+ * `whereBetween`, `whereIn` and `whereNot` were dropped silently, so the
177
+ * count was not slow, it was WRONG, and always wrong in the same direction —
178
+ * too big, because every dropped predicate is a predicate that would have
179
+ * excluded rows.
180
+ *
181
+ * Two live callers decided on it. `TrackStore.countStaging({ deviceId,
182
+ * olderThanMs })` sends the cutoff as `whereBetween: { lastSeen: [0, …] }`
183
+ * and got the device's whole staging history instead. The notification-centre
184
+ * artefact gallery pages on `total` from this method while listing through
185
+ * `query`, and its filter is almost entirely `whereIn`/`whereBetween`
186
+ * (visibility, kind, device, time window, expiry) — so `total` counted rows
187
+ * the page could never show, including other users'.
188
+ *
189
+ * `select` mode, deliberately: `count` must be forgiving in exactly the way
190
+ * `query` is, or the two disagree again for a new reason.
191
+ */
154
192
  count({ namespace, collection, filter }: SettingsCountInput): Promise<number>;
155
193
  histogram({ namespace, collection, field, bucketSize, origin, filter, }: SettingsHistogramInput): Promise<readonly HistogramBucket[]>;
156
194
  isEmpty({ namespace, collection }: SettingsIsEmptyInput): Promise<boolean>;
@@ -221,6 +259,24 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
221
259
  * Returns null if the backend has not been initialized yet.
222
260
  */
223
261
  getDatabase(): Database.Database | null;
262
+ /**
263
+ * `EXPLAIN QUERY PLAN` for a statement, as one string per plan row.
264
+ *
265
+ * Answers the question the timing alone cannot: "1.6 s for 501 rows" is a
266
+ * full scan and is also a cold disk, and only the plan tells them apart. It
267
+ * runs on the SAME connection as the statement, because that is the only
268
+ * connection whose `sqlite_stat1` visibility and temp schema match.
269
+ *
270
+ * `EXPLAIN QUERY PLAN` prepares and plans; it does not execute the statement,
271
+ * so it costs no page reads of its own. It is still called only from the
272
+ * slow-call path, once per shape per window.
273
+ *
274
+ * Never throws. A plan that cannot be taken (a statement the engine will no
275
+ * longer prepare, a closed handle mid-shutdown) is a missing ANSWER, not a
276
+ * failure worth turning a slow query into a crash — the profiler falls back
277
+ * to the line it would have printed anyway.
278
+ */
279
+ private explainQueryPlan;
224
280
  /**
225
281
  * Run one synchronous statement under the profiler.
226
282
  *
@@ -265,6 +321,16 @@ export declare class SqliteSettingsBackend implements ISettingsBackend {
265
321
  * storage report turns it off without a rebuild.
266
322
  */
267
323
  private scheduleMapGrowthCheck;
324
+ /**
325
+ * One-shot `ANALYZE`, long after boot, only if the database has none.
326
+ *
327
+ * `analyze-stats.ts` has the measurement that justifies it: without
328
+ * `sqlite_stat1` the planner walked 150 133 media rows to return six, and one
329
+ * `ANALYZE` took that same query from 727 ms to 3.2 ms. It is scheduled
330
+ * rather than run inline because it is a 14 s synchronous statement on the
331
+ * one thread the cluster reads configuration through.
332
+ */
333
+ private scheduleIndexAnalysis;
268
334
  private scheduleStorageReport;
269
335
  private logStorageReport;
270
336
  private readPragmaNumber;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A stable, low-cardinality name for the SHAPE of a SQL statement.
3
+ *
4
+ * ## Why the profiler needs this
5
+ *
6
+ * `SqliteOpProfiler` aggregated on `(op, collection)`, and that pair is not an
7
+ * identity — it is a table. On 2026-08-25 the busiest line of a 90 s operator
8
+ * stall read `query pipeline-analytics:tracks — 1005 calls, 2694 ms,
9
+ * max 1615 ms, 17981 rows`, and there was no way to tell from it WHICH of the
10
+ * eleven distinct reads that addon issues against that one table had spent the
11
+ * second and a half. Every one of them was equally accused; the aggregate could
12
+ * not clear a single one.
13
+ *
14
+ * The shape is the missing half. It is derived from the statement the engine
15
+ * actually prepared, so it cannot drift from the code the way a hand-passed
16
+ * label would, and it collapses to exactly one string per call site — which is
17
+ * what makes it safe as part of an aggregate key.
18
+ *
19
+ * ## What is thrown away, and why
20
+ *
21
+ * - **The projection.** `queryDeclared` always selects every column, so the
22
+ * `SELECT` list is the table's schema restated — pure noise, and the single
23
+ * longest part of the statement.
24
+ * - **The table name.** It is already the `collection` field of the report.
25
+ * - **The width of an `IN` list.** `IN (?, ?, … ×500)` and `IN (?)` are the
26
+ * same call site; keeping the literal placeholders would make every page of
27
+ * a chunked read its own aggregate row. The COUNT is kept (`IN (?×500)`)
28
+ * because it is the difference between "reads one row" and "reads five
29
+ * hundred", which is the whole question being asked.
30
+ *
31
+ * Bound values are never included. They are not needed to name a call site, and
32
+ * a statement's parameters routinely carry device ids, keys and user data — a
33
+ * profiler line is not a place to put them.
34
+ */
35
+ /**
36
+ * Longest fingerprint kept, in characters.
37
+ *
38
+ * A shape is a log field printed five to a line; past this it stops being
39
+ * readable and starts pushing the numbers off the end. Every real statement in
40
+ * this engine fits well inside it — the cap exists for the pathological
41
+ * `whereIn` with forty distinct fields, not for the normal case.
42
+ */
43
+ export declare const SQL_SHAPE_MAX_LENGTH = 220;
44
+ /** Appended when a shape is cut at {@link SQL_SHAPE_MAX_LENGTH}. */
45
+ export declare const SQL_SHAPE_ELLIPSIS = "\u2026";
46
+ /**
47
+ * How many distinct raw statements the memo holds before it is dropped.
48
+ *
49
+ * The engine prepares its SQL fresh on every call, so without a memo this runs
50
+ * a handful of regexes per statement at a few hundred statements a second.
51
+ * With one, it runs them once per call site. The bound exists because the raw
52
+ * key is unbounded in principle (an `IN` list of a new width mints a new
53
+ * entry): at the cap the memo is cleared rather than grown, which costs one
54
+ * re-derivation per shape and cannot leak.
55
+ */
56
+ export declare const SQL_SHAPE_MEMO_MAX_ENTRIES = 512;
57
+ /**
58
+ * Name the shape of `sql`, whose table is `collection`.
59
+ *
60
+ * Memoised on the raw statement — see {@link SQL_SHAPE_MEMO_MAX_ENTRIES}.
61
+ */
62
+ export declare function sqlShape(sql: string, collection: string): string;
63
+ /** Test seam: forget every memoised shape. */
64
+ export declare function resetSqlShapeMemo(): void;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-BUEU7B1V.mjs";
1
+ import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-Bh25kEUH.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  //#region src/builtins/system-config/system-config.addon.ts
8
8
  /**
9
9
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -1,4 +1,4 @@
1
- import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  //#region src/builtins/system-config/system-config.addon.ts
3
3
  /**
4
4
  * Built-in `system-config` addon — Phase 4 of the settings redesign.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DqcaE-k1.js");
6
+ const require_dist = require("../../dist-D-R0SuP-.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  let node_path = require("node:path");
9
9
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-BUEU7B1V.mjs";
1
+ import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-Bh25kEUH.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  import * as path$1 from "node:path";
4
4
  import path from "node:path";
@@ -3704,24 +3704,6 @@ var RecordingRetentionSchema = z.object({
3704
3704
  maxSizeGb: z.number().min(0).optional()
3705
3705
  });
3706
3706
  /**
3707
- * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
3708
- * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
3709
- * previews at. Five graduated steps; absent on a config = `standard` (the
3710
- * shipped default, matching `sheet-geometry`/`sheet-composer`).
3711
- *
3712
- * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
3713
- * Each window's index sidecar carries its own tile dims, so a camera whose
3714
- * preset changed over time renders every historical window at the dims it was
3715
- * written with.
3716
- */
3717
- var ScrubThumbnailPresetSchema = z.enum([
3718
- "minimal",
3719
- "low",
3720
- "standard",
3721
- "high",
3722
- "max"
3723
- ]);
3724
- /**
3725
3707
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
3726
3708
  *
3727
3709
  * `bands` is the ONLY authored recording intent: what to record, when, and on
@@ -3729,7 +3711,11 @@ var ScrubThumbnailPresetSchema = z.enum([
3729
3711
  * other field is a storage knob (profiles, segment length, retention, scrub).
3730
3712
  *
3731
3713
  * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
3732
- * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
3714
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30,
3715
+ * and `scrubThumbnails` — a five-step fidelity knob for a sprite tier that was
3716
+ * deleted on 2026-07-24 and had ZERO consumers in the recorder — on 2026-08-25
3717
+ * (D62: a switch that writes a store nobody reads is worse than no switch).
3718
+ * Stored rows keep loading: the READ schema is `.strip()` (config-store.ts).
3733
3719
  * A stale caller must fail loudly — silently stripping its legacy intent would
3734
3720
  * persist a band-less config, i.e. silently stop recording the camera.
3735
3721
  */
@@ -3752,14 +3738,7 @@ var RecordingConfigSchema = z.object({
3752
3738
  * "off" is the absence of a covering band, never a band value.
3753
3739
  */
3754
3740
  bands: z.array(RecordingBandSchema).default([]),
3755
- retention: RecordingRetentionSchema.optional(),
3756
- /**
3757
- * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
3758
- * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
3759
- * windows only — existing sheets are immutable, and each window's index
3760
- * carries its own tile dims so mixed-preset history renders correctly.
3761
- */
3762
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
3741
+ retention: RecordingRetentionSchema.optional()
3763
3742
  }).strict();
3764
3743
  /**
3765
3744
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
@@ -8060,11 +8039,26 @@ var settingsStoreCapability = {
8060
8039
  key: z.string(),
8061
8040
  value: z.unknown()
8062
8041
  }), z.void(), { kind: "mutation" }),
8063
- /** Get all entries matching an optional filter. */
8042
+ /**
8043
+ * Rows matching a filter.
8044
+ *
8045
+ * `columns` is a SQL-level projection: the engine SELECTs only those
8046
+ * columns (plus the primary key) instead of the whole row. It is the only
8047
+ * way to stop paying disk for a column that is discarded — a heavy JSON
8048
+ * column dominates the row, and dropping it in JS after the read pays every
8049
+ * byte first. Measured on the live `pipeline-analytics:tracks` table: a
8050
+ * 501-row page is 4.6 MB whole and 0.11 MB projected, same index, same
8051
+ * plan.
8052
+ *
8053
+ * The engine REFUSES a column it cannot serve rather than omitting it —
8054
+ * see `SettingsQueryInput.columns` for why a projection may not be
8055
+ * forgiving the way a predicate is.
8056
+ */
8064
8057
  query: method(z.object({
8065
8058
  namespace: z.string().optional(),
8066
8059
  collection: z.string(),
8067
- filter: QueryFilterSchema.optional()
8060
+ filter: QueryFilterSchema.optional(),
8061
+ columns: z.array(z.string()).readonly().optional()
8068
8062
  }), z.array(SettingsRecordSchema).readonly()),
8069
8063
  /** Insert a new record. */
8070
8064
  insert: method(z.object({
@@ -8229,7 +8223,17 @@ var dataStoreProviderCapability = {
8229
8223
  query: method(z.object({
8230
8224
  namespace: z.string().optional(),
8231
8225
  collection: z.string(),
8232
- filter: QueryFilterSchema.optional()
8226
+ filter: QueryFilterSchema.optional(),
8227
+ /**
8228
+ * SQL-level column projection — MUST mirror `settings-store.query`.
8229
+ *
8230
+ * This is the second schema the same call passes through, and Zod
8231
+ * objects STRIP what they do not declare: a field present on the door
8232
+ * and absent here does not error, it silently disappears one hop before
8233
+ * the engine, and the caller concludes the projection does nothing.
8234
+ * `data-door-schema-parity.spec.ts` is what keeps the two in step.
8235
+ */
8236
+ columns: z.array(z.string()).readonly().optional()
8233
8237
  }), z.array(SettingsRecordSchema).readonly()),
8234
8238
  /** Insert a new record. */
8235
8239
  insert: method(z.object({
@@ -22863,7 +22867,27 @@ var faceGalleryCapability = {
22863
22867
  scope: "system",
22864
22868
  mode: "singleton",
22865
22869
  methods: {
22866
- listIdentities: method(z.void(), z.array(IdentitySchema).readonly()),
22870
+ listIdentities: method(z.object({
22871
+ /**
22872
+ * Inline {@link IdentitySchema.coverBase64} on every row.
22873
+ *
22874
+ * Default `false`, the same inversion `listRecentFaces` and
22875
+ * `listPlates` took on 2026-08-25 (see `include-crops-default.ts` for
22876
+ * why the burden belongs on the caller that WANTS the bytes). Measured
22877
+ * on the live hub the same day: four identities cost 40,979 B with the
22878
+ * covers inline, ~10 KB of base64 per row, on a query this UI mounts
22879
+ * four times and the viewer holds at `staleTime: 30_000`.
22880
+ *
22881
+ * Nothing loses its avatar: `coverMediaKey` is already on every row and
22882
+ * the `event-media` plane serves that key `immutable` with an ETag.
22883
+ *
22884
+ * **This is an INPUT field, so it does not reach the addon until the
22885
+ * next train** — the hub router validates cap inputs against its own
22886
+ * compiled Zod and strips a key it does not know. Until then the
22887
+ * provider sees `undefined`, which resolves to `false`: the cheap shape
22888
+ * is what ships, and the opt-in becomes reachable when the train lands.
22889
+ */
22890
+ includeCrops: z.boolean().optional() }).optional(), z.array(IdentitySchema).readonly()),
22867
22891
  createIdentity: method(z.object({ name: z.string().min(1) }), IdentitySchema, {
22868
22892
  kind: "mutation",
22869
22893
  auth: "admin"
@@ -26225,7 +26249,13 @@ var plateGalleryCapability = {
26225
26249
  kind: "mutation",
26226
26250
  auth: "admin"
26227
26251
  }),
26228
- listVehicles: method(z.void(), z.array(VehicleSchema).readonly()),
26252
+ listVehicles: method(z.object({
26253
+ /** Inline {@link VehicleSchema.coverBase64} on every row. Default
26254
+ * `false` — the vehicle twin of `faceGallery.listIdentities`'s
26255
+ * option; `coverMediaKey` + the `event-media` plane carry the picture.
26256
+ * INPUT field: stripped by the hub router until the train ships, which
26257
+ * resolves to `false` and is exactly the intended default. */
26258
+ includeCrops: z.boolean().optional() }).optional(), z.array(VehicleSchema).readonly()),
26229
26259
  createVehicle: method(z.object({ name: z.string().min(1) }), VehicleSchema, {
26230
26260
  kind: "mutation",
26231
26261
  auth: "admin"
@@ -3704,24 +3704,6 @@ var RecordingRetentionSchema = zod.z.object({
3704
3704
  maxSizeGb: zod.z.number().min(0).optional()
3705
3705
  });
3706
3706
  /**
3707
- * Scrub-thumbnail fidelity preset — the single per-camera selector bundling the
3708
- * sprite tile RESOLUTION + JPEG QUALITY the recorder packs timeline-scrub
3709
- * previews at. Five graduated steps; absent on a config = `standard` (the
3710
- * shipped default, matching `sheet-geometry`/`sheet-composer`).
3711
- *
3712
- * Existing sheets are IMMUTABLE — a changed preset applies to NEW windows only.
3713
- * Each window's index sidecar carries its own tile dims, so a camera whose
3714
- * preset changed over time renders every historical window at the dims it was
3715
- * written with.
3716
- */
3717
- var ScrubThumbnailPresetSchema = zod.z.enum([
3718
- "minimal",
3719
- "low",
3720
- "standard",
3721
- "high",
3722
- "max"
3723
- ]);
3724
- /**
3725
3707
  * The full per-camera recording intent — the wire shape of a RecordingTarget.
3726
3708
  *
3727
3709
  * `bands` is the ONLY authored recording intent: what to record, when, and on
@@ -3729,7 +3711,11 @@ var ScrubThumbnailPresetSchema = zod.z.enum([
3729
3711
  * other field is a storage knob (profiles, segment length, retention, scrub).
3730
3712
  *
3731
3713
  * STRICT on purpose: the legacy authoring surface (`schedule`/`schedules`/
3732
- * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30.
3714
+ * `triggers`/`preBufferSec`/`postBufferSec`/`rules`) was retired 2026-07-30,
3715
+ * and `scrubThumbnails` — a five-step fidelity knob for a sprite tier that was
3716
+ * deleted on 2026-07-24 and had ZERO consumers in the recorder — on 2026-08-25
3717
+ * (D62: a switch that writes a store nobody reads is worse than no switch).
3718
+ * Stored rows keep loading: the READ schema is `.strip()` (config-store.ts).
3733
3719
  * A stale caller must fail loudly — silently stripping its legacy intent would
3734
3720
  * persist a band-less config, i.e. silently stop recording the camera.
3735
3721
  */
@@ -3752,14 +3738,7 @@ var RecordingConfigSchema = zod.z.object({
3752
3738
  * "off" is the absence of a covering band, never a band value.
3753
3739
  */
3754
3740
  bands: zod.z.array(RecordingBandSchema).default([]),
3755
- retention: RecordingRetentionSchema.optional(),
3756
- /**
3757
- * Per-camera scrub-thumbnail fidelity preset (resolution + JPEG quality for
3758
- * timeline-scrub sprite previews). Absent = `standard`. Applies to NEW
3759
- * windows only — existing sheets are immutable, and each window's index
3760
- * carries its own tile dims so mixed-preset history renders correctly.
3761
- */
3762
- scrubThumbnails: ScrubThumbnailPresetSchema.optional()
3741
+ retention: RecordingRetentionSchema.optional()
3763
3742
  }).strict();
3764
3743
  /**
3765
3744
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
@@ -8060,11 +8039,26 @@ var settingsStoreCapability = {
8060
8039
  key: zod.z.string(),
8061
8040
  value: zod.z.unknown()
8062
8041
  }), zod.z.void(), { kind: "mutation" }),
8063
- /** Get all entries matching an optional filter. */
8042
+ /**
8043
+ * Rows matching a filter.
8044
+ *
8045
+ * `columns` is a SQL-level projection: the engine SELECTs only those
8046
+ * columns (plus the primary key) instead of the whole row. It is the only
8047
+ * way to stop paying disk for a column that is discarded — a heavy JSON
8048
+ * column dominates the row, and dropping it in JS after the read pays every
8049
+ * byte first. Measured on the live `pipeline-analytics:tracks` table: a
8050
+ * 501-row page is 4.6 MB whole and 0.11 MB projected, same index, same
8051
+ * plan.
8052
+ *
8053
+ * The engine REFUSES a column it cannot serve rather than omitting it —
8054
+ * see `SettingsQueryInput.columns` for why a projection may not be
8055
+ * forgiving the way a predicate is.
8056
+ */
8064
8057
  query: method(zod.z.object({
8065
8058
  namespace: zod.z.string().optional(),
8066
8059
  collection: zod.z.string(),
8067
- filter: QueryFilterSchema.optional()
8060
+ filter: QueryFilterSchema.optional(),
8061
+ columns: zod.z.array(zod.z.string()).readonly().optional()
8068
8062
  }), zod.z.array(SettingsRecordSchema).readonly()),
8069
8063
  /** Insert a new record. */
8070
8064
  insert: method(zod.z.object({
@@ -8229,7 +8223,17 @@ var dataStoreProviderCapability = {
8229
8223
  query: method(zod.z.object({
8230
8224
  namespace: zod.z.string().optional(),
8231
8225
  collection: zod.z.string(),
8232
- filter: QueryFilterSchema.optional()
8226
+ filter: QueryFilterSchema.optional(),
8227
+ /**
8228
+ * SQL-level column projection — MUST mirror `settings-store.query`.
8229
+ *
8230
+ * This is the second schema the same call passes through, and Zod
8231
+ * objects STRIP what they do not declare: a field present on the door
8232
+ * and absent here does not error, it silently disappears one hop before
8233
+ * the engine, and the caller concludes the projection does nothing.
8234
+ * `data-door-schema-parity.spec.ts` is what keeps the two in step.
8235
+ */
8236
+ columns: zod.z.array(zod.z.string()).readonly().optional()
8233
8237
  }), zod.z.array(SettingsRecordSchema).readonly()),
8234
8238
  /** Insert a new record. */
8235
8239
  insert: method(zod.z.object({
@@ -22863,7 +22867,27 @@ var faceGalleryCapability = {
22863
22867
  scope: "system",
22864
22868
  mode: "singleton",
22865
22869
  methods: {
22866
- listIdentities: method(zod.z.void(), zod.z.array(IdentitySchema).readonly()),
22870
+ listIdentities: method(zod.z.object({
22871
+ /**
22872
+ * Inline {@link IdentitySchema.coverBase64} on every row.
22873
+ *
22874
+ * Default `false`, the same inversion `listRecentFaces` and
22875
+ * `listPlates` took on 2026-08-25 (see `include-crops-default.ts` for
22876
+ * why the burden belongs on the caller that WANTS the bytes). Measured
22877
+ * on the live hub the same day: four identities cost 40,979 B with the
22878
+ * covers inline, ~10 KB of base64 per row, on a query this UI mounts
22879
+ * four times and the viewer holds at `staleTime: 30_000`.
22880
+ *
22881
+ * Nothing loses its avatar: `coverMediaKey` is already on every row and
22882
+ * the `event-media` plane serves that key `immutable` with an ETag.
22883
+ *
22884
+ * **This is an INPUT field, so it does not reach the addon until the
22885
+ * next train** — the hub router validates cap inputs against its own
22886
+ * compiled Zod and strips a key it does not know. Until then the
22887
+ * provider sees `undefined`, which resolves to `false`: the cheap shape
22888
+ * is what ships, and the opt-in becomes reachable when the train lands.
22889
+ */
22890
+ includeCrops: zod.z.boolean().optional() }).optional(), zod.z.array(IdentitySchema).readonly()),
22867
22891
  createIdentity: method(zod.z.object({ name: zod.z.string().min(1) }), IdentitySchema, {
22868
22892
  kind: "mutation",
22869
22893
  auth: "admin"
@@ -26225,7 +26249,13 @@ var plateGalleryCapability = {
26225
26249
  kind: "mutation",
26226
26250
  auth: "admin"
26227
26251
  }),
26228
- listVehicles: method(zod.z.void(), zod.z.array(VehicleSchema).readonly()),
26252
+ listVehicles: method(zod.z.object({
26253
+ /** Inline {@link VehicleSchema.coverBase64} on every row. Default
26254
+ * `false` — the vehicle twin of `faceGallery.listIdentities`'s
26255
+ * option; `coverMediaKey` + the `event-media` plane carry the picture.
26256
+ * INPUT field: stripped by the hub router until the train ships, which
26257
+ * resolves to `false` and is exactly the intended default. */
26258
+ includeCrops: zod.z.boolean().optional() }).optional(), zod.z.array(VehicleSchema).readonly()),
26229
26259
  createVehicle: method(zod.z.object({ name: zod.z.string().min(1) }), VehicleSchema, {
26230
26260
  kind: "mutation",
26231
26261
  auth: "admin"