@hraness/sponge 0.2.5 → 0.3.0

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 (227) hide show
  1. package/README.md +23 -10
  2. package/bin/sponge.js +8 -7
  3. package/dist/contracts/index.js +24 -6
  4. package/dist/contracts/validate.js +106 -29
  5. package/dist/migrations/008.d.ts +3 -0
  6. package/dist/migrations/008.js +5 -0
  7. package/dist/migrations/009.d.ts +3 -0
  8. package/dist/migrations/009.js +8 -0
  9. package/dist/migrations/010.d.ts +3 -0
  10. package/dist/migrations/010.js +11 -0
  11. package/dist/migrations/index.d.ts +15 -0
  12. package/dist/migrations/index.js +39 -0
  13. package/dist/src/acquisition/capture.js +87 -28
  14. package/dist/src/acquisition/cleanup.d.ts +0 -4
  15. package/dist/src/acquisition/cleanup.js +3 -5
  16. package/dist/src/acquisition/errors.js +17 -2
  17. package/dist/src/acquisition/files.d.ts +0 -1
  18. package/dist/src/acquisition/files.js +14 -11
  19. package/dist/src/acquisition/index.d.ts +2 -3
  20. package/dist/src/acquisition/index.js +1 -2
  21. package/dist/src/acquisition/lock.js +1 -1
  22. package/dist/src/acquisition/normalize.js +57 -18
  23. package/dist/src/acquisition/pins.js +22 -4
  24. package/dist/src/acquisition/process.js +67 -29
  25. package/dist/src/acquisition/runtime.d.ts +5 -7
  26. package/dist/src/acquisition/runtime.js +129 -68
  27. package/dist/src/acquisition/verify-helper.js +10 -3
  28. package/dist/src/annotations/quote.js +40 -12
  29. package/dist/src/annotations/store.d.ts +0 -1
  30. package/dist/src/annotations/store.js +66 -14
  31. package/dist/src/app/article.d.ts +2 -1
  32. package/dist/src/app/article.js +66 -70
  33. package/dist/src/app/bundle-files.d.ts +0 -4
  34. package/dist/src/app/bundle-files.js +11 -14
  35. package/dist/src/app/markdown.js +18 -6
  36. package/dist/src/app/source.d.ts +0 -4
  37. package/dist/src/app/source.js +18 -14
  38. package/dist/src/cli/arguments.d.ts +2 -8
  39. package/dist/src/cli/arguments.js +67 -80
  40. package/dist/src/cli/commands.d.ts +42 -0
  41. package/dist/src/cli/commands.js +795 -0
  42. package/dist/src/cli/config.js +12 -9
  43. package/dist/src/cli/digest.d.ts +1 -1
  44. package/dist/src/cli/digest.js +39 -17
  45. package/dist/src/cli/import.js +25 -4
  46. package/dist/src/cli/indexing.d.ts +2 -0
  47. package/dist/src/cli/indexing.js +26 -9
  48. package/dist/src/cli/main.js +47 -386
  49. package/dist/src/cli/portability.d.ts +0 -2
  50. package/dist/src/cli/portability.js +13 -8
  51. package/dist/src/cli/queue.js +12 -8
  52. package/dist/src/cli/runtimes.d.ts +1 -3
  53. package/dist/src/cli/runtimes.js +13 -6
  54. package/dist/src/cli/terminal.js +1 -1
  55. package/dist/src/cli/worker.d.ts +7 -0
  56. package/dist/src/cli/worker.js +54 -15
  57. package/dist/src/derivations/kinds.d.ts +34 -0
  58. package/dist/src/derivations/kinds.js +14 -0
  59. package/dist/src/derivations/types.d.ts +48 -0
  60. package/dist/src/derivations/types.js +1 -0
  61. package/dist/src/digest/derivation.d.ts +25 -0
  62. package/dist/src/digest/derivation.js +124 -0
  63. package/dist/src/digest/editorial.d.ts +10 -46
  64. package/dist/src/digest/editorial.js +73 -59
  65. package/dist/src/digest/endpoint.js +63 -27
  66. package/dist/src/digest/pipeline.d.ts +53 -32
  67. package/dist/src/digest/pipeline.js +357 -125
  68. package/dist/src/digest/schema.d.ts +44 -17
  69. package/dist/src/digest/schema.js +132 -44
  70. package/dist/src/errors.d.ts +46 -0
  71. package/dist/src/errors.js +117 -0
  72. package/dist/src/identity/query-policy.d.ts +0 -5
  73. package/dist/src/identity/query-policy.js +61 -9
  74. package/dist/src/identity/uri.d.ts +6 -5
  75. package/dist/src/identity/uri.js +37 -18
  76. package/dist/src/identity/visibility.d.ts +11 -1
  77. package/dist/src/identity/visibility.js +81 -17
  78. package/dist/src/importers/apply.js +104 -36
  79. package/dist/src/importers/custody.js +27 -9
  80. package/dist/src/importers/inspect.js +125 -38
  81. package/dist/src/importers/jungle-digest.d.ts +31 -24
  82. package/dist/src/importers/jungle-digest.js +217 -115
  83. package/dist/src/importers/jungle-store.d.ts +20 -11
  84. package/dist/src/importers/jungle-store.js +136 -41
  85. package/dist/src/importers/jungle.d.ts +32 -16
  86. package/dist/src/importers/jungle.js +145 -32
  87. package/dist/src/importers/manifest.js +57 -21
  88. package/dist/src/importers/verify-helper.js +18 -4
  89. package/dist/src/indexing/image.js +29 -10
  90. package/dist/src/indexing/index.d.ts +37 -6
  91. package/dist/src/indexing/index.js +360 -124
  92. package/dist/src/indexing/jobs.js +156 -47
  93. package/dist/src/indexing/prune-run.d.ts +2 -0
  94. package/dist/src/indexing/prune-run.js +69 -26
  95. package/dist/src/indexing/prune.d.ts +12 -3
  96. package/dist/src/indexing/prune.js +85 -35
  97. package/dist/src/indexing/runner.d.ts +2 -1
  98. package/dist/src/indexing/runner.js +83 -40
  99. package/dist/src/indexing/upgrade.d.ts +9 -0
  100. package/dist/src/indexing/upgrade.js +79 -0
  101. package/dist/src/inference/asr/bridge.js +6 -2
  102. package/dist/src/inference/asr/protocol.js +109 -19
  103. package/dist/src/inference/asr/runtime.js +58 -21
  104. package/dist/src/inference/asr/windows.js +9 -5
  105. package/dist/src/inference/embedding/index.d.ts +2 -6
  106. package/dist/src/inference/embedding/index.js +89 -29
  107. package/dist/src/ingest/attempt.d.ts +29 -0
  108. package/dist/src/ingest/attempt.js +55 -0
  109. package/dist/src/media/capture-helper.js +7 -2
  110. package/dist/src/media/capture.d.ts +2 -1
  111. package/dist/src/media/capture.js +124 -43
  112. package/dist/src/media/handler.d.ts +4 -2
  113. package/dist/src/media/handler.js +88 -70
  114. package/dist/src/media/transcribe.js +63 -21
  115. package/dist/src/metadata/article.js +49 -17
  116. package/dist/src/models/artifacts.d.ts +10 -8
  117. package/dist/src/models/artifacts.js +237 -106
  118. package/dist/src/models/catalog.d.ts +2 -5
  119. package/dist/src/models/catalog.js +94 -25
  120. package/dist/src/models/download.js +55 -15
  121. package/dist/src/models/runtime.d.ts +0 -2
  122. package/dist/src/models/runtime.js +69 -33
  123. package/dist/src/pdf/capture-helper.js +3 -1
  124. package/dist/src/pdf/capture.d.ts +2 -1
  125. package/dist/src/pdf/capture.js +95 -27
  126. package/dist/src/pdf/errors.js +12 -1
  127. package/dist/src/pdf/handler.d.ts +2 -1
  128. package/dist/src/pdf/handler.js +70 -71
  129. package/dist/src/pdf/index.d.ts +1 -3
  130. package/dist/src/pdf/index.js +0 -2
  131. package/dist/src/pdf/input.js +12 -7
  132. package/dist/src/pdf/normalize.js +80 -21
  133. package/dist/src/pdf/source-identity.js +3 -2
  134. package/dist/src/pdf/source.js +12 -8
  135. package/dist/src/pdf/verify-helper.js +15 -3
  136. package/dist/src/portability/backup.d.ts +2 -9
  137. package/dist/src/portability/backup.js +108 -22
  138. package/dist/src/portability/database.js +51 -17
  139. package/dist/src/portability/export.js +35 -9
  140. package/dist/src/portability/files.d.ts +1 -4
  141. package/dist/src/portability/files.js +127 -67
  142. package/dist/src/portability/gc.js +65 -22
  143. package/dist/src/publish/component.js +1 -1
  144. package/dist/src/publish/feed.d.ts +2 -2
  145. package/dist/src/publish/feed.js +75 -29
  146. package/dist/src/publish/identity.d.ts +5 -1
  147. package/dist/src/publish/identity.js +31 -11
  148. package/dist/src/publish/index.d.ts +2 -3
  149. package/dist/src/publish/index.js +34 -21
  150. package/dist/src/publish/projection.d.ts +2 -9
  151. package/dist/src/publish/projection.js +21 -14
  152. package/dist/src/search/extension.js +3 -1
  153. package/dist/src/search/hybrid.d.ts +3 -8
  154. package/dist/src/search/hybrid.js +45 -11
  155. package/dist/src/search/lexical.js +18 -4
  156. package/dist/src/search/query.js +56 -25
  157. package/dist/src/search/vector.js +37 -10
  158. package/dist/src/service/launch-agent.d.ts +5 -3
  159. package/dist/src/service/launch-agent.js +272 -83
  160. package/dist/src/service/lifetime.js +47 -14
  161. package/dist/src/service/loop.d.ts +3 -3
  162. package/dist/src/service/loop.js +47 -19
  163. package/dist/src/store/annotations.d.ts +32 -0
  164. package/dist/src/store/annotations.js +44 -0
  165. package/dist/src/store/backups.d.ts +18 -0
  166. package/dist/src/store/backups.js +110 -0
  167. package/dist/src/store/blobs.d.ts +24 -0
  168. package/dist/src/store/blobs.js +139 -0
  169. package/dist/src/store/changes.d.ts +5 -12
  170. package/dist/src/store/changes.js +108 -56
  171. package/dist/src/store/context.d.ts +15 -0
  172. package/dist/src/store/context.js +19 -0
  173. package/dist/src/store/derivations.d.ts +42 -0
  174. package/dist/src/store/derivations.js +104 -0
  175. package/dist/src/store/files.d.ts +0 -2
  176. package/dist/src/store/files.js +19 -24
  177. package/dist/src/store/ids.d.ts +30 -0
  178. package/dist/src/store/ids.js +34 -0
  179. package/dist/src/store/imports.d.ts +26 -0
  180. package/dist/src/store/imports.js +57 -0
  181. package/dist/src/store/index.d.ts +3 -186
  182. package/dist/src/store/index.js +2 -809
  183. package/dist/src/store/lease.d.ts +20 -0
  184. package/dist/src/store/lease.js +62 -0
  185. package/dist/src/store/library.d.ts +75 -0
  186. package/dist/src/store/library.js +174 -0
  187. package/dist/src/store/open.d.ts +7 -0
  188. package/dist/src/store/open.js +183 -0
  189. package/dist/src/store/queue.d.ts +65 -0
  190. package/dist/src/store/queue.js +327 -0
  191. package/dist/src/store/revisions.d.ts +12 -0
  192. package/dist/src/store/revisions.js +189 -0
  193. package/dist/src/store/search-index.d.ts +63 -0
  194. package/dist/src/store/search-index.js +190 -0
  195. package/dist/src/store/sync-ready.d.ts +6 -0
  196. package/dist/src/store/sync-ready.js +121 -0
  197. package/dist/src/store/types.d.ts +1 -5
  198. package/dist/src/store/types.js +1 -8
  199. package/dist/src/util/canonical.d.ts +2 -0
  200. package/dist/src/util/canonical.js +17 -0
  201. package/dist/src/util/exact.d.ts +2 -0
  202. package/dist/src/util/exact.js +9 -0
  203. package/dist/src/util/fsync.d.ts +4 -0
  204. package/dist/src/util/fsync.js +22 -0
  205. package/dist/src/util/hash.d.ts +4 -0
  206. package/dist/src/util/hash.js +8 -0
  207. package/dist/src/worker/basic.d.ts +0 -1
  208. package/dist/src/worker/basic.js +23 -7
  209. package/dist/src/worker/custody.d.ts +4 -1
  210. package/dist/src/worker/custody.js +111 -38
  211. package/dist/src/worker/engine.d.ts +2 -1
  212. package/dist/src/worker/engine.js +42 -20
  213. package/dist/src/worker/identity.js +13 -5
  214. package/dist/src/worker/journal.d.ts +1 -1
  215. package/dist/src/worker/journal.js +54 -24
  216. package/dist/src/worker/lock.d.ts +18 -0
  217. package/dist/src/worker/lock.js +196 -0
  218. package/dist/src/worker/runtime-gc.d.ts +4 -3
  219. package/dist/src/worker/runtime-gc.js +68 -41
  220. package/dist/src/worker/supervisor-child.js +19 -6
  221. package/dist/src/worker/supervisor.js +41 -8
  222. package/native/runtime-manifest.json +4 -4
  223. package/package.json +10 -2
  224. package/dist/src/digest/store.d.ts +0 -57
  225. package/dist/src/digest/store.js +0 -77
  226. package/dist/src/store/foundation.d.ts +0 -7
  227. package/dist/src/store/foundation.js +0 -7
