@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,19 +1,27 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { StoreError } from './types.js';
3
- import { digest } from './files.js';
2
+ import { sha256 } from '../util/hash.js';
3
+ import { kindOf } from '../derivations/kinds.js';
4
+ import { SpongeError } from '../errors.js';
5
+ import { indexReadable } from '../search/lexical.js';
6
+ import { isFileLocator } from './ids.js';
4
7
  const now = () => new Date().toISOString();
5
- const fail = (code = 'invalid-input') => { throw new StoreError(code); };
8
+ const fail = (code = 'invalid-input') => {
9
+ throw new SpongeError(code);
10
+ };
6
11
  /** Bound on one change's JSON; imported note bodies (64 KiB) plus anchors fit with room for escaping. */
7
- export const maxChangeBytes = 262144;
12
+ const maxChangeBytes = 262144;
8
13
  /** Hybrid logical clock `ms.counter.device`: zero-padded so every device sorts the strings alike. */
9
14
  function tick(db) {
10
15
  const row = db.prepare('SELECT device_id,clock_ms,clock_counter FROM device WHERE id=1').get() ?? fail('integrity');
11
16
  const wall = Date.now(), prior = Number(row.clock_ms), ms = Math.max(wall, prior), counter = ms === prior ? Number(row.clock_counter) + 1 : 0;
12
17
  db.prepare('UPDATE device SET clock_ms=?,clock_counter=? WHERE id=1').run(ms, counter);
13
- return { deviceId: String(row.device_id), hlc: `${String(ms).padStart(15, '0')}.${String(counter).padStart(9, '0')}.${row.device_id}` };
18
+ return {
19
+ deviceId: String(row.device_id),
20
+ hlc: `${String(ms).padStart(15, '0')}.${String(counter).padStart(9, '0')}.${row.device_id}`,
21
+ };
14
22
  }
15
23
  /** Append without applying: for state that already exists (seeding). Callers hold the write transaction. */
