@hraness/sponge 0.1.0 → 0.2.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 (174) hide show
  1. package/README.md +163 -17
  2. package/bin/sponge.js +9 -2
  3. package/dist/contracts/index.d.ts +164 -0
  4. package/dist/contracts/index.js +6 -0
  5. package/dist/contracts/validate.d.ts +14 -0
  6. package/dist/contracts/validate.js +187 -0
  7. package/dist/migrations/001.d.ts +3 -0
  8. package/dist/migrations/001.js +33 -0
  9. package/dist/migrations/002.d.ts +3 -0
  10. package/dist/migrations/002.js +6 -0
  11. package/dist/migrations/003.d.ts +3 -0
  12. package/dist/migrations/003.js +10 -0
  13. package/dist/migrations/004.d.ts +3 -0
  14. package/dist/migrations/004.js +9 -0
  15. package/dist/src/acquisition/capture.d.ts +29 -0
  16. package/dist/src/acquisition/capture.js +160 -0
  17. package/dist/src/acquisition/cleanup.d.ts +6 -0
  18. package/dist/src/acquisition/cleanup.js +20 -0
  19. package/dist/src/acquisition/errors.d.ts +3 -0
  20. package/dist/src/acquisition/errors.js +20 -0
  21. package/dist/src/acquisition/files.d.ts +4 -0
  22. package/dist/src/acquisition/files.js +49 -0
  23. package/dist/src/acquisition/index.d.ts +6 -0
  24. package/dist/src/acquisition/index.js +4 -0
  25. package/dist/src/acquisition/lock.d.ts +1 -0
  26. package/dist/src/acquisition/lock.js +2 -0
  27. package/dist/src/acquisition/normalize.d.ts +11 -0
  28. package/dist/src/acquisition/normalize.js +62 -0
  29. package/dist/src/acquisition/pins.d.ts +3 -0
  30. package/dist/src/acquisition/pins.js +9 -0
  31. package/dist/src/acquisition/process.d.ts +16 -0
  32. package/dist/src/acquisition/process.js +63 -0
  33. package/dist/src/acquisition/runtime.d.ts +53 -0
  34. package/dist/src/acquisition/runtime.js +216 -0
  35. package/dist/src/acquisition/verify-helper.d.ts +1 -0
  36. package/dist/src/acquisition/verify-helper.js +19 -0
  37. package/dist/src/app/article.d.ts +23 -0
  38. package/dist/src/app/article.js +82 -0
  39. package/dist/src/app/bundle-files.d.ts +8 -0
  40. package/dist/src/app/bundle-files.js +66 -0
  41. package/dist/src/app/markdown.d.ts +6 -0
  42. package/dist/src/app/markdown.js +90 -0
  43. package/dist/src/app/source.d.ts +15 -0
  44. package/dist/src/app/source.js +40 -0
  45. package/dist/src/cli/arguments.d.ts +9 -0
  46. package/dist/src/cli/arguments.js +106 -0
  47. package/dist/src/cli/config.d.ts +8 -0
  48. package/dist/src/cli/config.js +45 -0
  49. package/dist/src/cli/import.d.ts +4 -0
  50. package/dist/src/cli/import.js +3 -0
  51. package/dist/src/cli/indexing.d.ts +7 -0
  52. package/dist/src/cli/indexing.js +21 -0
  53. package/dist/src/cli/main.d.ts +1 -0
  54. package/dist/src/cli/main.js +333 -0
  55. package/dist/src/cli/portability.d.ts +9 -0
  56. package/dist/src/cli/portability.js +39 -0
  57. package/dist/src/cli/queue.d.ts +2 -0
  58. package/dist/src/cli/queue.js +23 -0
  59. package/dist/src/cli/terminal.d.ts +2 -0
  60. package/dist/src/cli/terminal.js +4 -0
  61. package/dist/src/cli/worker.d.ts +16 -0
  62. package/dist/src/cli/worker.js +62 -0
  63. package/dist/src/importers/apply.d.ts +29 -0
  64. package/dist/src/importers/apply.js +85 -0
  65. package/dist/src/importers/custody.d.ts +6 -0
  66. package/dist/src/importers/custody.js +35 -0
  67. package/dist/src/importers/inspect.d.ts +21 -0
  68. package/dist/src/importers/inspect.js +97 -0
  69. package/dist/src/importers/manifest.d.ts +22 -0
  70. package/dist/src/importers/manifest.js +43 -0
  71. package/dist/src/importers/verify-helper.d.ts +1 -0
  72. package/dist/src/importers/verify-helper.js +27 -0
  73. package/dist/src/indexing/image.d.ts +10 -0
  74. package/dist/src/indexing/image.js +34 -0
  75. package/dist/src/indexing/index.d.ts +53 -0
  76. package/dist/src/indexing/index.js +123 -0
  77. package/dist/src/indexing/jobs.d.ts +14 -0
  78. package/dist/src/indexing/jobs.js +49 -0
  79. package/dist/src/indexing/runner.d.ts +23 -0
  80. package/dist/src/indexing/runner.js +114 -0
  81. package/dist/src/inference/asr/bridge.d.ts +1 -0
  82. package/dist/src/inference/asr/bridge.js +16 -0
  83. package/dist/src/inference/asr/protocol.d.ts +23 -0
  84. package/dist/src/inference/asr/protocol.js +60 -0
  85. package/dist/src/inference/asr/runtime.d.ts +29 -0
  86. package/dist/src/inference/asr/runtime.js +103 -0
  87. package/dist/src/inference/asr/windows.d.ts +9 -0
  88. package/dist/src/inference/asr/windows.js +41 -0
  89. package/dist/src/inference/embedding/index.d.ts +47 -0
  90. package/dist/src/inference/embedding/index.js +122 -0
  91. package/dist/src/media/capture-helper.d.ts +1 -0
  92. package/dist/src/media/capture-helper.js +16 -0
  93. package/dist/src/media/capture.d.ts +32 -0
  94. package/dist/src/media/capture.js +193 -0
  95. package/dist/src/media/handler.d.ts +22 -0
  96. package/dist/src/media/handler.js +82 -0
  97. package/dist/src/media/transcribe.d.ts +29 -0
  98. package/dist/src/media/transcribe.js +93 -0
  99. package/dist/src/metadata/article.d.ts +5 -0
  100. package/dist/src/metadata/article.js +51 -0
  101. package/dist/src/models/artifacts.d.ts +36 -0
  102. package/dist/src/models/artifacts.js +274 -0
  103. package/dist/src/models/catalog.d.ts +35 -0
  104. package/dist/src/models/catalog.js +71 -0
  105. package/dist/src/models/download.d.ts +9 -0
  106. package/dist/src/models/download.js +147 -0
  107. package/dist/src/models/runtime.d.ts +24 -0
  108. package/dist/src/models/runtime.js +37 -0
  109. package/dist/src/pdf/capture-helper.d.ts +1 -0
  110. package/dist/src/pdf/capture-helper.js +12 -0
  111. package/dist/src/pdf/capture.d.ts +29 -0
  112. package/dist/src/pdf/capture.js +156 -0
  113. package/dist/src/pdf/errors.d.ts +2 -0
  114. package/dist/src/pdf/errors.js +13 -0
  115. package/dist/src/pdf/handler.d.ts +24 -0
  116. package/dist/src/pdf/handler.js +84 -0
  117. package/dist/src/pdf/index.d.ts +4 -0
  118. package/dist/src/pdf/index.js +4 -0
  119. package/dist/src/pdf/input.d.ts +2 -0
  120. package/dist/src/pdf/input.js +46 -0
  121. package/dist/src/pdf/normalize.d.ts +10 -0
  122. package/dist/src/pdf/normalize.js +64 -0
  123. package/dist/src/pdf/source-identity.d.ts +4 -0
  124. package/dist/src/pdf/source-identity.js +12 -0
  125. package/dist/src/pdf/source.d.ts +6 -0
  126. package/dist/src/pdf/source.js +22 -0
  127. package/dist/src/pdf/verify-helper.d.ts +1 -0
  128. package/dist/src/pdf/verify-helper.js +17 -0
  129. package/dist/src/portability/backup.d.ts +23 -0
  130. package/dist/src/portability/backup.js +138 -0
  131. package/dist/src/portability/database.d.ts +14 -0
  132. package/dist/src/portability/database.js +78 -0
  133. package/dist/src/portability/export.d.ts +8 -0
  134. package/dist/src/portability/export.js +36 -0
  135. package/dist/src/portability/files.d.ts +33 -0
  136. package/dist/src/portability/files.js +78 -0
  137. package/dist/src/portability/gc.d.ts +21 -0
  138. package/dist/src/portability/gc.js +66 -0
  139. package/dist/src/search/extension.d.ts +3 -0
  140. package/dist/src/search/extension.js +17 -0
  141. package/dist/src/search/hybrid.d.ts +39 -0
  142. package/dist/src/search/hybrid.js +118 -0
  143. package/dist/src/search/query.d.ts +12 -0
  144. package/dist/src/search/query.js +47 -0
  145. package/dist/src/search/vector.d.ts +21 -0
  146. package/dist/src/search/vector.js +21 -0
  147. package/dist/src/service/launch-agent.d.ts +47 -0
  148. package/dist/src/service/launch-agent.js +179 -0
  149. package/dist/src/service/lifetime.d.ts +7 -0
  150. package/dist/src/service/lifetime.js +73 -0
  151. package/dist/src/service/loop.d.ts +21 -0
  152. package/dist/src/service/loop.js +94 -0
  153. package/dist/src/store/files.d.ts +12 -0
  154. package/dist/src/store/files.js +101 -0
  155. package/dist/src/store/index.d.ts +118 -0
  156. package/dist/src/store/index.js +727 -0
  157. package/dist/src/store/types.d.ts +90 -0
  158. package/dist/src/store/types.js +8 -0
  159. package/dist/src/worker/basic.d.ts +10 -0
  160. package/dist/src/worker/basic.js +27 -0
  161. package/dist/src/worker/custody.d.ts +13 -0
  162. package/dist/src/worker/custody.js +185 -0
  163. package/dist/src/worker/engine.d.ts +28 -0
  164. package/dist/src/worker/engine.js +78 -0
  165. package/dist/src/worker/identity.d.ts +7 -0
  166. package/dist/src/worker/identity.js +27 -0
  167. package/dist/src/worker/journal.d.ts +5 -0
  168. package/dist/src/worker/journal.js +30 -0
  169. package/dist/src/worker/supervisor-child.d.ts +1 -0
  170. package/dist/src/worker/supervisor-child.js +33 -0
  171. package/dist/src/worker/supervisor.d.ts +3 -0
  172. package/dist/src/worker/supervisor.js +47 -0
  173. package/native/runtime-manifest.json +40 -0
  174. package/package.json +36 -6