@@ -1,6 +1,6 @@
1
1
  import type { DatabaseSync } from 'node:sqlite';
2
- import { type SearchGeneration, type VectorHit } from './vector.js';
3
- export interface SearchHit {
2
+ import { type SearchGeneration } from './vector.js';
3
+ interface SearchHit {
4
4
  itemId: string;
5
5
  revisionId: string;
6
6
  title: string;
@@ -24,12 +24,6 @@ export type QueryEmbedder = (query: string, pin: SearchGeneration, signal: Abort
24
24
  fingerprint: string;
25
25
  vector: readonly number[];
26
26
  }>;
27
- /** Reciprocal rank fusion after per-channel item collapsing; chunks cannot amplify rank. */
28
- export declare function fuse(lexicalHits: {
29
- itemId: string;
30
- revisionId: string;
31
- title: string;
32
- }[], semanticHits: VectorHit[], limit: number): SearchHit[];
33
27
  export declare function hybridSearch(db: DatabaseSync, query: string, options?: {
34
28
  profileId?: string;
35
29
  limit?: number;
@@ -37,3 +31,4 @@ export declare function hybridSearch(db: DatabaseSync, query: string, options?:
37
31
  embed?: QueryEmbedder;
38
32
  signal?: AbortSignal;
39
33
  }): Promise<SearchResult>;
34
+ export {};
@@ -1,10 +1,14 @@
1
1
  import { chunkDocument } from '../indexing/index.js';
2
2
  import { lexicalSearch } from './lexical.js';
3
3
  import { pinSearchGeneration, vectorSearch } from './vector.js';
4
+ import { SpongeError } from '../errors.js';
4
5
  /** Reciprocal rank fusion after per-channel item collapsing; chunks cannot amplify rank. */
5
- export function fuse(lexicalHits, semanticHits, limit) {
6
+ function fuse(lexicalHits, semanticHits, limit) {
6
7
  const hits = new Map();
7
- for (const [method, rows] of [['lexical', lexicalHits], ['semantic', semanticHits]]) {
8
+ for (const [method, rows] of [
9
+ ['lexical', lexicalHits],
10
+ ['semantic', semanticHits],
11
+ ]) {
8
12
  const seen = new Set();
9
13
  let rank = 0;
10
14
  for (const row of rows) {
@@ -13,7 +17,14 @@ export function fuse(lexicalHits, semanticHits, limit) {
13
17
  seen.add(row.itemId);
14
18
  rank++;
15
19
  const previous = hits.get(row.itemId), vector = method === 'semantic' ? row : undefined;
16
- const anchor = vector ? { kind: vector.source.kind === 'image' ? 'image' : 'document', representationId: vector.source.representationId, chunk: vector.source.ordinal, assetSha256: vector.source.assetSha256 } : { kind: 'document' };
20
+ const anchor = vector
21
+ ? {
22
+ kind: vector.source.kind === 'image' ? 'image' : 'document',
23
+ representationId: vector.source.representationId,
24
+ chunk: vector.source.ordinal,
25
+ assetSha256: vector.source.assetSha256,
26
+ }
27
+ : { kind: 'document' };
17
28
  if (previous) {
18
29
  previous.score += 1 / (60 + rank);
19
30
  previous.methods.push(method);
@@ -21,7 +32,14 @@ export function fuse(lexicalHits, semanticHits, limit) {
21
32
  previous.anchor = anchor;
22
33
  }
23
34
  else
24
- hits.set(row.itemId, { itemId: row.itemId, revisionId: row.revisionId, title: row.title, score: 1 / (60 + rank), methods: [method], anchor });
35
+ hits.set(row.itemId, {
36
+ itemId: row.itemId,
37
+ revisionId: row.revisionId,
38
+ title: row.title,
39
+ score: 1 / (60 + rank),
40
+ methods: [method],
41
+ anchor,
42
+ });
25
43
  }
26
44
  }
27
45
  return [...hits.values()].sort((a, b) => b.score - a.score || a.itemId.localeCompare(b.itemId)).slice(0, limit);
@@ -31,7 +49,9 @@ function projectAnchors(db, hits) {
31
49
  for (const hit of hits) {
32
50
  if (hit.anchor.kind !== 'document' || !hit.anchor.representationId || hit.anchor.chunk === undefined)
33
51
  continue;
34
- const row = db.prepare('SELECT p.markdown,i.kind,r.extractor_name AS extractor FROM representations p JOIN revisions r ON r.id=p.revision_id JOIN items i ON i.id=r.item_id WHERE p.id=? AND p.revision_id=?').get(hit.anchor.representationId, hit.revisionId);
52
+ const row = db
53
+ .prepare('SELECT p.markdown,i.kind,r.extractor_name AS extractor FROM representations p JOIN revisions r ON r.id=p.revision_id JOIN items i ON i.id=r.item_id WHERE p.id=? AND p.revision_id=?')
54
+ .get(hit.anchor.representationId, hit.revisionId);
35
55
  if (!row || typeof row.markdown !== 'string')
36
56
  continue;
37
57
  const chunks = chunkDocument(row.markdown), chunk = chunks[hit.anchor.chunk];
@@ -51,13 +71,13 @@ function projectAnchors(db, hits) {
51
71
  const end = start + chunk.length;
52
72
  if (row.kind === 'pdf') {
53
73
  const markers = [...row.markdown.matchAll(/^## Page ([1-9][0-9]*)$/gm)];
54
- const marker = markers.filter(m => m.index <= start).at(-1) ?? markers.find(m => m.index < end);
74
+ const marker = markers.filter((m) => m.index <= start).at(-1) ?? markers.find((m) => m.index < end);
55
75
  if (marker)
56
76
  hit.anchor.page = Number(marker[1]);
57
77
  }
58
78
  else if (row.extractor === 'sponge-media') {
59
79
  const markers = [...row.markdown.matchAll(/^\[(\d+):(\d{2}):(\d{2})\] /gm)];
60
- const marker = markers.filter(m => m.index <= start).at(-1) ?? markers.find(m => m.index < end);
80
+ const marker = markers.filter((m) => m.index <= start).at(-1) ?? markers.find((m) => m.index < end);
61
81
  if (marker)
62
82
  hit.anchor.startMs = (Number(marker[1]) * 3600 + Number(marker[2]) * 60 + Number(marker[3])) * 1000;
63
83
  }
@@ -65,8 +85,15 @@ function projectAnchors(db, hits) {
65
85
  }
66
86
  export async function hybridSearch(db, query, options = {}) {
67
87
  const limit = options.limit ?? 20, timeout = options.semanticTimeoutMs ?? 10000;
68
- if (!query.trim() || Buffer.byteLength(query) > 4096 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(timeout) || timeout < 1 || timeout > 120000)
69
- throw Error('invalid-search-query');
88
+ if (!query.trim() ||
89
+ Buffer.byteLength(query) > 4096 ||
90
+ !Number.isSafeInteger(limit) ||
91
+ limit < 1 ||
92
+ limit > 100 ||
93
+ !Number.isSafeInteger(timeout) ||
94
+ timeout < 1 ||
95
+ timeout > 120000)
96
+ throw new SpongeError('invalid-search-query');
70
97
  const pin = pinSearchGeneration(db, options.profileId ?? 'default'), warnings = [];
71
98
  let encoded;
72
99
  if (pin && options.embed) {
@@ -75,7 +102,10 @@ export async function hybridSearch(db, query, options = {}) {
75
102
  if (options.signal?.aborted)
76
103
  abort();
77
104
  let timedOut = false;
78
- const timer = setTimeout(() => { timedOut = true; controller.abort(); }, timeout);
105
+ const timer = setTimeout(() => {
106
+ timedOut = true;
107
+ controller.abort();
108
+ }, timeout);
79
109
  try {
80
110
  const result = await options.embed(query, pin, controller.signal);
81
111
  if (timedOut)
@@ -106,7 +136,11 @@ export async function hybridSearch(db, query, options = {}) {
106
136
  warnings.push('semantic-unavailable');
107
137
  }
108
138
  }
109
- const result = { hits: fuse(lexicalSearch(db, query, Math.min(500, limit * 5)), semantic, limit), warnings, generation: pin };
139
+ const result = {
140
+ hits: fuse(lexicalSearch(db, query, Math.min(500, limit * 5)), semantic, limit),
141
+ warnings,
142
+ generation: pin,
143
+ };
110
144
  projectAnchors(db, result.hits);
111
145
  db.exec('COMMIT');
112
146
  return result;
@@ -1,6 +1,20 @@
1
1
  /** Contentless FTS rows are keyed by readable_fts_rows.id; items.rowid is not VACUUM-stable. Callers own the transaction. */
2
- export function indexReadable(db, itemId, revisionId, title, markdown) { unindexReadable(db, itemId); const row = db.prepare('INSERT INTO readable_fts_rows(item_id,revision_id) VALUES(?,?) RETURNING id').get(itemId, revisionId); db.prepare('INSERT INTO readable_fts(rowid,title,markdown) VALUES(?,?,?)').run(row.id, title, markdown); }
3
- export function unindexReadable(db, itemId) { const row = db.prepare('DELETE FROM readable_fts_rows WHERE item_id=? RETURNING id').get(itemId); if (row)
4
- db.prepare('DELETE FROM readable_fts WHERE rowid=?').run(row.id); }
2
+ export function indexReadable(db, itemId, revisionId, title, markdown) {
3
+ unindexReadable(db, itemId);
4
+ const row = db
5
+ .prepare('INSERT INTO readable_fts_rows(item_id,revision_id) VALUES(?,?) RETURNING id')
6
+ .get(itemId, revisionId);
7
+ db.prepare('INSERT INTO readable_fts(rowid,title,markdown) VALUES(?,?,?)').run(row.id, title, markdown);
8
+ }
9
+ export function unindexReadable(db, itemId) {
10
+ const row = db.prepare('DELETE FROM readable_fts_rows WHERE item_id=? RETURNING id').get(itemId);
11
+ if (row)
12
+ db.prepare('DELETE FROM readable_fts WHERE rowid=?').run(row.id);
13
+ }
5
14
  /** Literal phrase, bm25 rank then item id; excludes tombstones and superseded revisions. */
6
- export function lexicalSearch(db, query, limit) { const literal = '"' + query.replaceAll('"', '""') + '"'; return db.prepare('SELECT i.id AS itemId,i.title,i.current_revision AS revisionId FROM readable_fts f JOIN readable_fts_rows m ON m.id=f.rowid JOIN items i ON i.id=m.item_id WHERE readable_fts MATCH ? AND i.deleted_at IS NULL AND i.current_revision=m.revision_id ORDER BY rank,i.id LIMIT ?').all(literal, limit); }
15
+ export function lexicalSearch(db, query, limit) {
16
+ const literal = '"' + query.replaceAll('"', '""') + '"';
17
+ return db
18
+ .prepare('SELECT i.id AS itemId,i.title,i.current_revision AS revisionId FROM readable_fts f JOIN readable_fts_rows m ON m.id=f.rowid JOIN items i ON i.id=m.item_id WHERE readable_fts MATCH ? AND i.deleted_at IS NULL AND i.current_revision=m.revision_id ORDER BY rank,i.id LIMIT ?')
19
+ .all(literal, limit);
20
+ }
@@ -2,39 +2,62 @@ import { randomUUID } from 'node:crypto';
2
2
  import { join } from 'node:path';
3
3
  import { rm } from 'node:fs/promises';
4
4
  import { acquireWorkerCustody, proveAttemptCleanup } from '../worker/custody.js';
5
- import { directory, writeJournal, readJournal, syncDir } from '../worker/journal.js';
5
+ import { directory, writeJournal, readJournal } from '../worker/journal.js';
6
+ import { syncDirectoryAsync } from '../util/fsync.js';
6
7
  import { supervisedRunner } from '../worker/supervisor.js';
7
- import { embedBatch, embeddingFingerprint, embeddingTextBytes, resolveIndexSpace, projectVector, encodeEmbeddingRequest, EmbeddingError } from '../inference/embedding/index.js';
8
+ import { embedBatch, embeddingFingerprint, embeddingTextBytes, resolveIndexSpace, projectVector, encodeEmbeddingRequest, } from '../inference/embedding/index.js';
9
+ import { SpongeError, codeOf } from '../errors.js';
8
10
  /** One custody-held helper run. Mutates coordination journals only, never library content or job tables. */
9
11
  async function custodyEmbed(store, runtime, inputs, signal, options) {
10
12
  const custody = await acquireWorkerCustody(store.root), attemptId = randomUUID();
11
13
  let scratchRoot, cleanupUnknown = false;
12
14
  try {
13
15
  const attempts = await directory(join(custody.directory, 'attempts', attemptId));
14
- await writeJournal(join(attempts, 'query.json'), { schemaVersion: 1, kind: 'query', attemptId, ownerIncarnation: custody.incarnation });
16
+ await writeJournal(join(attempts, 'query.json'), {
17
+ schemaVersion: 1,
18
+ kind: 'query',
19
+ attemptId,
20
+ ownerIncarnation: custody.incarnation,
21
+ });
15
22
  scratchRoot = await directory(join(store.root, 'staging', 'query-' + attemptId));
16
- await writeJournal(join(scratchRoot, 'ownership.json'), { schemaVersion: 1, kind: 'query', requestId: attemptId, ownerIncarnation: custody.incarnation });
23
+ await writeJournal(join(scratchRoot, 'ownership.json'), {
24
+ schemaVersion: 1,
25
+ kind: 'query',
26
+ requestId: attemptId,
27
+ ownerIncarnation: custody.incarnation,
28
+ });
17
29
  await options.onReady?.({ attemptId, ownerDirectory: custody.directory, scratchRoot });
18
30
  const expected = encodeEmbeddingRequest(inputs).inputHashes;
19
- const result = await (options.embed ?? embedBatch)(inputs, { runtime, scratchRoot, signal, runProcess: supervisedRunner(custody.directory, custody.incarnation, attemptId) });
20
- if (result.fingerprint !== embeddingFingerprint(runtime.helperSha256) || result.inputHashes.length !== expected.length || result.inputHashes.some((h, i) => h !== expected[i]) || result.vectors.length !== expected.length)
21
- throw new EmbeddingError('integrity');
31
+ const result = await (options.embed ?? embedBatch)(inputs, {
32
+ runtime,
33
+ scratchRoot,
34
+ signal,
35
+ runProcess: supervisedRunner(custody.directory, custody.incarnation, attemptId),
36
+ });
37
+ if (result.fingerprint !== embeddingFingerprint(runtime.helperSha256) ||
38
+ result.inputHashes.length !== expected.length ||
39
+ result.inputHashes.some((h, i) => h !== expected[i]) ||
40
+ result.vectors.length !== expected.length)
41
+ throw new SpongeError('integrity');
22
42
  return result.vectors;
23
43
  }
24
44
  catch (error) {
25
- cleanupUnknown = error.code === 'cleanup-required';
45
+ cleanupUnknown = codeOf(error) === 'cleanup-required';
26
46
  throw error;
27
47
  }
28
48
  finally {
29
- const proved = !cleanupUnknown && await proveAttemptCleanup(custody.directory, custody.incarnation, attemptId);
49
+ const proved = !cleanupUnknown && (await proveAttemptCleanup(custody.directory, custody.incarnation, attemptId));
30
50
  if (!proved)
31
- throw new EmbeddingError('cleanup-required');
51
+ throw new SpongeError('cleanup-required');
32
52
  if (scratchRoot) {
33
53
  const marker = await readJournal(join(scratchRoot, 'ownership.json'));
34
- if (marker.schemaVersion !== 1 || marker.kind !== 'query' || marker.requestId !== attemptId || marker.ownerIncarnation !== custody.incarnation)
35
- throw new EmbeddingError('cleanup-required');
54
+ if (marker.schemaVersion !== 1 ||
55
+ marker.kind !== 'query' ||
56
+ marker.requestId !== attemptId ||
57
+ marker.ownerIncarnation !== custody.incarnation)
58
+ throw new SpongeError('cleanup-required');
36
59
  await rm(scratchRoot, { recursive: true });
37
- await syncDir(join(store.root, 'staging'));
60
+ await syncDirectoryAsync(join(store.root, 'staging'));
38
61
  }
39
62
  await custody.release();
40
63
  }
@@ -43,24 +66,32 @@ async function custodyEmbed(store, runtime, inputs, signal, options) {
43
66
  export function createQueryEmbedder(store, runtime, options = {}) {
44
67
  return async (query, pin, signal) => {
45
68
  if (signal.aborted)
46
- throw new EmbeddingError('cancelled');
69
+ throw new SpongeError('cancelled');
47
70
  const space = resolveIndexSpace(runtime.helperSha256, pin.fingerprint);
48
71
  if (!space || space.dimensions !== pin.dimensions)
49
- throw new EmbeddingError('integrity');
72
+ throw new SpongeError('integrity');
50
73
  const [vector] = await custodyEmbed(store, runtime, [{ kind: 'query', text: query }], signal, options);
51
74
  return { fingerprint: pin.fingerprint, vector: projectVector(vector, space.dimensions) };
52
75
  };
53
76
  }
54
77
  /** Unit 768-dimension document vectors for short texts, 16 per helper run; text is clipped to the helper's byte budget on a character boundary. */
55
78
  export function createTextEmbedder(store, runtime, options = {}) {
56
- const room = embeddingTextBytes - Buffer.byteLength('title: none | text: '), clip = (t) => { let out = ''; for (const ch of t) {
57
- if (Buffer.byteLength(out + ch) > room)
58
- break;
59
- out += ch;
60
- } return out; };
61
- return async (texts, signal) => { const out = []; for (let i = 0; i < texts.length; i += 16) {
62
- if (signal.aborted)
63
- throw new EmbeddingError('cancelled');
64
- out.push(...await custodyEmbed(store, runtime, texts.slice(i, i + 16).map(t => ({ kind: 'document', text: clip(t) })), signal, options));
65
- } return out; };
79
+ const room = embeddingTextBytes - Buffer.byteLength('title: none | text: '), clip = (t) => {
80
+ let out = '';
81
+ for (const ch of t) {
82
+ if (Buffer.byteLength(out + ch) > room)
83
+ break;
84
+ out += ch;
85
+ }
86
+ return out;
87
+ };
88
+ return async (texts, signal) => {
89
+ const out = [];
90
+ for (let i = 0; i < texts.length; i += 16) {
91
+ if (signal.aborted)
92
+ throw new SpongeError('cancelled');
93
+ out.push(...(await custodyEmbed(store, runtime, texts.slice(i, i + 16).map((t) => ({ kind: 'document', text: clip(t) })), signal, options)));
94
+ }
95
+ return out;
96
+ };
66
97
  }
@@ -1,22 +1,49 @@
1
- import { encodeVector, spaceEncoding } from '../indexing/index.js';
1
+ import { encodeVector, sourceInput, spaceEncoding } from '../indexing/index.js';
2
+ import { SpongeError } from '../errors.js';
2
3
  export function pinSearchGeneration(db, profileId) {
3
- const row = db.prepare('SELECT g.id,g.fingerprint,s.dimensions FROM active_indexes a JOIN index_generations g ON g.id=a.generation_id JOIN embedding_spaces s ON s.fingerprint=g.fingerprint WHERE a.profile_id=?').get(profileId);
4
+ const row = db
5
+ .prepare('SELECT g.id,g.fingerprint,s.dimensions FROM active_indexes a JOIN index_generations g ON g.id=a.generation_id JOIN embedding_spaces s ON s.fingerprint=g.fingerprint WHERE a.profile_id=?')
6
+ .get(profileId);
4
7
  if (!row)
5
8
  return undefined;
6
9
  return { id: String(row.id), fingerprint: String(row.fingerprint), dimensions: Number(row.dimensions), profileId };
7
10
  }
8
11
  /** Native scalar scan keeps vectors inside SQLite; SQL bounds materialized result rows. */
9
12
  export function vectorSearch(db, pin, query, limit = 50) {
10
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500 || query.fingerprint !== pin.fingerprint || query.vector.length !== pin.dimensions || query.vector.some(n => !Number.isFinite(n)) || Math.abs(Math.hypot(...query.vector) - 1) > .001)
11
- throw Error('invalid-search-vector');
13
+ if (!Number.isSafeInteger(limit) ||
14
+ limit < 1 ||
15
+ limit > 500 ||
16
+ query.fingerprint !== pin.fingerprint ||
17
+ query.vector.length !== pin.dimensions ||
18
+ query.vector.some((n) => !Number.isFinite(n)) ||
19
+ Math.abs(Math.hypot(...query.vector) - 1) > 0.001)
20
+ throw new SpongeError('invalid-search-vector');
12
21
  if (db.prepare('SELECT vec_version() AS version').get()?.version !== 'v0.1.9')
13
- throw Error('unsupported-vector-runtime');
14
- const generation = db.prepare('SELECT g.fingerprint,s.dimensions,s.identity_json FROM index_generations g JOIN embedding_spaces s ON s.fingerprint=g.fingerprint WHERE g.id=?').get(pin.id);
22
+ throw new SpongeError('unsupported-vector-runtime');
23
+ const generation = db
24
+ .prepare('SELECT g.fingerprint,s.dimensions,s.identity_json FROM index_generations g JOIN embedding_spaces s ON s.fingerprint=g.fingerprint WHERE g.id=?')
25
+ .get(pin.id);
15
26
  if (!generation || generation.fingerprint !== pin.fingerprint || Number(generation.dimensions) !== pin.dimensions)
16
- throw Error('search-generation-missing');
27
+ throw new SpongeError('search-generation-missing');
17
28
  // int8 spaces quantize the query the same way; cosine over codes is the cosine of the dequantized vectors because per-vector scales cancel.
18
29
  const encoding = spaceEncoding(JSON.parse(String(generation.identity_json))), bytes = encodeVector(query.vector, encoding), distance = encoding === 'int8' ? 'vec_distance_cosine(vec_int8(v.vector),vec_int8(?))' : 'vec_distance_cosine(v.vector,?)';
19
- const rows = db.prepare('SELECT v.source_id,v.revision_id,v.derivation_hash,v.asset_sha256,i.id AS item_id,i.title,' + distance + ' AS distance FROM vectors v JOIN revisions r ON r.id=v.revision_id JOIN items i ON i.id=r.item_id WHERE v.generation_id=? AND i.deleted_at IS NULL AND i.current_revision=v.revision_id ORDER BY distance,v.source_id LIMIT ?').all(bytes, pin.id, limit);
20
- return rows.map(row => { const descriptor = JSON.parse(String(row.source_id)), distance = Number(row.distance); if (!Array.isArray(descriptor) || descriptor[0] !== row.revision_id || !['document', 'image'].includes(String(descriptor[1])) || typeof descriptor[2] !== 'string' || !Number.isFinite(distance))
21
- throw Error('invalid-search-index'); const source = { sourceId: String(row.source_id), itemId: String(row.item_id), revisionId: String(row.revision_id), derivationHash: String(row.derivation_hash), kind: descriptor[1], ...(descriptor[1] === 'image' ? { assetSha256: String(row.asset_sha256) } : { representationId: descriptor[2], ordinal: Number(descriptor[3]) }) }; return { itemId: String(row.item_id), revisionId: String(row.revision_id), title: String(row.title), source, distance }; });
30
+ // Coverage rows name content; the space stores one vector per content hash, shared by every item and generation that has it.
31
+ const rows = db
32
+ .prepare('SELECT c.source_id,c.revision_id,c.content_hash,i.id AS item_id,i.title,' +
33
+ distance +
34
+ ' AS distance FROM index_inputs c JOIN embeddings v ON v.fingerprint=? AND v.content_hash=c.content_hash JOIN revisions r ON r.id=c.revision_id JOIN items i ON i.id=r.item_id WHERE c.generation_id=? AND i.deleted_at IS NULL AND i.current_revision=c.revision_id ORDER BY distance,c.source_id LIMIT ?')
35
+ .all(bytes, pin.fingerprint, pin.id, limit);
36
+ return rows.map((row) => {
37
+ const distance = Number(row.distance);
38
+ let source;
39
+ try {
40
+ source = sourceInput(row);
41
+ }
42
+ catch {
43
+ throw new SpongeError('invalid-search-index');
44
+ }
45
+ if (!Number.isFinite(distance))
46
+ throw new SpongeError('invalid-search-index');
47
+ return { itemId: source.itemId, revisionId: source.revisionId, title: String(row.title), source, distance };
48
+ });
22
49
  }
@@ -1,6 +1,6 @@
1
1
  import type { LibraryStore } from '../store/index.js';
2
2
  import { type ProcessIdentity } from '../worker/identity.js';
3
- export interface LaunchResult {
3
+ interface LaunchResult {
4
4
  code: number;
5
5
  stdout: string;
6
6
  }
@@ -22,7 +22,7 @@ interface Pin {
22
22
  sha256: string;
23
23
  }
24
24
  interface Installation {
25
- language: "en" | null;
25
+ language: 'en' | null;
26
26
  schemaVersion: 1;
27
27
  id: string;
28
28
  label: string;
@@ -37,7 +37,9 @@ interface Installation {
37
37
  launcherHash: string;
38
38
  pins: Pin[];
39
39
  }
40
- export declare function agentPlist(record: Pick<Installation, 'label' | 'node' | 'launcher'>): string;
40
+ export declare function agentPlist(record: Pick<Installation, 'label' | 'node' | 'launcher'> & {
41
+ log: string;
42
+ }): string;
41
43
  export declare function serviceStatus(store: LibraryStore, env: ServiceEnvironment): Promise<Record<string, unknown>>;
42
44
  export declare function installService(store: LibraryStore, env: ServiceEnvironment, options?: {
43
45
  language?: 'en';