16
- export function append(db, change) {
24
+ function append(db, change) {
17
25
  const { deviceId, hlc } = tick(db);
18
26
  db.prepare('INSERT INTO changes(id,device_id,hlc,entity,entity_id,field,value_json,created_at) VALUES(?,?,?,?,?,?,?,?)').run(randomUUID(), deviceId, hlc, change.entity, change.entityId, change.field, JSON.stringify(change.value ?? null), now());
19
27
  }
@@ -28,54 +36,38 @@ export function mutate(db, entity, entityId, field, value) {
28
36
  export function listChanges(db, after = 0, limit = 500) {
29
37
  if (!Number.isSafeInteger(after) || after < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
30
38
  fail();
31
- return db.prepare('SELECT * FROM changes WHERE seq>? ORDER BY seq LIMIT ?').all(after, limit).map(r => ({ seq: Number(r.seq), id: String(r.id), deviceId: String(r.device_id), hlc: String(r.hlc), entity: r.entity, entityId: String(r.entity_id), field: String(r.field), value: JSON.parse(String(r.value_json)) }));
39
+ return db
40
+ .prepare('SELECT * FROM changes WHERE seq>? ORDER BY seq LIMIT ?')
41
+ .all(after, limit)
42
+ .map((r) => ({
43
+ seq: Number(r.seq),
44
+ id: String(r.id),
45
+ deviceId: String(r.device_id),
46
+ hlc: String(r.hlc),
47
+ entity: r.entity,
48
+ entityId: String(r.entity_id),
49
+ field: String(r.field),
50
+ value: JSON.parse(String(r.value_json)),
51
+ }));
32
52
  }
33
53
  const object = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
34
54
  const str = (v, max = 65536) => typeof v === 'string' && v.length > 0 && Buffer.byteLength(v) <= max;
35
55
  const optional = (v, max = 256) => v === null || str(v, max);
36
56
  const status = (v) => v === null || v === 'accepted' || v === 'rejected';
37
57
  /** Deterministic source-ref id, so replayed item creation yields the same row on every device. */
38
- export const sourceRefId = (itemId) => digest(JSON.stringify(['source-ref-v1', itemId]));
39
- const parsed = (json) => { try {
40
- return JSON.parse(String(json));
41
- }
42
- catch {
43
- return String(json);
44
- } };
45
- const changed = (r) => { if (!Number(r.changes))
46
- fail('unavailable'); };
47
- /**
48
- * Review JSON in one canonical shape, whatever order its paths were written in. A digest is reviewed per field;
49
- * an editorial derivation (a reviewed source note kept whole) has one path, `review.note`.
50
- */
51
- export function setReviewPath(review, output, path, value, kind = 'digest') {
52
- if (kind === 'editorial') {
53
- if (path !== 'review.note')
54
- fail();
55
- return value ? { note: value } : {};
58
+ export const sourceRefId = (itemId) => sha256(JSON.stringify(['source-ref-v1', itemId]));
59
+ const parsed = (json) => {
60
+ try {
61
+ return JSON.parse(String(json));
56
62
  }
57
- if (kind !== 'digest')
58
- fail();
59
- const ideas = output.ideas.map((_, i) => review.ideas?.[i] ?? null);
60
- const quotes = output.ideas.map((idea, i) => idea.quotes.map((_, j) => review.quotes?.[i]?.[j] ?? null));
61
- let gist = review.gist ?? null;
62
- const m = /^review\.(?:(gist)|ideas\.(\d{1,2})|quotes\.(\d{1,2})\.(\d{1,2}))$/.exec(path) ?? fail();
63
- if (m[1])
64
- gist = value;
65
- else if (m[2] !== undefined) {
66
- const i = Number(m[2]);
67
- if (i >= ideas.length)
68
- fail();
69
- ideas[i] = value;
63
+ catch {
64
+ return String(json);
70
65
  }
71
- else {
72
- const i = Number(m[3]), j = Number(m[4]);
73
- if (i >= quotes.length || j >= quotes[i].length)
74
- fail();
75
- quotes[i][j] = value;
76
- }
77
- return { ...(gist ? { gist } : {}), ...(ideas.some(v => v) ? { ideas } : {}), ...(quotes.some(q => q.some(v => v)) ? { quotes } : {}) };
78
- }
66
+ };
67
+ const changed = (r) => {
68
+ if (!Number(r.changes))
69
+ fail('unavailable');
70
+ };
79
71
  /**
80
72
  * Map one change onto its row. Table-driven: every user-state field Sponge writes is listed here,
81
73
  * and nothing else writes those columns. Objects (revisions, derivation outputs) must already exist.
@@ -87,7 +79,13 @@ export function applyChange(db, change) {
87
79
  fail();
88
80
  switch (`${entity}.${field.startsWith('review.') ? 'review' : field}`) {
89
81
  case 'item.created': {
90
- if (!object(v) || !['article', 'pdf', 'media'].includes(v.kind) || !str(v.createdAt, 64) || !optional(v.sourceKey, 8192) || !(v.realm === null || typeof v.realm === 'string' && Buffer.byteLength(v.realm) <= 256) || !optional(v.locator, 16384))
82
+ if (!object(v) ||
83
+ !['article', 'pdf', 'media'].includes(v.kind) ||
84
+ !str(v.createdAt, 64) ||
85
+ !optional(v.sourceKey, 8192) ||
86
+ !(v.realm === null || (typeof v.realm === 'string' && Buffer.byteLength(v.realm) <= 256)) ||
87
+ !optional(v.locator, 16384) ||
88
+ (v.locator !== null && isFileLocator(v.locator)))
91
89
  fail();
92
90
  db.prepare('INSERT INTO items(id,kind,created_at) VALUES(?,?,?) ON CONFLICT(id) DO NOTHING').run(id, v.kind, v.createdAt);
93
91
  if (v.sourceKey !== null && v.locator !== null)
@@ -100,10 +98,19 @@ export function applyChange(db, change) {
100
98
  if (v !== null && !db.prepare('SELECT 1 FROM revisions WHERE id=? AND item_id=?').get(v, id))
101
99
  fail('unavailable');
102
100
  return changed(db.prepare('UPDATE items SET current_revision=? WHERE id=?').run(v, id));
103
- case 'item.title':
101
+ case 'item.capturedTitle':
104
102
  if (typeof v !== 'string' || Buffer.byteLength(v) > 4096)
105
103
  fail();
106
- return changed(db.prepare('UPDATE items SET title=? WHERE id=?').run(v, id));
104
+ changed(db.prepare('UPDATE items SET title_captured=? WHERE id=?').run(v, id));
105
+ return refreshTitle(db, id);
106
+ case 'item.noteTitle':
107
+ case 'item.titleOverride':
108
+ if (!optional(v, 4096))
109
+ fail();
110
+ changed(db
111
+ .prepare(`UPDATE items SET ${field === 'noteTitle' ? 'title_note' : 'title_override'}=? WHERE id=?`)
112
+ .run(v, id));
113
+ return refreshTitle(db, id);
107
114
  case 'item.visibility':
108
115
  if (v !== null && v !== 'public' && v !== 'private')
109
116
  fail();
@@ -113,7 +120,14 @@ export function applyChange(db, change) {
113
120
  fail();
114
121
  return changed(db.prepare('UPDATE items SET deleted_at=COALESCE(deleted_at,?) WHERE id=?').run(v, id));
115
122
  case 'annotation.created':
116
- if (!object(v) || !str(v.itemId, 256) || !['note', 'highlight', 'comment'].includes(v.kind) || !optional(v.revisionId) || !optional(v.parentId) || typeof v.body !== 'string' || !str(v.createdAt, 64) || !('anchor' in v))
123
+ if (!object(v) ||
124
+ !str(v.itemId, 256) ||
125
+ !['note', 'highlight', 'comment'].includes(v.kind) ||
126
+ !optional(v.revisionId) ||
127
+ !optional(v.parentId) ||
128
+ typeof v.body !== 'string' ||
129
+ !str(v.createdAt, 64) ||
130
+ !('anchor' in v))
117
131
  fail();
118
132
  db.prepare('INSERT INTO annotations(id,item_id,revision_id,anchor_json,body,created_at,kind,parent_id) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING').run(id, v.itemId, v.revisionId, JSON.stringify(v.anchor), v.body, v.createdAt, v.kind, v.parentId);
119
133
  return;
@@ -134,12 +148,31 @@ export function applyChange(db, change) {
134
148
  if (!status(v))
135
149
  fail();
136
150
  const row = db.prepare('SELECT kind,output_json,review_json FROM derivations WHERE id=?').get(id) ?? fail('unavailable');
137
- db.prepare('UPDATE derivations SET review_json=? WHERE id=?').run(JSON.stringify(setReviewPath(JSON.parse(String(row.review_json)), JSON.parse(String(row.output_json)), field, v, String(row.kind))), id);
151
+ db.prepare('UPDATE derivations SET review_json=? WHERE id=?').run(JSON.stringify(kindOf(row.kind).applyReview(JSON.parse(String(row.review_json)), JSON.parse(String(row.output_json)), field, v)), id);
138
152
  return;
139
153
  }
140
- default: fail();
154
+ default:
155
+ fail();
141
156
  }
142
157
  }
158
+ /**
159
+ * `items.title` is derived: the user's override, else an importer's owner title, else the captured title. A changed title
160
+ * is re-indexed for lexical search at once; replay on a device without the FTS row leaves indexing to capture.
161
+ */
162
+ export function refreshTitle(db, id) {
163
+ const row = db.prepare('SELECT title,COALESCE(title_override,title_note,title_captured) next FROM items WHERE id=?').get(id) ??
164
+ fail('unavailable');
165
+ if (row.title === row.next)
166
+ return;
167
+ db.prepare('UPDATE items SET title=? WHERE id=?').run(String(row.next), id);
168
+ const fts = db
169
+ .prepare("SELECT m.revision_id,p.markdown FROM readable_fts_rows m JOIN representations p ON p.revision_id=m.revision_id AND p.kind='markdown' WHERE m.item_id=?")
170
+ .get(id);
171
+ if (fts && typeof fts.markdown === 'string')
172
+ indexReadable(db, id, String(fts.revision_id), String(row.next), fts.markdown);
173
+ }
174
+ /** Title fields across schemas: schema 5 logged `title`, schema 9 split it (migration 9 renames the old entries). */
175
+ const titleFields = ['title', 'capturedTitle', 'noteTitle', 'titleOverride'];
143
176
  /**
144
177
  * Seed the log with user state written before schema 5, so another device can rebuild it.
145
178
  * Idempotent: a field that already has a change is never seeded again. Rows already exist, so this appends only.
@@ -147,14 +180,25 @@ export function applyChange(db, change) {
147
180
  export function seedChanges(db) {
148
181
  let count = 0;
149
182
  const seed = (entity, entityId, field, value) => {
150
- if (db.prepare('SELECT 1 FROM changes WHERE entity=? AND entity_id=? AND field=? LIMIT 1').get(entity, entityId, field))
183
+ const fields = field === 'title' ? titleFields : [field];
184
+ if (db
185
+ .prepare(`SELECT 1 FROM changes WHERE entity=? AND entity_id=? AND field IN (${fields.map(() => '?').join(',')}) LIMIT 1`)
186
+ .get(entity, entityId, ...fields))
151
187
  return;
152
188
  append(db, { entity, entityId, field, value });
153
189
  count++;
154
190
  };
155
191
  for (const i of db.prepare('SELECT * FROM items ORDER BY created_at,rowid').all()) {
156
- const id = String(i.id), source = db.prepare('SELECT source_key,realm_key,locator FROM source_refs WHERE item_id=? ORDER BY rowid LIMIT 1').get(id);
157
- seed('item', id, 'created', { kind: i.kind, sourceKey: source ? String(source.source_key) : null, realm: source ? String(source.realm_key) : null, locator: source ? String(source.locator) : null, createdAt: String(i.created_at) });
192
+ const id = String(i.id), source = db
193
+ .prepare('SELECT source_key,realm_key,locator FROM source_refs WHERE item_id=? ORDER BY rowid LIMIT 1')
194
+ .get(id);
195
+ seed('item', id, 'created', {
196
+ kind: i.kind,
197
+ sourceKey: source ? String(source.source_key) : null,
198
+ realm: source ? String(source.realm_key) : null,
199
+ locator: source ? String(source.locator) : null,
200
+ createdAt: String(i.created_at),
201
+ });
158
202
  if (i.current_revision !== null)
159
203
  seed('item', id, 'currentRevision', String(i.current_revision));
160
204
  if (i.title !== '')
@@ -166,7 +210,15 @@ export function seedChanges(db) {
166
210
  }
167
211
  for (const a of db.prepare('SELECT * FROM annotations ORDER BY parent_id IS NOT NULL,created_at,rowid').all()) {
168
212
  const id = String(a.id);
169
- seed('annotation', id, 'created', { itemId: String(a.item_id), kind: a.kind, revisionId: a.revision_id ?? null, parentId: a.parent_id ?? null, anchor: parsed(a.anchor_json), body: String(a.body), createdAt: String(a.created_at) });
213
+ seed('annotation', id, 'created', {
214
+ itemId: String(a.item_id),
215
+ kind: a.kind,
216
+ revisionId: a.revision_id ?? null,
217
+ parentId: a.parent_id ?? null,
218
+ anchor: parsed(a.anchor_json),
219
+ body: String(a.body),
220
+ createdAt: String(a.created_at),
221
+ });
170
222
  if (a.updated_at !== null)
171
223
  seed('annotation', id, 'body', { body: String(a.body), updatedAt: String(a.updated_at) });
172
224
  if (a.deleted_at !== null)
@@ -0,0 +1,15 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { StoreOptions } from './types.js';
3
+ export type Row = Record<string, any>;
4
+ export declare const now: () => string;
5
+ /** What every store module shares: the canonical library root, the one connection, and the fault hooks. */
6
+ export interface Context {
7
+ readonly root: string;
8
+ readonly db: DatabaseSync;
9
+ readonly options: StoreOptions;
10
+ }
11
+ /** The store's write boundary: one `BEGIN IMMEDIATE` per call, committed only if `fn` returns. Never nested. */
12
+ export declare function transaction<T>(db: DatabaseSync, fn: () => T): T;
13
+ /** Increases with readable revision changes and tombstones, never with an identical refresh. */
14
+ export declare const revisionEpoch: (db: DatabaseSync) => number;
15
+ export declare const bumpEpoch: (db: DatabaseSync) => void;
@@ -0,0 +1,19 @@
1
+ export const now = () => new Date().toISOString();
2
+ /** The store's write boundary: one `BEGIN IMMEDIATE` per call, committed only if `fn` returns. Never nested. */
3
+ export function transaction(db, fn) {
4
+ db.exec('BEGIN IMMEDIATE');
5
+ try {
6
+ const result = fn();
7
+ db.exec('COMMIT');
8
+ return result;
9
+ }
10
+ catch (error) {
11
+ db.exec('ROLLBACK');
12
+ throw error;
13
+ }
14
+ }
15
+ /** Increases with readable revision changes and tombstones, never with an identical refresh. */
16
+ export const revisionEpoch = (db) => Number(db.prepare('SELECT revision_epoch FROM library_info WHERE id=1').get()?.revision_epoch);
17
+ export const bumpEpoch = (db) => {
18
+ db.exec('UPDATE library_info SET revision_epoch=revision_epoch+1 WHERE id=1');
19
+ };
@@ -0,0 +1,42 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import { type DerivationOf, type Kind, type KindTypes } from '../derivations/kinds.js';
3
+ import type { PublicDigest } from '../derivations/types.js';
4
+ import { type Context } from './context.js';
5
+ /** What storing a derivation of kind `K` takes: its item, revision and producer, plus the kind's own candidate. */
6
+ export type PutDerivation<K extends Kind> = {
7
+ itemId: string;
8
+ revisionId: string;
9
+ producer: Record<string, unknown>;
10
+ } & KindTypes[K]['candidate'];
11
+ /** Content address of a derivation: the same output from the same input is the same immutable object on every device. */
12
+ export declare const derivationId: (revisionId: string, kind: string, inputHash: string, output: string) => string;
13
+ /**
14
+ * Store a derivation the kind admits against the revision it was made from. Derivations are immutable data, not change-log
15
+ * entries: a different output is a new object, the same output a duplicate.
16
+ */
17
+ export declare function putDerivation<K extends Kind>(db: DatabaseSync, kind: K, input: PutDerivation<K>): {
18
+ id: string;
19
+ duplicate: boolean;
20
+ };
21
+ /** Review fields of a derivation: each changed path the kind names is its own logged change. */
22
+ export declare function reviewDerivation<K extends Kind>(db: DatabaseSync, kind: K, id: string, input: KindTypes[K]['review']): DerivationOf<K>;
23
+ /**
24
+ * The public digest of an item, from the first kind in `publishOrder` with a current derivation and never from two: the
25
+ * accepted fields of the current digest (v2, or a stored v1), else the public subset of an accepted editorial note (lead,
26
+ * claims, verbatim quotes within the v2 limits). Never attributions, and never the note's other content.
27
+ */
28
+ export declare function publicDerivation(db: DatabaseSync, itemId: string): PublicDigest | undefined;
29
+ /** Derived objects (digests, editorial notes) of the library's revisions, by kind. */
30
+ export declare class Derivations {
31
+ private readonly c;
32
+ constructor(c: Context);
33
+ put<K extends Kind>(kind: K, input: PutDerivation<K>): {
34
+ id: string;
35
+ duplicate: boolean;
36
+ };
37
+ read<K extends Kind>(kind: K, id: string): DerivationOf<K> | undefined;
38
+ current<K extends Kind>(kind: K, itemId: string): DerivationOf<K> | undefined;
39
+ /** The newest stored derivation for exactly these inputs, so a rerun reuses it instead of asking the model again. */
40
+ find<K extends Kind>(kind: K, revisionId: string, inputHash: string): DerivationOf<K> | undefined;
41
+ review<K extends Kind>(kind: K, id: string, input: KindTypes[K]['review']): DerivationOf<K>;
42
+ }
@@ -0,0 +1,104 @@
1
+ import { markdownFor } from '../annotations/store.js';
2
+ import { derivationKinds, publishOrder } from '../derivations/kinds.js';
3
+ import { hash, text } from './files.js';
4
+ import { sha256 } from '../util/hash.js';
5
+ import { mutate } from './changes.js';
6
+ import { now, transaction } from './context.js';
7
+ import { SpongeError } from '../errors.js';
8
+ const fail = (code = 'invalid-input') => {
9
+ throw new SpongeError(code);
10
+ };
11
+ /** Content address of a derivation: the same output from the same input is the same immutable object on every device. */
12
+ export const derivationId = (revisionId, kind, inputHash, output) => sha256(JSON.stringify(['derivation-v1', revisionId, kind, inputHash, output]));
13
+ const row = (r) => ({
14
+ id: String(r.id),
15
+ itemId: String(r.item_id),
16
+ revisionId: String(r.revision_id),
17
+ kind: String(r.kind),
18
+ producer: JSON.parse(String(r.producer_json)),
19
+ inputHash: String(r.input_hash),
20
+ output: JSON.parse(String(r.output_json)),
21
+ review: JSON.parse(String(r.review_json)),
22
+ createdAt: String(r.created_at),
23
+ });
24
+ /**
25
+ * Store a derivation the kind admits against the revision it was made from. Derivations are immutable data, not change-log
26
+ * entries: a different output is a new object, the same output a duplicate.
27
+ */
28
+ export function putDerivation(db, kind, input) {
29
+ const producer = JSON.stringify(input.producer);
30
+ if (!producer || Buffer.byteLength(producer) > 4096)
31
+ fail();
32
+ if (db.prepare('SELECT item_id FROM revisions WHERE id=?').get(input.revisionId)?.item_id !== input.itemId)
33
+ fail('unavailable');
34
+ const admitted = derivationKinds[kind].admit(input, () => markdownFor(db, input.revisionId));
35
+ const id = derivationId(input.revisionId, kind, admitted.inputHash, admitted.output);
36
+ if (db.prepare('SELECT 1 FROM derivations WHERE id=?').get(id))
37
+ return { id, duplicate: true };
38
+ db.prepare('INSERT INTO derivations(id,item_id,revision_id,kind,schema_version,producer_json,input_hash,output_json,created_at) VALUES(?,?,?,?,?,?,?,?,?)').run(id, input.itemId, input.revisionId, kind, admitted.schemaVersion, producer, admitted.inputHash, admitted.output, now());
39
+ return { id, duplicate: false };
40
+ }
41
+ function readDerivation(db, kind, id) {
42
+ const r = db.prepare('SELECT * FROM derivations WHERE id=? AND kind=?').get(id, kind);
43
+ return r ? row(r) : undefined;
44
+ }
45
+ /** The newest derivation of this kind made from the item's current revision. */
46
+ function currentDerivation(db, kind, itemId) {
47
+ const r = db
48
+ .prepare('SELECT d.* FROM derivations d JOIN items i ON i.id=d.item_id AND i.current_revision=d.revision_id WHERE d.item_id=? AND d.kind=? ORDER BY d.created_at DESC,d.rowid DESC LIMIT 1')
49
+ .get(itemId, kind);
50
+ return r ? row(r) : undefined;
51
+ }
52
+ /** Review fields of a derivation: each changed path the kind names is its own logged change. */
53
+ export function reviewDerivation(db, kind, id, input) {
54
+ const d = readDerivation(db, kind, id) ?? fail('unavailable');
55
+ for (const [path, value] of derivationKinds[kind].reviewPaths(d, input))
56
+ mutate(db, 'derivation', id, path, value);
57
+ return readDerivation(db, kind, id);
58
+ }
59
+ /**
60
+ * The public digest of an item, from the first kind in `publishOrder` with a current derivation and never from two: the
61
+ * accepted fields of the current digest (v2, or a stored v1), else the public subset of an accepted editorial note (lead,
62
+ * claims, verbatim quotes within the v2 limits). Never attributions, and never the note's other content.
63
+ */
64
+ export function publicDerivation(db, itemId) {
65
+ for (const kind of publishOrder) {
66
+ const d = currentDerivation(db, kind, itemId);
67
+ if (d)
68
+ return derivationKinds[kind].publish?.(d, () => markdownFor(db, d.revisionId));
69
+ }
70
+ return undefined;
71
+ }
72
+ /** Derived objects (digests, editorial notes) of the library's revisions, by kind. */
73
+ export class Derivations {
74
+ c;
75
+ constructor(c) {
76
+ this.c = c;
77
+ }
78
+ put(kind, input) {
79
+ text(input.itemId, 256);
80
+ text(input.revisionId, 256);
81
+ return transaction(this.c.db, () => putDerivation(this.c.db, kind, input));
82
+ }
83
+ read(kind, id) {
84
+ text(id, 256);
85
+ return readDerivation(this.c.db, kind, id);
86
+ }
87
+ current(kind, itemId) {
88
+ text(itemId, 256);
89
+ return currentDerivation(this.c.db, kind, itemId);
90
+ }
91
+ /** The newest stored derivation for exactly these inputs, so a rerun reuses it instead of asking the model again. */
92
+ find(kind, revisionId, inputHash) {
93
+ text(revisionId, 256);
94
+ hash(inputHash);
95
+ const r = this.c.db
96
+ .prepare('SELECT * FROM derivations WHERE revision_id=? AND kind=? AND input_hash=? ORDER BY created_at DESC,rowid DESC LIMIT 1')
97
+ .get(revisionId, kind, inputHash);
98
+ return r ? row(r) : undefined;
99
+ }
100
+ review(kind, id, input) {
101
+ text(id, 256);
102
+ return transaction(this.c.db, () => reviewDerivation(this.c.db, kind, id, input));
103
+ }
104
+ }
@@ -1,8 +1,6 @@
1
- export declare const digest: (value: string | Uint8Array) => string;
2
1
  export declare function hash(value: string): void;
3
2
  export declare function text(value: string, max?: number): void;
4
3
  export declare function integer(value: number, maximum?: number): void;
5
- export declare function syncDirectory(path: string): void;
6
4
  /** Reject symlinks in every existing component; callers use canonical /private/tmp on macOS. */
7
5
  export declare function safeDirectory(path: string, create?: boolean): string;
8
6
  export declare function confinedFile(root: string, path: string): string;
@@ -1,40 +1,31 @@
1
- import { constants, closeSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, realpathSync } from 'node:fs';
1
+ import { constants, closeSync, fstatSync, lstatSync, mkdirSync, openSync, readSync, realpathSync } from 'node:fs';
2
2
  import { dirname, isAbsolute, join, parse, relative, sep } from 'node:path';
3
3
  import { createHash } from 'node:crypto';
4
- import { StoreError } from './types.js';
5
- export const digest = (value) => createHash('sha256').update(value).digest('hex');
4
+ import { syncDirectory } from '../util/fsync.js';
5
+ import { SpongeError } from '../errors.js';
6
6
  export function hash(value) {
7
7
  if (!/^[a-f0-9]{64}$/.test(value))
8
- throw new StoreError('invalid-input');
8
+ throw new SpongeError('invalid-input');
9
9
  }
10
10
  export function text(value, max = 4096) {
11
11
  if (typeof value !== 'string' || value.length === 0 || Buffer.byteLength(value) > max || value.includes('\0'))
12
- throw new StoreError('invalid-input');
12
+ throw new SpongeError('invalid-input');
13
13
  }
14
14
  export function integer(value, maximum = Number.MAX_SAFE_INTEGER) {
15
15
  if (!Number.isSafeInteger(value) || value < 0 || value > maximum)
16
- throw new StoreError('invalid-input');
17
- }
18
- export function syncDirectory(path) {
19
- const fd = openSync(path, constants.O_RDONLY);
20
- try {
21
- fsyncSync(fd);
22
- }
23
- finally {
24
- closeSync(fd);
25
- }
16
+ throw new SpongeError('invalid-input');
26
17
  }
27
18
  /** Reject symlinks in every existing component; callers use canonical /private/tmp on macOS. */
28
19
  export function safeDirectory(path, create = false) {
29
20
  if (!isAbsolute(path) || path.includes('\0') || path.split(sep).includes('..'))
30
- throw new StoreError('invalid-input');
21
+ throw new SpongeError('invalid-input');
31
22
  let current = parse(path).root;
32
23
  for (const part of path.slice(current.length).split(sep).filter(Boolean)) {
33
24
  current = join(current, part);
34
25
  try {
35
26
  const stat = lstatSync(current);
36
27
  if (!stat.isDirectory() || stat.isSymbolicLink())
37
- throw new StoreError('integrity');
28
+ throw new SpongeError('integrity');
38
29
  }
39
30
  catch (error) {
40
31
  if (error.code !== 'ENOENT' || !create)
@@ -48,14 +39,14 @@ export function safeDirectory(path, create = false) {
48
39
  export function confinedFile(root, path) {
49
40
  const rel = relative(root, path);
50
41
  if (!rel || rel.startsWith('..' + sep) || rel === '..' || isAbsolute(rel))
51
- throw new StoreError('integrity');
42
+ throw new SpongeError('integrity');
52
43
  safeDirectory(dirname(path));
53
44
  return path;
54
45
  }
55
46
  export function regularFile(path) {
56
47
  const stat = lstatSync(path);
57
48
  if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1)
58
- throw new StoreError('integrity');
49
+ throw new SpongeError('integrity');
59
50
  }
60
51
  function checkedFile(path, expectedHash, expectedBytes, retain) {
61
52
  regularFile(path);
@@ -63,7 +54,7 @@ function checkedFile(path, expectedHash, expectedBytes, retain) {
63
54
  try {
64
55
  const stat = fstatSync(fd);
65
56
  if (!stat.isFile() || stat.size !== expectedBytes || stat.nlink !== 1)
66
- throw new StoreError('integrity');
57
+ throw new SpongeError('integrity');
67
58
  const h = createHash('sha256'), buffer = Buffer.allocUnsafe(64 * 1024);
68
59
  let count = 0;
69
60
  const chunks = [];
@@ -73,13 +64,13 @@ function checkedFile(path, expectedHash, expectedBytes, retain) {
73
64
  break;
74
65
  count += read;
75
66
  if (count > expectedBytes)
76
- throw new StoreError('integrity');
67
+ throw new SpongeError('integrity');
77
68
  h.update(buffer.subarray(0, read));
78
69
  if (retain)
79
70
  chunks.push(Buffer.from(buffer.subarray(0, read)));
80
71
  }
81
72
  if (count !== expectedBytes || h.digest('hex') !== expectedHash)
82
- throw new StoreError('integrity');
73
+ throw new SpongeError('integrity');
83
74
  return Buffer.concat(chunks);
84
75
  }
85
76
  finally {
@@ -97,5 +88,9 @@ export function pathExists(path) {
97
88
  throw error;
98
89
  }
99
90
  }
100
- export function verifyFile(path, expectedHash, expectedBytes) { checkedFile(path, expectedHash, expectedBytes, false); }
101
- export function readVerifiedFile(path, expectedHash, expectedBytes) { return checkedFile(path, expectedHash, expectedBytes, true); }
91
+ export function verifyFile(path, expectedHash, expectedBytes) {
92
+ checkedFile(path, expectedHash, expectedBytes, false);
93
+ }
94
+ export function readVerifiedFile(path, expectedHash, expectedBytes) {
95
+ return checkedFile(path, expectedHash, expectedBytes, true);
96
+ }
@@ -0,0 +1,30 @@
1
+ import type { MetadataClaim } from '../../contracts/index.js';
2
+ /**
3
+ * Ids of immutable captured objects: domain-tagged content addresses scoped by their (content-addressed) revision, so two
4
+ * devices capturing the same bytes write the same rows. See docs/decisions/content-ids.md.
5
+ */
6
+ export declare const representationId: (revisionId: string, kind: string, mimeType: string, contentSha256: string) => string;
7
+ export declare const blockId: (revisionId: string, ordinal: number, kind: string, text: string) => string;
8
+ export type ClaimColumns = [
9
+ field: string,
10
+ valueJson: string,
11
+ source: string,
12
+ observedAt: string,
13
+ rule: string | null,
14
+ timezoneKnown: number | null,
15
+ observedJson: string | null,
16
+ locator: string | null,
17
+ selected: number | null,
18
+ extractionVersion: string | null,
19
+ datePrecision: string | null
20
+ ];
21
+ /** The stored columns of a claim after `id` and `revision_id`, in table order: what capture inserts and what its id hashes. */
22
+ export declare const claimColumns: (claim: MetadataClaim) => ClaimColumns;
23
+ export declare const claimId: (revisionId: string, ordinal: number, columns: readonly (string | number | null)[]) => string;
24
+ /** A locator that names a file on this device. Only `file:` locators carry a path; `local:` is their portable form. */
25
+ export declare const isFileLocator: (locator: string) => boolean;
26
+ /**
27
+ * What syncs in place of a `file:` locator: `local:<domain-tagged hash of the URL>/<basename>`. The same path gives the same
28
+ * locator; the path itself is kept only in `local_sources`. Total: any other locator is returned unchanged.
29
+ */
30
+ export declare function portableLocator(locator: string): string;
@@ -0,0 +1,34 @@
1
+ import { contentId, sha256 } from '../util/hash.js';
2
+ /**
3
+ * Ids of immutable captured objects: domain-tagged content addresses scoped by their (content-addressed) revision, so two
4
+ * devices capturing the same bytes write the same rows. See docs/decisions/content-ids.md.
5
+ */
6
+ export const representationId = (revisionId, kind, mimeType, contentSha256) => contentId('representation-v1', [revisionId, kind, mimeType, contentSha256]);
7
+ export const blockId = (revisionId, ordinal, kind, text) => contentId('block-v1', [revisionId, ordinal, kind, sha256(text)]);
8
+ /** The stored columns of a claim after `id` and `revision_id`, in table order: what capture inserts and what its id hashes. */
9
+ export const claimColumns = (claim) => [
10
+ claim.field,
11
+ JSON.stringify(claim.value),
12
+ claim.source,
13
+ claim.observedAt,
14
+ claim.rule ?? null,
15
+ claim.timezoneKnown === undefined ? null : Number(claim.timezoneKnown),
16
+ claim.observed === undefined ? null : JSON.stringify(claim.observed),
17
+ claim.locator ?? null,
18
+ claim.selected === undefined ? null : Number(claim.selected),
19
+ claim.extractionVersion ?? null,
20
+ claim.datePrecision ?? null,
21
+ ];
22
+ export const claimId = (revisionId, ordinal, columns) => contentId('metadata-claim-v1', [revisionId, ordinal, ...columns]);
23
+ /** A locator that names a file on this device. Only `file:` locators carry a path; `local:` is their portable form. */
24
+ export const isFileLocator = (locator) => /^file:/i.test(locator);
25
+ /**
26
+ * What syncs in place of a `file:` locator: `local:<domain-tagged hash of the URL>/<basename>`. The same path gives the same
27
+ * locator; the path itself is kept only in `local_sources`. Total: any other locator is returned unchanged.
28
+ */
29
+ export function portableLocator(locator) {
30
+ if (!isFileLocator(locator))
31
+ return locator;
32
+ const name = locator.split(/[?#]/)[0].split('/').filter(Boolean).pop() ?? '';
33
+ return `local:${contentId('local-path-v1', locator)}/${name.slice(0, 1024)}`;
34
+ }
@@ -0,0 +1,26 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import * as jungle from '../importers/jungle-store.js';
3
+ import { type Context } from './context.js';
4
+ /** The receipt an importer left for exactly this source state, if any. */
5
+ export declare function importReceipt(db: DatabaseSync, namespace: string, sourceKey: string, sourceHash: string): {
6
+ itemId: string;
7
+ outcome: string;
8
+ } | null;
9
+ /** What importers read and write: their realm's items and revisions, their receipts, and Jungle Reading notes. */
10
+ export declare class Imports {
11
+ private readonly c;
12
+ constructor(c: Context);
13
+ /** The item an importer realm holds for one source key, with its current revision; deleted items are reported, not hidden. */
14
+ item(realm: string, sourceKey: string): {
15
+ itemId: string;
16
+ deleted: boolean;
17
+ revisionId: string | null;
18
+ } | null;
19
+ revision(itemId: string, bundleHash: string): string;
20
+ receipt(namespace: string, sourceKey: string, sourceHash: string): {
21
+ itemId: string;
22
+ outcome: string;
23
+ } | null;
24
+ planJungleNote(input: jungle.JungleNoteInput): jungle.JunglePlan;
25
+ applyJungleNote(input: jungle.JungleNoteInput): jungle.JunglePlan;
26
+ }