@@ -0,0 +1,62 @@
1
+ import { join } from 'node:path';
2
+ import { inspectNative } from '../models/runtime.js';
3
+ import { drainIndexQueue } from '../indexing/runner.js';
4
+ import { drainLibrary } from '../worker/basic.js';
5
+ import { recoverServiceLifetime } from '../service/lifetime.js';
6
+ import { StoreError } from '../store/types.js';
7
+ import { runWorkerService } from '../service/loop.js';
8
+ import { installService, uninstallService, serviceStatus, installedServiceId, serviceEnvironment } from '../service/launch-agent.js';
9
+ export async function workerAction(store, action, version, environment, options = {}) {
10
+ const env = environment ?? serviceEnvironment(version);
11
+ if (action === 'install')
12
+ return installService(store, env, options);
13
+ if (action === 'uninstall')
14
+ return uninstallService(store, env);
15
+ return serviceStatus(store, env);
16
+ }
17
+ /** Dispatch only work that claimNext can admit; delayed retries retain their due time. */
18
+ export function hasDueAcquisition(store) {
19
+ const now = new Date().toISOString();
20
+ return store.listJobs().some(job => job.operation !== 'index' && ['queued', 'delayed'].includes(String(job.state)) && job.cancel_requested !== 1 && (job.not_before === null || job.not_before === undefined || String(job.not_before) <= now));
21
+ }
22
+ export async function drainServiceQueues(store, options, version, signal) {
23
+ if (store.pendingCleanupCount())
24
+ throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
25
+ const results = [];
26
+ let acquisitionError;
27
+ if (hasDueAcquisition(store)) {
28
+ try {
29
+ const runtime = options.runtime ?? await options.resolveCaptureRuntime?.();
30
+ if (!runtime)
31
+ throw Object.assign(Error('runtime-missing'), { code: 'runtime-missing' });
32
+ let asrRuntime = options.asrRuntime;
33
+ try {
34
+ asrRuntime ??= await inspectNative('asr', join(store.root, 'runtime', 'models'), version);
35
+ }
36
+ catch (error) {
37
+ if (!['runtime-missing', 'unsupported'].includes(String(error.code)))
38
+ throw error;
39
+ }
40
+ results.push(...await drainLibrary(store, { ...options, runtime, asrRuntime, signal, allowPrivateNetwork: false }));
41
+ }
42
+ catch (error) {
43
+ acquisitionError = error;
44
+ }
45
+ }
46
+ // A missing capture setup must not starve already queued, independently runnable indexes.
47
+ if (!signal.aborted && store.pendingCleanupCount() === 0 && store.listJobs().some(job => job.operation === 'index' && job.state === 'queued')) {
48
+ const runtime = await inspectNative('embedding', join(store.root, 'runtime', 'models'), version);
49
+ results.push(...await drainIndexQueue(store, { runtime, signal }));
50
+ }
51
+ if (acquisitionError)
52
+ throw acquisitionError;
53
+ return results;
54
+ }
55
+ export async function serveWorker(store, options, version, environment) {
56
+ const env = environment ?? serviceEnvironment(version), installationId = installedServiceId(store, env);
57
+ if (!options.signal)
58
+ throw Error('signal-required');
59
+ if (!await recoverServiceLifetime(store, installationId, env.alive))
60
+ throw new StoreError('owner-busy');
61
+ return runWorkerService(store, { installationId, signal: options.signal, drain: signal => drainServiceQueues(store, options, version, signal) });
62
+ }
@@ -0,0 +1,29 @@
1
+ import type { LibraryStore } from '../store/index.js';
2
+ import { type BundleVerifier } from './inspect.js';
3
+ import type { CaptureRuntime } from '../acquisition/runtime.js';
4
+ export interface ImportOptions {
5
+ runtime: CaptureRuntime;
6
+ dryRun?: boolean;
7
+ limit?: number;
8
+ offset?: number;
9
+ signal?: AbortSignal;
10
+ verify?: BundleVerifier;
11
+ }
12
+ export interface ImportReport {
13
+ schemaVersion: 1;
14
+ manifestSha256: string;
15
+ namespace: string;
16
+ dryRun: boolean;
17
+ entries: {
18
+ id: string;
19
+ status: 'ready' | 'imported' | 'duplicate' | 'unsupported' | 'failed';
20
+ sourceHash?: string;
21
+ historicalIntegrity?: 'verified' | 'unavailable';
22
+ itemId?: string;
23
+ code?: string;
24
+ privacy: 'private' | 'published';
25
+ }[];
26
+ nextOffset: number | null;
27
+ }
28
+ /** Additive bounded batches. Every applied entry is reverified; source bundles are read-only. */
29
+ export declare function importManifest(store: LibraryStore, path: string, options: ImportOptions): Promise<ImportReport>;
@@ -0,0 +1,85 @@
1
+ import { hash } from '../acquisition/files.js';
2
+ import { failure } from '../acquisition/errors.js';
3
+ import { bundleBytes, bundleText } from '../app/bundle-files.js';
4
+ import { localizeMarkdown } from '../app/markdown.js';
5
+ import { selectedMetadata } from '../metadata/article.js';
6
+ import { drainQueue } from '../worker/engine.js';
7
+ import { readImportManifest, importBundleRoot } from './manifest.js';
8
+ import { inspectImportBundle, releasedBundleVerifier } from './inspect.js';
9
+ const errorCode = (error) => { const code = error?.code ?? (error instanceof Error ? error.message : ''); return ['unsupported', 'invalid-input', 'integrity', 'limit-exceeded', 'cancelled', 'cleanup-required', 'runtime-missing', 'owner-busy', 'unavailable'].includes(code) ? code : 'unclassified'; };
10
+ async function promote(context, bundle, namespace, entry, sourceHash) {
11
+ const { store, attempt, signal, checkpoint } = context;
12
+ let committed = false;
13
+ let result = failure(attempt.attemptId, 'unclassified');
14
+ try {
15
+ const source = store.sourceForJob(attempt.jobId);
16
+ if (!source)
17
+ throw Error('integrity');
18
+ await checkpoint('extracting');
19
+ const markdown = await bundleText(bundle.root, bundle.markdown), localized = localizeMarkdown(bundle.kind === 'pdf' ? markdown.replace(/^<!-- pdf-page: ([1-9][0-9]*) -->$/gm, '## Page $1') : markdown, bundle.assets), assets = [];
20
+ for (const asset of bundle.assets) {
21
+ if (signal.aborted)
22
+ throw Error('cancelled');
23
+ const stored = await store.putBlob(attempt, asset.sha256, asset.bytes, bundleBytes(bundle.root, asset));
24
+ assets.push({ ...stored, mediaType: asset.mediaType, relationship: asset.relationship });
25
+ }
26
+ if (signal.aborted)
27
+ throw Error('cancelled');
28
+ await checkpoint('promoting');
29
+ const title = selectedMetadata(bundle.metadata, 'title')?.value;
30
+ store.commitRevision({ ...attempt, itemId: source.itemId, bundleHash: hash(JSON.stringify(['wordcell-import-v1', sourceHash])), markdown: localized.markdown, title: typeof title === 'string' ? title : undefined, extractor: { name: 'sponge-wordcell-import', version: '1' }, status: bundle.status === 'partial' || localized.missingImages ? 'partial' : 'complete', metadata: [...bundle.metadata, ...Object.entries({ importNamespace: namespace, externalSourceId: entry.id, sourcePrivacy: entry.privacy }).map(([field, value]) => ({ field, value, source: bundle.kind === 'pdf' ? 'pdf' : 'html', observedAt: new Date().toISOString(), rule: 'sponge-import-v1' }))], assets, warnings: bundle.status === 'partial' || localized.missingImages ? ['partial'] : [], importedState: { namespace, sourceKey: entry.id, sourceHash, importerVersion: '1', notes: entry.notes, readingState: entry.readingState, privacy: entry.privacy } });
31
+ committed = true;
32
+ result = { ok: true, itemId: source.itemId };
33
+ }
34
+ catch (error) {
35
+ result = failure(attempt.attemptId, errorCode(error), 'storage');
36
+ }
37
+ if (!committed)
38
+ store.failAttempt(attempt, result.ok ? 'unclassified' : result.code);
39
+ store.settleAttempt(attempt);
40
+ return { ...result, cleanup: 'settled' };
41
+ }
42
+ /** Additive bounded batches. Every applied entry is reverified; source bundles are read-only. */
43
+ export async function importManifest(store, path, options) {
44
+ const { root, manifest, sha256 } = await readImportManifest(path), limit = options.limit ?? 20, offset = options.offset ?? 0;
45
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || !Number.isSafeInteger(offset) || offset < 0 || offset > manifest.entries.length)
46
+ throw Error('invalid-input');
47
+ const dryRun = options.dryRun !== false, report = { schemaVersion: 1, manifestSha256: sha256, namespace: manifest.namespace, dryRun, entries: [], nextOffset: offset + limit < manifest.entries.length ? offset + limit : null };
48
+ const verify = options.verify ?? releasedBundleVerifier(options.runtime, store.root, options.signal);
49
+ for (const entry of manifest.entries.slice(offset, offset + limit)) {
50
+ if (options.signal?.aborted)
51
+ throw Object.assign(Error('cancelled'), { code: 'cancelled' });
52
+ try {
53
+ const bundle = await inspectImportBundle(await importBundleRoot(root, entry), verify), sourceHash = hash(JSON.stringify([bundle.importHash, entry])), receipt = store.lookupImportReceipt(manifest.namespace, entry.id, sourceHash);
54
+ if (options.signal?.aborted)
55
+ throw Object.assign(Error('cancelled'), { code: 'cancelled' });
56
+ const base = { id: entry.id, sourceHash, historicalIntegrity: bundle.historicalIntegrity, privacy: entry.privacy };
57
+ if (receipt) {
58
+ report.entries.push({ ...base, status: 'duplicate', itemId: receipt.itemId });
59
+ continue;
60
+ }
61
+ if (dryRun) {
62
+ report.entries.push({ ...base, status: 'ready' });
63
+ continue;
64
+ }
65
+ const submission = store.submitSource({ kind: bundle.kind, sourceKey: hash(JSON.stringify([manifest.namespace, entry.id])), locator: bundle.sourceUrl, realm: 'import:' + manifest.namespace, idempotencyKey: 'import:' + hash(JSON.stringify([manifest.namespace, entry.id, sourceHash])), intent: 'refresh' });
66
+ const job = store.listJobs().find(row => row.id === submission.jobId);
67
+ if (job && ['failed', 'needs-auth', 'needs-permission', 'unsupported'].includes(String(job.state)))
68
+ store.retryJob(submission.jobId, { automatic: false });
69
+ const results = await drainQueue(store, { jobId: submission.jobId, signal: options.signal, handlers: { [bundle.kind]: context => promote(context, bundle, manifest.namespace, entry, sourceHash) } });
70
+ const result = results[0];
71
+ if (!result || !result.ok)
72
+ throw Object.assign(Error(result && !result.ok ? result.code : 'integrity'), { code: result && !result.ok ? result.code : 'integrity' });
73
+ report.entries.push({ ...base, status: 'imported', itemId: submission.itemId });
74
+ }
75
+ catch (error) {
76
+ const code = errorCode(error);
77
+ report.entries.push({ id: entry.id, status: code === 'unsupported' ? 'unsupported' : 'failed', code, privacy: entry.privacy });
78
+ if (code === 'cleanup-required' || code === 'owner-busy' || code === 'cancelled') {
79
+ report.nextOffset = offset + report.entries.length - 1;
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ return report;
85
+ }
@@ -0,0 +1,6 @@
1
+ import { supervisedRunner } from '../worker/supervisor.js';
2
+ /** Read-only verification still owns subprocesses, including during an import preview. */
3
+ export declare function withImportVerification<T>(libraryRoot: string, verify: (context: {
4
+ scratchRoot: string;
5
+ runProcess: ReturnType<typeof supervisedRunner>;
6
+ }) => Promise<T>): Promise<T>;
@@ -0,0 +1,35 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { rm } from 'node:fs/promises';
4
+ import { acquireWorkerCustody, proveAttemptCleanup } from '../worker/custody.js';
5
+ import { directory, writeJournal, readJournal, syncDir } from '../worker/journal.js';
6
+ import { supervisedRunner } from '../worker/supervisor.js';
7
+ const pending = () => Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
8
+ /** Read-only verification still owns subprocesses, including during an import preview. */
9
+ export async function withImportVerification(libraryRoot, verify) {
10
+ const custody = await acquireWorkerCustody(libraryRoot), attemptId = randomUUID();
11
+ let scratchRoot, unknown = false;
12
+ try {
13
+ const attempt = await directory(join(custody.directory, 'attempts', attemptId));
14
+ await writeJournal(join(attempt, 'import-verification.json'), { schemaVersion: 1, kind: 'import-verification', attemptId, ownerIncarnation: custody.incarnation });
15
+ scratchRoot = await directory(join(libraryRoot, 'staging', 'import-verification-' + attemptId));
16
+ await writeJournal(join(scratchRoot, 'ownership.json'), { schemaVersion: 1, kind: 'import-verification', requestId: attemptId, ownerIncarnation: custody.incarnation });
17
+ return await verify({ scratchRoot, runProcess: supervisedRunner(custody.directory, custody.incarnation, attemptId) });
18
+ }
19
+ catch (error) {
20
+ unknown = error?.code === 'cleanup-required' || (error instanceof Error && error.message === 'cleanup-required');
21
+ throw error;
22
+ }
23
+ finally {
24
+ if (unknown || !await proveAttemptCleanup(custody.directory, custody.incarnation, attemptId))
25
+ throw pending();
26
+ if (scratchRoot) {
27
+ const marker = await readJournal(join(scratchRoot, 'ownership.json'));
28
+ if (marker.schemaVersion !== 1 || marker.kind !== 'import-verification' || marker.requestId !== attemptId || marker.ownerIncarnation !== custody.incarnation)
29
+ throw pending();
30
+ await rm(scratchRoot, { recursive: true });
31
+ await syncDir(join(libraryRoot, 'staging'));
32
+ }
33
+ await custody.release();
34
+ }
35
+ }
@@ -0,0 +1,21 @@
1
+ import type { Asset, MetadataClaim } from '../../contracts/index.js';
2
+ import { type CaptureRuntime } from '../acquisition/runtime.js';
3
+ export interface ImportedBundle {
4
+ root: string;
5
+ kind: 'article' | 'pdf';
6
+ sourceUrl: string;
7
+ schemaVersion: number;
8
+ status: 'complete' | 'partial';
9
+ markdown: Asset;
10
+ assets: (Asset & {
11
+ relationship: 'image' | 'source-pdf';
12
+ })[];
13
+ metadata: MetadataClaim[];
14
+ manifestSha256: string;
15
+ historicalIntegrity: 'verified' | 'unavailable';
16
+ importHash: string;
17
+ }
18
+ export type BundleVerifier = (root: string, kind: 'article' | 'pdf') => Promise<unknown>;
19
+ export declare function releasedBundleVerifier(runtime: CaptureRuntime, libraryRoot: string, signal?: AbortSignal): BundleVerifier;
20
+ /** An import-time digest never upgrades a missing historical document digest. */
21
+ export declare function inspectImportBundle(root: string, verify: BundleVerifier): Promise<ImportedBundle>;
@@ -0,0 +1,97 @@
1
+ import { fileURLToPath, pathToFileURL } from 'node:url';
2
+ import { join } from 'node:path';
3
+ import { limits } from '../../contracts/index.js';
4
+ import { validateAsset } from '../../contracts/validate.js';
5
+ import { object, metadataFromMarkdown } from '../acquisition/normalize.js';
6
+ import { readConfined, hash } from '../acquisition/files.js';
7
+ import { withImportVerification } from './custody.js';
8
+ import { inspectCaptureRuntime } from '../acquisition/runtime.js';
9
+ import { articleMetadata } from '../metadata/article.js';
10
+ import { bundleBytes } from '../app/bundle-files.js';
11
+ export function releasedBundleVerifier(runtime, libraryRoot, signal) {
12
+ return async (root, kind) => {
13
+ const checked = await inspectCaptureRuntime(runtime.root, runtime.pins), helper = fileURLToPath(new URL(import.meta.url.endsWith('.ts') ? './verify-helper.ts' : './verify-helper.js', import.meta.url));
14
+ return withImportVerification(libraryRoot, async ({ scratchRoot, runProcess }) => {
15
+ const args = [helper, checked.root, root, kind];
16
+ const result = await runProcess(process.platform === 'darwin' ? '/usr/bin/sandbox-exec' : checked.bun, process.platform === 'darwin' ? ['-p', '(version 1) (allow default) (deny network*)', checked.bun, ...args] : args, { env: { PATH: '/usr/bin:/bin', HOME: checked.root, TMPDIR: scratchRoot, LANG: 'en_US.UTF-8' }, deadlineMs: 45000, maxOutputBytes: 65536, signal });
17
+ if (result.cleanup !== 'settled')
18
+ throw Error('cleanup-required');
19
+ if (result.stopped)
20
+ throw Error(result.stopped);
21
+ if (result.code !== 0)
22
+ throw Error('unsupported');
23
+ return JSON.parse(result.stdout);
24
+ });
25
+ };
26
+ }
27
+ function imageType(bytes) { if (bytes[0] === 255 && bytes[1] === 216)
28
+ return 'image/jpeg'; if (bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])))
29
+ return 'image/png'; if (bytes.toString('ascii', 0, 4) === 'RIFF' && bytes.toString('ascii', 8, 12) === 'WEBP')
30
+ return 'image/webp'; if (/^GIF8[79]a/.test(bytes.toString('ascii', 0, 6)))
31
+ return 'image/gif'; if (bytes.toString('ascii', 4, 12) === 'ftypavif')
32
+ return 'image/avif'; throw Error('unsupported'); }
33
+ /** An import-time digest never upgrades a missing historical document digest. */
34
+ export async function inspectImportBundle(root, verify) {
35
+ const manifestBytes = await readConfined(root, 'capture.json', limits.metadataBytes), manifest = object(JSON.parse(manifestBytes.toString('utf8'))), kind = manifest.kind === 'pdf' ? 'pdf' : 'article';
36
+ if (kind === 'pdf' ? manifest.schemaVersion !== 1 : ![1, 2, 3, 4].includes(Number(manifest.schemaVersion)))
37
+ throw Error('unsupported');
38
+ const result = object(await verify(root, kind)), inspection = object(result.inspection), document = object(kind === 'pdf' ? inspection.markdown : inspection.document);
39
+ if (inspection.schemaVersion !== manifest.schemaVersion || !['verified', 'unavailable'].includes(String(document.integrity)) || !Array.isArray(inspection.assets) || inspection.assets.length > limits.imagesPerItem)
40
+ throw Error('integrity');
41
+ if (result.ok !== true && (!Array.isArray(result.issues) || !result.issues.length || result.issues.some(raw => { const issue = object(raw); return !(issue.kind === 'document-integrity' && issue.path === document.path && document.integrity === 'unavailable' && (kind === 'article' || issue.integrity === 'unavailable')); })))
42
+ throw Error('integrity');
43
+ if (inspection.status !== undefined && inspection.status !== null && !['complete', 'partial'].includes(String(inspection.status)))
44
+ throw Error('unsupported');
45
+ const markdownBytes = await readConfined(root, String(document.path), limits.markdownBytes), markdown = { relativePath: String(document.path), bytes: markdownBytes.length, sha256: hash(markdownBytes), mediaType: 'text/markdown' };
46
+ validateAsset(markdown);
47
+ if (markdown.sha256 !== document.sha256 || markdown.bytes !== document.bytes)
48
+ throw Error('integrity');
49
+ let unavailable = document.integrity === 'unavailable';
50
+ const assets = [];
51
+ let total = 0;
52
+ for (const raw of inspection.assets) {
53
+ const asset = object(raw);
54
+ if (!['verified', 'unavailable'].includes(String(asset.integrity)))
55
+ throw Error('integrity');
56
+ unavailable ||= asset.integrity === 'unavailable';
57
+ const bytes = await readConfined(root, String(asset.path), limits.imageBytes);
58
+ total += bytes.length;
59
+ if (total > 500 * 1024 ** 2)
60
+ throw Error('limit-exceeded');
61
+ const value = { relativePath: String(asset.path), bytes: bytes.length, sha256: hash(bytes), mediaType: imageType(bytes), relationship: 'image' };
62
+ validateAsset(value);
63
+ const expectedHash = kind === 'pdf' ? asset.sha256 : asset.expectedSha256, expectedBytes = kind === 'pdf' ? asset.bytes : asset.expectedBytes;
64
+ if (asset.integrity === 'verified' && (expectedHash !== value.sha256 || expectedBytes !== value.bytes))
65
+ throw Error('integrity');
66
+ assets.push(value);
67
+ }
68
+ if (kind === 'pdf') {
69
+ const source = object(inspection.source);
70
+ if (source.integrity !== 'verified')
71
+ throw Error('integrity');
72
+ const asset = { relativePath: String(source.path), sha256: String(source.sha256), bytes: Number(source.bytes), mediaType: 'application/pdf', relationship: 'source-pdf' };
73
+ validateAsset(asset);
74
+ for await (const _ of bundleBytes(root, asset)) { }
75
+ assets.push(asset);
76
+ }
77
+ if (assets.length !== new Set(assets.map(a => a.relativePath)).size)
78
+ throw Error('integrity');
79
+ const sourceUrl = kind === 'pdf' ? (object(manifest.source).requestedUrl ?? pathToFileURL(join(root, String(object(inspection.source).path))).href) : inspection.sourceUrl;
80
+ if (typeof sourceUrl !== 'string' || sourceUrl.length > 16384 || !['http:', 'https:', 'file:'].includes(new URL(sourceUrl).protocol))
81
+ throw Error('integrity');
82
+ const observedAt = typeof manifest.capturedAt === 'string' && Number.isFinite(Date.parse(manifest.capturedAt)) ? manifest.capturedAt : '1970-01-01T00:00:00.000Z';
83
+ const metadata = kind === 'article' ? (manifest.metadata === undefined ? metadataFromMarkdown(markdownBytes.toString('utf8'), observedAt) : articleMetadata(manifest.metadata, observedAt)) : [];
84
+ if (kind === 'pdf') {
85
+ const document = object(manifest.document ?? {});
86
+ for (const field of ['title', 'author', 'subject', 'creator', 'producer', 'createdAt', 'modifiedAt']) {
87
+ const value = document[field];
88
+ if (typeof value === 'string' && value && Buffer.byteLength(value) <= 16384)
89
+ metadata.push({ field, value, source: 'pdf', observedAt, rule: 'wordcell-pdf-v1', selected: true });
90
+ }
91
+ }
92
+ metadata.push({ field: 'historicalIntegrity', value: unavailable ? 'unavailable' : 'verified', source: kind === 'pdf' ? 'pdf' : 'html', observedAt, rule: 'sponge-import-v1' });
93
+ if (hash(await readConfined(root, 'capture.json', limits.metadataBytes)) !== hash(manifestBytes))
94
+ throw Error('integrity');
95
+ const manifestSha256 = hash(manifestBytes), importHash = hash(JSON.stringify([manifestSha256, markdown.sha256, ...assets.map(a => [a.relativePath, a.sha256, a.bytes])]));
96
+ return { root, kind, sourceUrl, schemaVersion: Number(manifest.schemaVersion), status: unavailable || inspection.status !== 'complete' ? 'partial' : 'complete', markdown, assets, metadata, manifestSha256, historicalIntegrity: unavailable ? 'unavailable' : 'verified', importHash };
97
+ }
@@ -0,0 +1,22 @@
1
+ export interface ImportEntry {
2
+ id: string;
3
+ bundle: string;
4
+ privacy: 'private' | 'published';
5
+ notes: {
6
+ id: string;
7
+ body: string;
8
+ }[];
9
+ readingState?: Record<string, unknown>;
10
+ }
11
+ export interface ImportManifest {
12
+ schemaVersion: 1;
13
+ namespace: string;
14
+ entries: ImportEntry[];
15
+ }
16
+ /** Explicit local source manifest; no recursive discovery or implicit public selection. */
17
+ export declare function readImportManifest(path: string): Promise<{
18
+ root: string;
19
+ manifest: ImportManifest;
20
+ sha256: string;
21
+ }>;
22
+ export declare function importBundleRoot(root: string, entry: ImportEntry): Promise<string>;
@@ -0,0 +1,43 @@
1
+ import { dirname, isAbsolute, join } from 'node:path';
2
+ import { realpath, lstat } from 'node:fs/promises';
3
+ import { readConfined, hash } from '../acquisition/files.js';
4
+ import { object } from '../acquisition/normalize.js';
5
+ function bounded(value, max) { if (typeof value !== 'string' || !value || Buffer.byteLength(value) > max || value.includes('\0'))
6
+ throw Error('invalid-input'); return value; }
7
+ function keys(value, allowed) { if (Object.keys(value).some(k => !allowed.includes(k)))
8
+ throw Error('invalid-input'); }
9
+ /** Explicit local source manifest; no recursive discovery or implicit public selection. */
10
+ export async function readImportManifest(path) {
11
+ if (!isAbsolute(path) || await realpath(path) !== path)
12
+ throw Error('invalid-input');
13
+ const root = dirname(path), bytes = await readConfined(root, path.slice(root.length + 1), 1024 ** 2), raw = object(JSON.parse(bytes.toString('utf8')));
14
+ keys(raw, ['schemaVersion', 'namespace', 'entries']);
15
+ if (raw.schemaVersion !== 1 || !Array.isArray(raw.entries) || raw.entries.length > 1000)
16
+ throw Error('unsupported');
17
+ const namespace = bounded(raw.namespace, 128);
18
+ if (!/^[A-Za-z0-9_.-]+$/.test(namespace))
19
+ throw Error('invalid-input');
20
+ const ids = new Set();
21
+ const entries = raw.entries.map(value => {
22
+ const entry = object(value);
23
+ keys(entry, ['id', 'bundle', 'privacy', 'notes', 'readingState']);
24
+ const id = bounded(entry.id, 256), bundle = bounded(entry.bundle, 4096);
25
+ if (ids.has(id) || isAbsolute(bundle) || bundle.includes('\\') || bundle.split('/').some(p => !p || p === '.' || p === '..') || !['private', 'published'].includes(String(entry.privacy)))
26
+ throw Error('invalid-input');
27
+ ids.add(id);
28
+ if (entry.notes !== undefined && !Array.isArray(entry.notes))
29
+ throw Error('invalid-input');
30
+ const noteIds = new Set();
31
+ const notes = (entry.notes ?? []).map(value => { const note = object(value); keys(note, ['id', 'body']); const id = bounded(note.id, 256), body = bounded(note.body, 65536); if (noteIds.has(id))
32
+ throw Error('invalid-input'); noteIds.add(id); return { id, body }; });
33
+ if (notes.length > 100)
34
+ throw Error('limit-exceeded');
35
+ const readingState = entry.readingState === undefined ? undefined : object(entry.readingState);
36
+ if (readingState && Buffer.byteLength(JSON.stringify(readingState)) > 65536)
37
+ throw Error('limit-exceeded');
38
+ return { id, bundle, privacy: entry.privacy, notes, ...(readingState ? { readingState } : {}) };
39
+ });
40
+ return { root, manifest: { schemaVersion: 1, namespace, entries }, sha256: hash(bytes) };
41
+ }
42
+ export async function importBundleRoot(root, entry) { const path = join(root, entry.bundle); if (await realpath(path) !== path || (await lstat(path)).isSymbolicLink() || !(await lstat(path)).isDirectory())
43
+ throw Error('integrity'); return path; }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ /** Only released Wordcell public verifiers, with no source or document text in output. */
2
+ const runtime = process.argv[2], bundle = process.argv[3], kind = process.argv[4];
3
+ try {
4
+ let result;
5
+ if (kind === 'pdf') {
6
+ const { verifyPdfCaptureBundle } = await import(import.meta.resolve('@hraness/wordcell/pdf', `${runtime}/package.json`));
7
+ result = await verifyPdfCaptureBundle(bundle, { maxDocumentBytes: 16 * 1024 ** 2, maxSourceBytes: 100 * 1024 ** 2, maxVerifiedAssets: 200, maxAssetBytes: 20 * 1024 ** 2, maxTotalAssetBytes: 500 * 1024 ** 2, maxVerificationMs: 30000 });
8
+ const { markdown, ...document } = result.inspection.markdown;
9
+ result = { ...result, inspection: { ...result.inspection, markdown: document } };
10
+ }
11
+ else {
12
+ const { verifyCaptureBundle } = await import(import.meta.resolve('@hraness/wordcell/clip/bundle-reader', `${runtime}/package.json`));
13
+ result = await verifyCaptureBundle(bundle, { verifyAssets: true, maxDocumentBytes: 16 * 1024 ** 2, maxVerifiedAssets: 200, maxAssetBytes: 20 * 1024 ** 2, maxTotalAssetBytes: 500 * 1024 ** 2, maxAssetVerificationMs: 30000 });
14
+ const { markdown, ...document } = result.inspection.document;
15
+ const { metadata, ...inspection } = result.inspection;
16
+ result = { ...result, inspection: { ...inspection, document } };
17
+ }
18
+ const output = JSON.stringify(result);
19
+ if (Buffer.byteLength(output) > 65536)
20
+ throw Error('limit');
21
+ process.stdout.write(output);
22
+ }
23
+ catch {
24
+ process.stdout.write(JSON.stringify({ ok: false, code: 'unsupported' }));
25
+ process.exitCode = 1;
26
+ }
27
+ export {};
@@ -0,0 +1,10 @@
1
+ import { runOwned } from '../acquisition/process.js';
2
+ /** Derived pixels are disposable; source image blobs remain unchanged. */
3
+ export declare function deriveEmbeddingImage(bytes: Uint8Array, options: {
4
+ scratchRoot: string;
5
+ runProcess?: typeof runOwned;
6
+ signal?: AbortSignal;
7
+ }): Promise<{
8
+ png: Buffer;
9
+ sha256: string;
10
+ }>;
@@ -0,0 +1,34 @@
1
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { privateRoot, readConfined, hash } from '../acquisition/files.js';
4
+ import { runOwned } from '../acquisition/process.js';
5
+ import { encodeEmbeddingRequest, EmbeddingError } from '../inference/embedding/index.js';
6
+ /** Derived pixels are disposable; source image blobs remain unchanged. */
7
+ export async function deriveEmbeddingImage(bytes, options) {
8
+ if (process.platform !== 'darwin' || process.arch !== 'arm64')
9
+ throw new EmbeddingError('unsupported');
10
+ if (!bytes.length || bytes.length > 20 * 1024 ** 2)
11
+ throw new EmbeddingError('limit-exceeded');
12
+ const root = await privateRoot(options.scratchRoot), scratch = await mkdtemp(join(root, 'image-derive-'));
13
+ let settled = true;
14
+ try {
15
+ const input = join(scratch, 'source'), output = join(scratch, 'image.png');
16
+ await writeFile(input, bytes, { mode: 0o600, flag: 'wx' });
17
+ const run = async (args) => { settled = false; const result = await (options.runProcess ?? runOwned)('/usr/bin/sandbox-exec', ['-p', '(version 1) (allow default) (deny network*)', '/usr/bin/sips', ...args], { env: { PATH: '/usr/bin:/bin', HOME: scratch, TMPDIR: scratch, LANG: 'C' }, deadlineMs: 30000, maxOutputBytes: 65536, signal: options.signal }); settled = result.cleanup === 'settled'; if (!settled)
18
+ throw new EmbeddingError('cleanup-required'); if (result.stopped)
19
+ throw new EmbeddingError(result.stopped); if (result.code !== 0)
20
+ throw new EmbeddingError('unsupported'); return result.stdout; };
21
+ const metadata = await run(['-g', 'pixelWidth', '-g', 'pixelHeight', input]);
22
+ const width = Number(metadata.match(/pixelWidth:\s*(\d+)/)?.[1]), height = Number(metadata.match(/pixelHeight:\s*(\d+)/)?.[1]);
23
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1 || width * height > 40_000_000)
24
+ throw new EmbeddingError('limit-exceeded');
25
+ await run(['--resampleHeightWidthMax', '512', '--setProperty', 'format', 'png', input, '--out', output]);
26
+ const png = await readConfined(scratch, 'image.png', 2 * 1024 ** 2);
27
+ encodeEmbeddingRequest([{ kind: 'image', png }]);
28
+ return { png, sha256: hash(png) };
29
+ }
30
+ finally {
31
+ if (settled)
32
+ await rm(scratch, { recursive: true, force: true });
33
+ }
34
+ }
@@ -0,0 +1,53 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ export interface IndexInput {
3
+ sourceId: string;
4
+ revisionId: string;
5
+ itemId: string;
6
+ derivationHash: string;
7
+ kind: 'document' | 'image';
8
+ text?: string;
9
+ representationId?: string;
10
+ ordinal?: number;
11
+ assetSha256?: string;
12
+ }
13
+ export interface GenerationStatus {
14
+ id: string;
15
+ fingerprint: string;
16
+ state: string;
17
+ revisionEpoch: number;
18
+ expected: number;
19
+ completed: number;
20
+ profileId: string;
21
+ }
22
+ export declare class IndexError extends Error {
23
+ code: 'invalid-index' | 'stale-index' | 'incomplete-index' | 'cancelled-index';
24
+ constructor(code: 'invalid-index' | 'stale-index' | 'incomplete-index' | 'cancelled-index');
25
+ }
26
+ /** Unicode-safe chunks leave room for the fixed document prompt within 8192 UTF-8 bytes. */
27
+ export declare function chunkDocument(text: string): string[];
28
+ /** Identity must already be the verified embedder fingerprint. Existing identities are immutable. */
29
+ export declare function createGeneration(db: DatabaseSync, options: {
30
+ profileId: string;
31
+ fingerprint: string;
32
+ identity: Record<string, string | number>;
33
+ dimensions: number;
34
+ }): GenerationStatus;
35
+ export declare function generationStatus(db: DatabaseSync, id: string): GenerationStatus;
36
+ export declare function pendingInputs(db: DatabaseSync, id: string, limit?: number): IndexInput[];
37
+ export declare function writeBatch(db: DatabaseSync, id: string, fingerprint: string, batch: readonly {
38
+ sourceId: string;
39
+ revisionId: string;
40
+ derivationHash: string;
41
+ inputHash: string;
42
+ vector: readonly number[];
43
+ }[]): void;
44
+ export declare function activateGeneration(db: DatabaseSync, id: string): GenerationStatus;
45
+ export declare function cancelGeneration(db: DatabaseSync, id: string): void;
46
+ /** Read-side join removes deleted/refreshed revisions even when an old generation stays active. */
47
+ export declare function activeVectors(db: DatabaseSync, profileId: string): {
48
+ fingerprint: string;
49
+ generationId: string;
50
+ source: IndexInput;
51
+ vector: number[];
52
+ }[];
53
+ export declare const rollbackGeneration: typeof activateGeneration;