@mobileaidev/ai-app-bridge 0.2.15 → 0.3.0-rc.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.
Files changed (156) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +306 -53
  4. package/bin/ai-app-bridge.js +51 -4504
  5. package/bin/android-permissions.js +152 -0
  6. package/bin/android-uia-xml.js +74 -0
  7. package/bin/artifact-paths.js +127 -1
  8. package/bin/bridge-forward.js +56 -0
  9. package/bin/command-discovery.js +86 -0
  10. package/bin/command-errors.js +60 -0
  11. package/bin/command-registry.js +504 -0
  12. package/bin/command-request.js +14 -0
  13. package/bin/command-router.js +80 -0
  14. package/bin/connection-cache.js +119 -0
  15. package/bin/device-provider.js +2934 -0
  16. package/bin/execution-host.js +596 -0
  17. package/bin/execution-runtime.js +101 -0
  18. package/bin/fact-codec.js +321 -0
  19. package/bin/fact-recorder.js +691 -0
  20. package/bin/fact-store.js +226 -0
  21. package/bin/feedback-probe.js +285 -0
  22. package/bin/intent/install-intent.js +267 -0
  23. package/bin/intent/intent-action-executor.js +129 -0
  24. package/bin/intent/intent-autonomous-adapter.js +46 -0
  25. package/bin/intent/intent-capture-port.js +64 -0
  26. package/bin/intent/intent-entry.js +226 -0
  27. package/bin/intent/intent-errors.js +28 -0
  28. package/bin/intent/intent-evidence-store.js +121 -0
  29. package/bin/intent/intent-lifetime.js +62 -0
  30. package/bin/intent/intent-observation-target.js +33 -0
  31. package/bin/intent/intent-observer.js +194 -0
  32. package/bin/intent/intent-production-adapter.js +334 -0
  33. package/bin/intent/intent-provider.js +27 -0
  34. package/bin/intent/intent-runtime.js +63 -0
  35. package/bin/intent/intent-worker.js +432 -0
  36. package/bin/intent/ios-intent-adapter.js +90 -0
  37. package/bin/intent/permission-intent.js +239 -0
  38. package/bin/intent/web-intent-adapter.js +31 -0
  39. package/bin/ios-device-outcome.js +47 -0
  40. package/bin/ios-execution.js +108 -0
  41. package/bin/ios-provider.js +497 -583
  42. package/bin/ios-runtime-binding.js +54 -0
  43. package/bin/ios-wda-execution.js +70 -0
  44. package/bin/ios-wda-port.js +98 -0
  45. package/bin/ios-wda-project.js +79 -0
  46. package/bin/mcp-server.js +83 -1040
  47. package/bin/mmap-scan-index.js +329 -0
  48. package/bin/observation-collector.js +862 -0
  49. package/bin/runtime-client.js +154 -0
  50. package/bin/runtime-directory.js +107 -0
  51. package/bin/runtime-protocol.js +36 -0
  52. package/bin/script/bounded-script-registry.js +103 -0
  53. package/bin/script/node-runtime-adapter.js +233 -0
  54. package/bin/script/progress-projector.js +63 -0
  55. package/bin/script/python-runtime-adapter.js +111 -0
  56. package/bin/script/rolling-summary.js +134 -0
  57. package/bin/script/script-agent-port.js +40 -0
  58. package/bin/script/script-assert.js +95 -0
  59. package/bin/script/script-capture-port.js +76 -0
  60. package/bin/script/script-catalog.js +85 -0
  61. package/bin/script/script-durable-restore.js +195 -0
  62. package/bin/script/script-entry-code.js +26 -0
  63. package/bin/script/script-entry-route.js +38 -0
  64. package/bin/script/script-entry.js +3 -0
  65. package/bin/script/script-errors.js +29 -0
  66. package/bin/script/script-evidence-store.js +22 -0
  67. package/bin/script/script-format-removed.js +26 -0
  68. package/bin/script/script-host-port.js +397 -0
  69. package/bin/script/script-ledger.js +64 -0
  70. package/bin/script/script-result.js +57 -0
  71. package/bin/script/script-sdk.js +152 -0
  72. package/bin/script/script-sdk.py +153 -0
  73. package/bin/script/script-session-channel.js +127 -0
  74. package/bin/script/script-spec.js +86 -0
  75. package/bin/script/script-supervisor.js +919 -0
  76. package/bin/script/templates/checkpoint-reentry.js +13 -0
  77. package/bin/segment-index.js +481 -0
  78. package/bin/segmented-fact-store.js +1571 -0
  79. package/bin/shared-kernel/android-h5-target.js +10 -0
  80. package/bin/shared-kernel/android-install-execution.js +176 -0
  81. package/bin/shared-kernel/android-sdk-endpoint.js +42 -0
  82. package/bin/shared-kernel/android-shell-execution.js +195 -0
  83. package/bin/shared-kernel/argument-schema.js +117 -0
  84. package/bin/shared-kernel/canonical-path.js +17 -0
  85. package/bin/shared-kernel/device-acknowledgements.js +53 -0
  86. package/bin/shared-kernel/device-completion-history.js +52 -0
  87. package/bin/shared-kernel/device-mutation-lease.js +219 -0
  88. package/bin/shared-kernel/device-ownership-recovery.js +95 -0
  89. package/bin/shared-kernel/device-ownership-store.js +94 -0
  90. package/bin/shared-kernel/evidence-adapters.js +251 -0
  91. package/bin/shared-kernel/evidence-archive.js +329 -0
  92. package/bin/shared-kernel/evidence-recording.js +131 -0
  93. package/bin/shared-kernel/evidence-schema.js +194 -0
  94. package/bin/shared-kernel/evidence-store.js +190 -0
  95. package/bin/shared-kernel/execution-admission.js +22 -0
  96. package/bin/shared-kernel/execution-contracts.js +171 -0
  97. package/bin/shared-kernel/execution-io.js +106 -0
  98. package/bin/shared-kernel/execution-ledger.js +125 -0
  99. package/bin/shared-kernel/execution-scope.js +87 -0
  100. package/bin/shared-kernel/execution-target.js +115 -0
  101. package/bin/shared-kernel/flutter-execution.js +13 -0
  102. package/bin/shared-kernel/flutter-h5-port.js +60 -0
  103. package/bin/shared-kernel/flutter-h5-target.js +9 -0
  104. package/bin/shared-kernel/flutter-target.js +75 -0
  105. package/bin/shared-kernel/h5-execution.js +11 -0
  106. package/bin/shared-kernel/h5-target.js +31 -0
  107. package/bin/shared-kernel/host-fact-store.js +49 -0
  108. package/bin/shared-kernel/ios-h5-target.js +9 -0
  109. package/bin/shared-kernel/ios-native-target.js +71 -0
  110. package/bin/shared-kernel/live-capture-query.js +115 -0
  111. package/bin/shared-kernel/managed-sdk-execution.js +78 -0
  112. package/bin/shared-kernel/native-execution.js +13 -0
  113. package/bin/shared-kernel/native-target.js +156 -0
  114. package/bin/shared-kernel/provider-command-contracts.js +55 -0
  115. package/bin/shared-kernel/recorded-payload-archive.js +195 -0
  116. package/bin/shared-kernel/request-context.js +35 -0
  117. package/bin/shared-kernel/semantic-node.js +55 -0
  118. package/bin/shared-kernel/summary-transformer.js +352 -0
  119. package/bin/shared-kernel/target-lease-protocol.js +47 -0
  120. package/bin/shared-kernel/text-wait.js +111 -0
  121. package/bin/shared-kernel/uia-execution.js +96 -0
  122. package/bin/shared-kernel/uia-protocol.js +214 -0
  123. package/bin/shared-kernel/uia-runtime-port.js +377 -0
  124. package/bin/shared-kernel/uia-target.js +39 -0
  125. package/bin/shared-kernel/web-dom-target.js +44 -0
  126. package/bin/shared-kernel/xml-attributes.js +25 -0
  127. package/bin/target-execution.js +275 -0
  128. package/bin/web/command-schema.js +60 -0
  129. package/bin/web/session-store.js +157 -0
  130. package/bin/web-provider.js +334 -553
  131. package/docs/COMMAND_CONTRACT.md +1563 -0
  132. package/docs/EVIDENCE_ARCHIVE.md +214 -0
  133. package/docs/INTENT_FOREGROUND.md +71 -0
  134. package/docs/INTENT_NATIVE_EDITING.md +79 -0
  135. package/docs/RELEASE.md +59 -0
  136. package/docs/SCRIPT_AUTHORING.md +489 -0
  137. package/node_modules/@mobileaidev/segmented-fact-store-native/LICENSE +201 -0
  138. package/node_modules/@mobileaidev/segmented-fact-store-native/NOTICE +7 -0
  139. package/node_modules/@mobileaidev/segmented-fact-store-native/binding.gyp +36 -0
  140. package/node_modules/@mobileaidev/segmented-fact-store-native/bindings/node/sfs_node.c +597 -0
  141. package/node_modules/@mobileaidev/segmented-fact-store-native/include/sfs.h +178 -0
  142. package/node_modules/@mobileaidev/segmented-fact-store-native/index.js +5 -0
  143. package/node_modules/@mobileaidev/segmented-fact-store-native/package.json +24 -0
  144. package/node_modules/@mobileaidev/segmented-fact-store-native/src/sfs.c +2349 -0
  145. package/package.json +60 -5
  146. package/runtime/ios-wda/AABWDABinding.h +19 -0
  147. package/runtime/ios-wda/AABWDABinding.m +97 -0
  148. package/runtime/ios-wda/AABWDAExecution.h +26 -0
  149. package/runtime/ios-wda/AABWDAExecution.m +172 -0
  150. package/runtime/ios-wda/AABWDAIntegration.h +71 -0
  151. package/runtime/ios-wda/AABWDAManagedRoutes.h +392 -0
  152. package/runtime/ios-wda/AABWDAReceiptStore.h +10 -0
  153. package/runtime/ios-wda/AABWDAReceiptStore.m +116 -0
  154. package/runtime/uia/ai-app-bridge-uia.jar +0 -0
  155. package/runtime/uia/manifest.json +22 -0
  156. package/skills/ai-app-bridge-use/SKILL.md +23 -360
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ async function main(ctx) {
4
+ const prior = ctx.resume();
5
+ if (prior && prior.step >= 1) {
6
+ return { passed: true, resumed: true, step: prior.step };
7
+ }
8
+ await ctx.call('status', {});
9
+ await ctx.checkpoint('after-status', { step: 1 });
10
+ return { passed: true, resumed: false, step: 1 };
11
+ }
12
+
13
+ module.exports = { main };
@@ -0,0 +1,481 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const DEFAULT_BUDGET_BYTES = 1024 * 1024 * 1024;
7
+
8
+ class SegmentIndex {
9
+ constructor(options = {}) {
10
+ const sqlite = resolveSqlite(options.sqliteModule);
11
+ const explicitPath = options.path || options.dbPath;
12
+ if (!explicitPath && !options.directory) {
13
+ throw new TypeError('SegmentIndex requires directory or path');
14
+ }
15
+ this.dbPath = path.resolve(explicitPath || path.join(options.directory, 'index.sqlite'));
16
+ this.directory = path.dirname(this.dbPath);
17
+ this.budgetBytes = positiveInteger(options.budgetBytes, DEFAULT_BUDGET_BYTES, 'budgetBytes');
18
+ this.pageSizeBytes = 4_096;
19
+ this.auxiliaryReserveBytes = 0;
20
+ this.maxPageCount = 1;
21
+ this.walAutocheckpointPages = 1;
22
+ this.journalSizeLimitBytes = this.pageSizeBytes;
23
+ this.configureCapacity();
24
+ fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
25
+ if (process.platform !== 'win32') fs.chmodSync(this.directory, 0o700);
26
+ this.db = null;
27
+ this.closed = false;
28
+ try {
29
+ this.db = new sqlite.DatabaseSync(this.dbPath);
30
+ this.configure();
31
+ this.createSchema();
32
+ restrictFiles(this.dbPath);
33
+ } catch (error) {
34
+ this.closed = true;
35
+ try {
36
+ this.db?.close();
37
+ } catch (_) {
38
+ // Preserve the database setup error.
39
+ }
40
+ throw error;
41
+ }
42
+ }
43
+
44
+ insert(fact, location) {
45
+ this.assertOpen();
46
+ validateIndexedFact(fact);
47
+ validateLocation(location);
48
+ this.db.prepare(`
49
+ INSERT INTO segment_records (
50
+ sequence, partition_name, target_key, runtime_epoch, action_id,
51
+ dedupe_key, occurred_at_ms, observed_at_ms, ingested_at_ms,
52
+ segment_id, frame_offset, payload_length
53
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
54
+ ON CONFLICT(sequence) DO UPDATE SET
55
+ partition_name = excluded.partition_name,
56
+ target_key = excluded.target_key,
57
+ runtime_epoch = excluded.runtime_epoch,
58
+ action_id = excluded.action_id,
59
+ dedupe_key = excluded.dedupe_key,
60
+ occurred_at_ms = excluded.occurred_at_ms,
61
+ observed_at_ms = excluded.observed_at_ms,
62
+ ingested_at_ms = excluded.ingested_at_ms,
63
+ segment_id = excluded.segment_id,
64
+ frame_offset = excluded.frame_offset,
65
+ payload_length = excluded.payload_length
66
+ `).run(
67
+ location.sequence,
68
+ fact.partition,
69
+ fact.targetKey,
70
+ fact.runtimeEpoch,
71
+ fact.actionId,
72
+ fact.dedupeKey,
73
+ fact.timestamps.occurredAtMs,
74
+ fact.timestamps.observedAtMs,
75
+ fact.timestamps.ingestedAtMs,
76
+ location.segmentId,
77
+ location.frameOffset,
78
+ location.payloadLength,
79
+ );
80
+ return this.locationForSequence(location.sequence);
81
+ }
82
+
83
+ insertBatch(records) {
84
+ this.assertOpen();
85
+ if (!Array.isArray(records)) throw new TypeError('records must be an array');
86
+ this.db.exec('BEGIN IMMEDIATE');
87
+ try {
88
+ for (const record of records) this.insert(record.fact, record.location);
89
+ this.db.exec('COMMIT');
90
+ } catch (error) {
91
+ try {
92
+ this.db.exec('ROLLBACK');
93
+ } catch (_) {
94
+ // Preserve the original indexing error.
95
+ }
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ query(query = {}) {
101
+ this.assertOpen();
102
+ const normalized = normalizeQuery(query);
103
+ const clauses = ['sequence > ?'];
104
+ const parameters = [normalized.afterSequence];
105
+ if (normalized.throughSequence !== null) {
106
+ clauses.push('sequence <= ?');
107
+ parameters.push(normalized.throughSequence);
108
+ }
109
+ if (normalized.targetKey !== null) {
110
+ clauses.push('target_key = ?');
111
+ parameters.push(normalized.targetKey);
112
+ }
113
+ if (normalized.partitions.length > 0) {
114
+ clauses.push(`partition_name IN (${normalized.partitions.map(() => '?').join(', ')})`);
115
+ parameters.push(...normalized.partitions);
116
+ }
117
+ if (normalized.runtimeEpoch !== null) {
118
+ clauses.push('runtime_epoch = ?');
119
+ parameters.push(normalized.runtimeEpoch);
120
+ }
121
+ if (normalized.hasActionId) {
122
+ if (normalized.actionId === null) clauses.push('action_id IS NULL');
123
+ else {
124
+ clauses.push('action_id = ?');
125
+ parameters.push(normalized.actionId);
126
+ }
127
+ }
128
+ if (normalized.fromObservedAtMs !== null) {
129
+ clauses.push('observed_at_ms >= ?');
130
+ parameters.push(normalized.fromObservedAtMs);
131
+ }
132
+ if (normalized.toObservedAtMs !== null) {
133
+ clauses.push('observed_at_ms <= ?');
134
+ parameters.push(normalized.toObservedAtMs);
135
+ }
136
+ const rows = this.db.prepare(`
137
+ SELECT *
138
+ FROM segment_records
139
+ WHERE ${clauses.join(' AND ')}
140
+ ORDER BY sequence ASC
141
+ LIMIT ?
142
+ `).all(...parameters, normalized.limit + 1);
143
+ return {
144
+ items: rows.slice(0, normalized.limit).map(rowToLocation),
145
+ hasMore: rows.length > normalized.limit,
146
+ };
147
+ }
148
+
149
+ findDedupe(dedupeKey) {
150
+ this.assertOpen();
151
+ if (dedupeKey === undefined || dedupeKey === null) return null;
152
+ const row = this.db.prepare('SELECT * FROM segment_records WHERE dedupe_key = ? LIMIT 1').get(String(dedupeKey));
153
+ return row ? rowToLocation(row) : null;
154
+ }
155
+
156
+ locationForSequence(sequence) {
157
+ this.assertOpen();
158
+ const row = this.db.prepare('SELECT * FROM segment_records WHERE sequence = ?').get(Number(sequence));
159
+ return row ? rowToLocation(row) : null;
160
+ }
161
+
162
+ maxSequence() {
163
+ this.assertOpen();
164
+ return Number(this.db.prepare('SELECT COALESCE(MAX(sequence), 0) AS value FROM segment_records').get().value);
165
+ }
166
+
167
+ metadata(key, createValue) {
168
+ this.assertOpen();
169
+ const normalized = String(key);
170
+ const row = this.db.prepare('SELECT value FROM segment_metadata WHERE key = ?').get(normalized);
171
+ if (row) return row.value;
172
+ if (typeof createValue !== 'function') return null;
173
+ const value = String(createValue());
174
+ this.db.prepare('INSERT OR IGNORE INTO segment_metadata(key, value) VALUES (?, ?)').run(normalized, value);
175
+ return this.db.prepare('SELECT value FROM segment_metadata WHERE key = ?').get(normalized).value;
176
+ }
177
+
178
+ setMetadata(key, value) {
179
+ this.assertOpen();
180
+ this.db.prepare(`
181
+ INSERT INTO segment_metadata(key, value) VALUES (?, ?)
182
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
183
+ `).run(String(key), String(value));
184
+ }
185
+
186
+ prunePartitionBefore(partition, firstAvailableSequence) {
187
+ this.assertOpen();
188
+ const result = this.db.prepare(`
189
+ DELETE FROM segment_records
190
+ WHERE partition_name = ? AND sequence < ?
191
+ `).run(String(partition), Number(firstAvailableSequence));
192
+ return Number(result.changes || 0);
193
+ }
194
+
195
+ clear() {
196
+ this.assertOpen();
197
+ this.db.exec('DELETE FROM segment_records');
198
+ }
199
+
200
+ reclaim({ force = false } = {}) {
201
+ this.assertOpen();
202
+ const beforeBytes = projectionBytes(this.directory);
203
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
204
+ const row = this.db.prepare('PRAGMA auto_vacuum').get();
205
+ const autoVacuum = Number(row?.auto_vacuum ?? Object.values(row || {})[0] ?? 0);
206
+ if (autoVacuum === 2) {
207
+ this.db.exec('PRAGMA incremental_vacuum');
208
+ } else if (force) {
209
+ // VACUUM can transiently allocate almost a second database, violating
210
+ // the independent projection budget. The adapter will replace the full
211
+ // SQLite projection with bounded mmap scanning instead.
212
+ const error = new Error('SQLite projection requires a bounded rebuild');
213
+ error.code = 'sqlite_projection_rebuild_required';
214
+ throw error;
215
+ }
216
+ this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)');
217
+ restrictFiles(this.dbPath);
218
+ const afterBytes = projectionBytes(this.directory);
219
+ return {
220
+ beforeBytes,
221
+ afterBytes,
222
+ reclaimedBytes: Math.max(0, beforeBytes - afterBytes),
223
+ autoVacuum: Number(this.db.prepare('PRAGMA auto_vacuum').get().auto_vacuum),
224
+ };
225
+ }
226
+
227
+ status() {
228
+ if (this.closed) return { ...this.lastStatus, closed: true };
229
+ const count = Number(this.db.prepare('SELECT COUNT(*) AS value FROM segment_records').get().value);
230
+ const maxSequence = this.maxSequence();
231
+ const stat = safeStat(this.dbPath);
232
+ const totalBytes = projectionBytes(this.directory);
233
+ return {
234
+ ok: true,
235
+ adapter: 'sqlite-projection',
236
+ authoritative: false,
237
+ rebuildable: true,
238
+ dbPath: this.dbPath,
239
+ count,
240
+ maxSequence,
241
+ bytes: stat?.size || 0,
242
+ totalBytes,
243
+ budgetBytes: this.budgetBytes,
244
+ overQuota: totalBytes > this.budgetBytes,
245
+ quotaScope: 'host-sqlite-projection',
246
+ pageSizeBytes: this.pageSizeBytes,
247
+ maxPageCount: this.maxPageCount,
248
+ auxiliaryReserveBytes: this.auxiliaryReserveBytes,
249
+ walAutocheckpointPages: this.walAutocheckpointPages,
250
+ journalSizeLimitBytes: this.journalSizeLimitBytes,
251
+ closed: false,
252
+ };
253
+ }
254
+
255
+ storageBytes() {
256
+ this.assertOpen();
257
+ return projectionBytes(this.directory);
258
+ }
259
+
260
+ close() {
261
+ if (this.closed) return;
262
+ let firstError = null;
263
+ try {
264
+ this.lastStatus = this.status();
265
+ } catch (error) {
266
+ firstError = error;
267
+ }
268
+ try {
269
+ this.db.close();
270
+ } catch (error) {
271
+ if (!firstError) firstError = error;
272
+ }
273
+ this.closed = true;
274
+ if (firstError) throw firstError;
275
+ }
276
+
277
+ configure() {
278
+ // auto_vacuum must be selected before the first table is created (and
279
+ // before switching to WAL) for a new projection database.
280
+ this.db.exec('PRAGMA auto_vacuum = INCREMENTAL');
281
+ this.db.exec(`PRAGMA page_size = ${this.pageSizeBytes}`);
282
+ if (typeof this.db.prepare === 'function') {
283
+ const row = this.db.prepare('PRAGMA page_size').get();
284
+ this.pageSizeBytes = Number(row?.page_size ?? Object.values(row || {})[0] ?? this.pageSizeBytes);
285
+ }
286
+ this.configureCapacity();
287
+ // The size cap belongs only to this rebuildable Host projection. It never
288
+ // asks the authoritative mmap engine (or a mobile App store) to evict.
289
+ this.db.exec(`PRAGMA max_page_count = ${this.maxPageCount}`);
290
+ this.db.exec('PRAGMA journal_mode = WAL');
291
+ this.db.exec('PRAGMA synchronous = NORMAL');
292
+ this.db.exec('PRAGMA busy_timeout = 5000');
293
+ this.db.exec(`PRAGMA wal_autocheckpoint = ${this.walAutocheckpointPages}`);
294
+ this.db.exec(`PRAGMA journal_size_limit = ${this.journalSizeLimitBytes}`);
295
+ }
296
+
297
+ configureCapacity() {
298
+ const desiredReserve = Math.max(64 * 1024, Math.floor(this.budgetBytes * 0.02));
299
+ this.auxiliaryReserveBytes = Math.min(
300
+ Math.max(0, this.budgetBytes - this.pageSizeBytes),
301
+ 16 * 1024 * 1024,
302
+ desiredReserve,
303
+ );
304
+ const mainBudgetBytes = Math.max(
305
+ this.pageSizeBytes,
306
+ this.budgetBytes - this.auxiliaryReserveBytes,
307
+ );
308
+ this.maxPageCount = Math.max(1, Math.floor(mainBudgetBytes / this.pageSizeBytes));
309
+ const walReserveBytes = Math.max(0, this.auxiliaryReserveBytes - (32 * 1024));
310
+ this.walAutocheckpointPages = Math.max(
311
+ 1,
312
+ Math.min(64, Math.floor(walReserveBytes / Math.max(this.pageSizeBytes, 1))),
313
+ );
314
+ this.journalSizeLimitBytes = Math.max(this.pageSizeBytes, walReserveBytes);
315
+ }
316
+
317
+ createSchema() {
318
+ this.db.exec(`
319
+ CREATE TABLE IF NOT EXISTS segment_records (
320
+ sequence INTEGER PRIMARY KEY,
321
+ partition_name TEXT NOT NULL,
322
+ target_key TEXT NOT NULL,
323
+ runtime_epoch TEXT NOT NULL,
324
+ action_id TEXT,
325
+ dedupe_key TEXT,
326
+ occurred_at_ms INTEGER NOT NULL,
327
+ observed_at_ms INTEGER NOT NULL,
328
+ ingested_at_ms INTEGER NOT NULL,
329
+ segment_id INTEGER NOT NULL,
330
+ frame_offset INTEGER NOT NULL,
331
+ payload_length INTEGER NOT NULL
332
+ );
333
+ CREATE TABLE IF NOT EXISTS segment_metadata (
334
+ key TEXT PRIMARY KEY,
335
+ value TEXT NOT NULL
336
+ );
337
+ CREATE UNIQUE INDEX IF NOT EXISTS segment_records_dedupe
338
+ ON segment_records(dedupe_key)
339
+ WHERE dedupe_key IS NOT NULL;
340
+ CREATE INDEX IF NOT EXISTS segment_records_target_sequence
341
+ ON segment_records(target_key, sequence);
342
+ CREATE INDEX IF NOT EXISTS segment_records_partition_sequence
343
+ ON segment_records(partition_name, sequence);
344
+ CREATE INDEX IF NOT EXISTS segment_records_action_sequence
345
+ ON segment_records(action_id, sequence)
346
+ WHERE action_id IS NOT NULL;
347
+ `);
348
+ }
349
+
350
+ assertOpen() {
351
+ if (this.closed) throw new Error('SegmentIndex is closed');
352
+ }
353
+ }
354
+
355
+ function normalizeQuery(query) {
356
+ if (!query || typeof query !== 'object' || Array.isArray(query)) throw new TypeError('query must be an object');
357
+ const limit = query.limit === undefined ? 100 : Number(query.limit);
358
+ if (!Number.isSafeInteger(limit) || limit <= 0 || limit > 1000) {
359
+ throw new TypeError('limit must be an integer between 1 and 1000');
360
+ }
361
+ const afterSequence = query.afterSequence === undefined ? 0 : Number(query.afterSequence);
362
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) {
363
+ throw new TypeError('afterSequence must be a non-negative safe integer');
364
+ }
365
+ return {
366
+ afterSequence,
367
+ throughSequence: nullableInteger(query.throughSequence, 'throughSequence'),
368
+ limit,
369
+ targetKey: nullableString(query.targetKey),
370
+ partitions: query.partitions === undefined
371
+ ? []
372
+ : [...new Set((Array.isArray(query.partitions) ? query.partitions : [query.partitions]).map(String))].sort(),
373
+ runtimeEpoch: nullableString(query.runtimeEpoch),
374
+ hasActionId: Object.prototype.hasOwnProperty.call(query, 'actionId'),
375
+ actionId: nullableString(query.actionId),
376
+ fromObservedAtMs: nullableInteger(query.fromObservedAtMs, 'fromObservedAtMs'),
377
+ toObservedAtMs: nullableInteger(query.toObservedAtMs, 'toObservedAtMs'),
378
+ };
379
+ }
380
+
381
+ function rowToLocation(row) {
382
+ return {
383
+ sequence: Number(row.sequence),
384
+ partition: row.partition_name,
385
+ targetKey: row.target_key,
386
+ runtimeEpoch: row.runtime_epoch,
387
+ actionId: row.action_id,
388
+ dedupeKey: row.dedupe_key,
389
+ timestamps: {
390
+ occurredAtMs: Number(row.occurred_at_ms),
391
+ observedAtMs: Number(row.observed_at_ms),
392
+ ingestedAtMs: Number(row.ingested_at_ms),
393
+ },
394
+ segmentId: Number(row.segment_id),
395
+ frameOffset: Number(row.frame_offset),
396
+ payloadLength: Number(row.payload_length),
397
+ };
398
+ }
399
+
400
+ function validateIndexedFact(fact) {
401
+ if (!fact || typeof fact !== 'object' || !fact.timestamps) throw new TypeError('normalized fact is required');
402
+ for (const [name, value] of [
403
+ ['partition', fact.partition],
404
+ ['targetKey', fact.targetKey],
405
+ ['runtimeEpoch', fact.runtimeEpoch],
406
+ ]) {
407
+ if (value === undefined || value === null || String(value).length === 0) {
408
+ throw new TypeError(`${name} is required`);
409
+ }
410
+ }
411
+ }
412
+
413
+ function validateLocation(location) {
414
+ if (!location || typeof location !== 'object') throw new TypeError('location is required');
415
+ for (const name of ['sequence', 'segmentId', 'frameOffset', 'payloadLength']) {
416
+ const value = Number(location[name]);
417
+ if (!Number.isSafeInteger(value) || value < 0 || (name === 'sequence' && value === 0)) {
418
+ throw new TypeError(`${name} must be a valid safe integer`);
419
+ }
420
+ }
421
+ }
422
+
423
+ function positiveInteger(value, fallback, name) {
424
+ const number = value === undefined ? fallback : Number(value);
425
+ if (!Number.isSafeInteger(number) || number <= 0) {
426
+ throw new TypeError(`${name} must be a positive safe integer`);
427
+ }
428
+ return number;
429
+ }
430
+
431
+ function resolveSqlite(injected) {
432
+ if (injected !== undefined) {
433
+ if (!injected || typeof injected.DatabaseSync !== 'function') throw new TypeError('sqliteModule.DatabaseSync is required');
434
+ return injected;
435
+ }
436
+ return require('node:sqlite');
437
+ }
438
+
439
+ function nullableString(value) {
440
+ return value === undefined || value === null ? null : String(value);
441
+ }
442
+
443
+ function nullableInteger(value, name) {
444
+ if (value === undefined || value === null) return null;
445
+ const number = Number(value);
446
+ if (!Number.isSafeInteger(number)) throw new TypeError(`${name} must be a safe integer`);
447
+ return number;
448
+ }
449
+
450
+ function restrictFiles(dbPath) {
451
+ if (process.platform === 'win32') return;
452
+ for (const filePath of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
453
+ try {
454
+ fs.chmodSync(filePath, 0o600);
455
+ } catch (error) {
456
+ if (error?.code !== 'ENOENT') throw error;
457
+ }
458
+ }
459
+ }
460
+
461
+ function safeStat(filePath) {
462
+ try {
463
+ return fs.statSync(filePath);
464
+ } catch (error) {
465
+ if (error?.code === 'ENOENT') return null;
466
+ throw error;
467
+ }
468
+ }
469
+
470
+ function projectionBytes(directory) {
471
+ let total = 0;
472
+ for (const name of ['index.sqlite', 'index.sqlite-wal', 'index.sqlite-shm']) {
473
+ total += safeStat(path.join(directory, name))?.size || 0;
474
+ }
475
+ return total;
476
+ }
477
+
478
+ module.exports = {
479
+ DEFAULT_BUDGET_BYTES,
480
+ SegmentIndex,
481
+ };