@hraness/sponge 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import { captureLockfile } from './lock.js';
2
2
  /** Immutable released media/PDF capability pins; installed qualification is recorded separately. */
3
3
  export const defaultCapturePins = {
4
- id: 'capture-ghostget-0.18.94-bun-1.3.14-darwin-arm64',
5
- ghostget: { version: '0.18.94', url: 'https://github.com/hraness/ghostget/releases/download/v0.18.94/hraness-ghostget-0.18.94.tgz', bytes: 12379758, sha256: '1409271320b4b35467d04a085a39e6c958d065c7f79c4b1092b2854861c85bbb' },
4
+ id: 'capture-ghostget-0.18.99-bun-1.3.14-darwin-arm64',
5
+ ghostget: { version: '0.18.99', url: 'https://github.com/hraness/ghostget/releases/download/v0.18.99/hraness-ghostget-0.18.99.tgz', bytes: 12470970, sha256: '91d226b2d69884f7119f11ae9316f37134c3da7e127e91ae210c982eced92530' },
6
6
  wordcell: { version: '0.27.0', url: 'https://github.com/hraness/wordcell/releases/download/v0.27.0/hraness-wordcell-0.27.0.tgz', bytes: 1465897, sha256: 'df47868147cda46d741895d3cd1ddcbec14a563b341aa09052545be7696c9262' },
7
7
  bun: { version: '1.3.14', url: 'https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-darwin-aarch64.zip', bytes: 23586433, sha256: 'd8b96221828ad6f97ac7ac0ab7e95872341af763001e8803e8267652c2652620', executableSha256: 'e0c90ec15d33363e6b70713d56bc3b2c7585c17f40a0fe0f8fd9305901d4e233', archiveExecutable: 'bun-darwin-aarch64/bun' },
8
- lockfile: captureLockfile, lockSha256: "c18916866933f1f59e7dd2067966477cc576ada21b9e6965ba5395035ad54b88",
8
+ lockfile: captureLockfile, lockSha256: "1dec6b6e96f6e2dc7ddecafb087ca5599b438a7d282e7b7d88aca5ebad1e9e6c",
9
9
  };
@@ -32,6 +32,7 @@ export interface SetupPlan {
32
32
  installationBudgetBytes: number;
33
33
  stagingBudgetBytes: number;
34
34
  installationBudgetKind: 'monitored';
35
+ installationDeadlineMs: number;
35
36
  dependencyDownloads: 'frozen-lock';
36
37
  requiredFreeBytes: number;
37
38
  capabilities: string[];
@@ -14,6 +14,8 @@ export class CaptureSetupError extends Error {
14
14
  // Retained inventory excludes the isolated Bun cache and temporary Bun zip.
15
15
  const budget = 1024 ** 3;
16
16
  const stagingBudget = 2 * budget;
17
+ // The frozen dependency install measured 460 s on a slow link; 300 s aborted valid setups.
18
+ const installDeadlineMs = 30 * 60 * 1000;
17
19
  function checkPins(pins) {
18
20
  if (!/^[a-z0-9][a-z0-9.-]{0,99}$/.test(pins.id) || hash(pins.lockfile) !== pins.lockSha256)
19
21
  throw new CaptureSetupError('invalid-input');
@@ -34,7 +36,7 @@ function checkPins(pins) {
34
36
  export function captureSetupPlan(pins) {
35
37
  checkPins(pins);
36
38
  const artifacts = [pins.bun, pins.ghostget, pins.wordcell], downloadBytes = artifacts.reduce((n, a) => n + a.bytes, 0);
37
- return { artifacts, downloadBytes, installationBudgetBytes: budget, stagingBudgetBytes: stagingBudget, installationBudgetKind: 'monitored', dependencyDownloads: 'frozen-lock', requiredFreeBytes: 2 * 1024 ** 3 + budget + stagingBudget + downloadBytes, capabilities: ['article-http', 'selected-ghostget-account', 'selected-attached-browser', 'verified-offline-images'] };
39
+ return { artifacts, downloadBytes, installationBudgetBytes: budget, stagingBudgetBytes: stagingBudget, installationBudgetKind: 'monitored', installationDeadlineMs: installDeadlineMs, dependencyDownloads: 'frozen-lock', requiredFreeBytes: 2 * 1024 ** 3 + budget + stagingBudget + downloadBytes, capabilities: ['article-http', 'selected-ghostget-account', 'selected-attached-browser', 'verified-offline-images'] };
38
40
  }
39
41
  async function download(pin, path, signal) {
40
42
  const boundedSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(120000)]) : AbortSignal.timeout(120000);
@@ -158,7 +160,7 @@ async function provisionRuntime(options) {
158
160
  await download(pins.wordcell, join(stage, `hraness-wordcell-${pins.wordcell.version}.tgz`), controller.signal);
159
161
  await writeFile(join(stage, 'package.json'), JSON.stringify({ private: true, dependencies: { '@hraness/ghostget': `./hraness-ghostget-${pins.ghostget.version}.tgz` } }), { mode: 0o600 });
160
162
  await writeFile(join(stage, 'bun.lock'), pins.lockfile, { mode: 0o600 });
161
- const install = await runOwned(bun, ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: stage, env: isolatedEnvironment(home, home, home), signal: controller.signal, deadlineMs: 300000, maxOutputBytes: 65536 });
163
+ const install = await runOwned(bun, ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: stage, env: isolatedEnvironment(home, home, home), signal: controller.signal, deadlineMs: installDeadlineMs, maxOutputBytes: 65536 });
162
164
  cleanup = install.cleanup;
163
165
  if (install.code !== 0 || install.stopped || cleanup !== 'settled')
164
166
  throw new CaptureSetupError(limitExceeded ? 'limit-exceeded' : install.stopped ?? 'unavailable');
@@ -124,7 +124,7 @@ export async function main(argv, version) {
124
124
  }
125
125
  if (args.command === 'setup') {
126
126
  const plan = captureSetupPlan(defaultCapturePins);
127
- process.stderr.write(`Capture setup: ${plan.downloadBytes} pinned artifact bytes, plus frozen dependency downloads. Temporary installation limit: ${plan.stagingBudgetBytes} bytes; retained runtime limit: ${plan.installationBudgetBytes} bytes. ${plan.requiredFreeBytes} free bytes required.\n`);
127
+ process.stderr.write(`Capture setup: ${plan.downloadBytes} pinned artifact bytes, plus frozen dependency downloads. Temporary installation limit: ${plan.stagingBudgetBytes} bytes; retained runtime limit: ${plan.installationBudgetBytes} bytes; dependency installation deadline: ${plan.installationDeadlineMs / 60000} minutes. ${plan.requiredFreeBytes} free bytes required.\n`);
128
128
  await provisionCapture({ runtimeRoot: join(store.root, 'runtime', 'capture'), pins: defaultCapturePins, signal: controller.signal });
129
129
  emit({ capture: 'ready' }, 'Capture ready');
130
130
  return 0;
@@ -23,7 +23,7 @@ export declare class IndexError extends Error {
23
23
  code: 'invalid-index' | 'stale-index' | 'incomplete-index' | 'cancelled-index';
24
24
  constructor(code: 'invalid-index' | 'stale-index' | 'incomplete-index' | 'cancelled-index');
25
25
  }
26
- /** Unicode-safe chunks leave room for the fixed document prompt within 8192 UTF-8 bytes. */
26
+ /** Unicode-safe chunks fit the fixed document prompt within the embedding text budget, preferring whitespace breaks. */
27
27
  export declare function chunkDocument(text: string): string[];
28
28
  /** Identity must already be the verified embedder fingerprint. Existing identities are immutable. */
29
29
  export declare function createGeneration(db: DatabaseSync, options: {
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { spaceFingerprint, encodeEmbeddingRequest } from '../inference/embedding/index.js';
2
+ import { spaceFingerprint, encodeEmbeddingRequest, embeddingTextBytes } from '../inference/embedding/index.js';
3
3
  export class IndexError extends Error {
4
4
  code;
5
5
  constructor(code) {
@@ -21,17 +21,25 @@ catch (e) {
21
21
  } }
22
22
  function epoch(db) { return Number(db.prepare('SELECT revision_epoch FROM library_info WHERE id=1').get()?.revision_epoch); }
23
23
  function revisions(db) { return db.prepare('SELECT id AS itemId,current_revision AS revisionId FROM items WHERE deleted_at IS NULL AND current_revision IS NOT NULL ORDER BY id').all(); }
24
- /** Unicode-safe chunks leave room for the fixed document prompt within 8192 UTF-8 bytes. */
25
- export function chunkDocument(text) { const chunks = []; let part = '', size = 0; for (const char of text) {
24
+ const chunkBytes = embeddingTextBytes - Buffer.byteLength('title: none | text: ');
25
+ /** Unicode-safe chunks fit the fixed document prompt within the embedding text budget, preferring whitespace breaks. */
26
+ export function chunkDocument(text) { const chunks = []; let part = '', size = 0, cut = -1, cutSize = 0; for (const char of text) {
26
27
  const bytes = Buffer.byteLength(char);
27
- if (size + bytes > 7600) {
28
- if (part.trim())
29
- chunks.push(part);
30
- part = '';
31
- size = 0;
28
+ if (size + bytes > chunkBytes) {
29
+ const at = cut > 0 && cutSize * 2 >= chunkBytes ? cut : part.length;
30
+ const head = part.slice(0, at);
31
+ if (head.trim())
32
+ chunks.push(head);
33
+ part = part.slice(at);
34
+ size = Buffer.byteLength(part);
35
+ cut = -1;
32
36
  }
33
37
  part += char;
34
38
  size += bytes;
39
+ if (/\s/u.test(char)) {
40
+ cut = part.length;
41
+ cutSize = size;
42
+ }
35
43
  } if (part.trim())
36
44
  chunks.push(part); return chunks; }
37
45
  function snapshot(db) {
@@ -41,7 +49,7 @@ function snapshot(db) {
41
49
  for (const doc of docs) {
42
50
  const chunks = chunkDocument(String(doc.markdown));
43
51
  for (let ordinal = 0; ordinal < chunks.length; ordinal++) {
44
- const text = chunks[ordinal], derivationHash = hash(JSON.stringify(['sponge-chunk-v1', String(doc.id), ordinal, text]));
52
+ const text = chunks[ordinal], derivationHash = hash(JSON.stringify(['sponge-chunk-v2', String(doc.id), ordinal, text]));
45
53
  inputs.push({ ...r, sourceId: JSON.stringify([r.revisionId, 'document', String(doc.id), ordinal]), kind: 'document', representationId: String(doc.id), ordinal, derivationHash });
46
54
  }
47
55
  }
@@ -73,7 +81,7 @@ export function pendingInputs(db, id, limit = 16) { if (!Number.isSafeInteger(li
73
81
  fail('cancelled-index'); if (row.state !== 'building')
74
82
  return []; const done = new Set(db.prepare('SELECT source_id FROM vectors WHERE generation_id=? AND input_hash IS NOT NULL').all(id).map(r => String(r.source_id))); return coverage.inputs.filter(i => !done.has(i.sourceId)).slice(0, limit).map(input => { if (input.kind === 'image')
75
83
  return input; const doc = db.prepare('SELECT markdown FROM representations WHERE id=? AND revision_id=?').get(input.representationId, input.revisionId); if (!doc || typeof doc.markdown !== 'string')
76
- return fail(); const text = chunkDocument(doc.markdown)[input.ordinal]; if (text === undefined || hash(JSON.stringify(['sponge-chunk-v1', input.representationId, input.ordinal, text])) !== input.derivationHash)
84
+ return fail(); const text = chunkDocument(doc.markdown)[input.ordinal]; if (text === undefined || hash(JSON.stringify(['sponge-chunk-v2', input.representationId, input.ordinal, text])) !== input.derivationHash)
77
85
  return fail(); return { ...input, text }; }); }
78
86
  export function writeBatch(db, id, fingerprint, batch) {
79
87
  if (!batch.length || batch.length > 256)
@@ -9,7 +9,10 @@ export declare const embeddingIdentity: {
9
9
  readonly prompt: "search-result-query-title-none-v1";
10
10
  readonly protocol: 1;
11
11
  readonly backend: "cpu-2threads-f32";
12
+ readonly textBudget: "utf8-1016-v1";
12
13
  };
14
+ /** The model's largest text signature is 1024 tokens; byte-fallback tokenization never yields more tokens than UTF-8 bytes. */
15
+ export declare const embeddingTextBytes = 1016;
13
16
  export type EmbeddingInput = {
14
17
  kind: 'query' | 'document';
15
18
  text: string;
@@ -3,7 +3,9 @@ import { constants } from 'node:fs';
3
3
  import { open, mkdtemp, chmod, writeFile, rm, realpath } from 'node:fs/promises';
4
4
  import { dirname, join, isAbsolute } from 'node:path';
5
5
  import { runOwned } from '../../acquisition/process.js';
6
- export const embeddingIdentity = { modelSha256: '92dcbea108899e5d6e30d919b0744f90d9967e80c67a4ab5503ac16d54f62eb0', librarySha256: '9526db9ee35e7eb4a5a58ac1e53330032f22a13890bd4ee01818fb380a57a362', runtime: 'LiteRT-LM@0.18.0', dimensions: 768, normalization: 'unit-l2', preprocessing: 'sips-max512-png-litert-v1', prompt: 'search-result-query-title-none-v1', protocol: 1, backend: 'cpu-2threads-f32' };
6
+ export const embeddingIdentity = { modelSha256: '92dcbea108899e5d6e30d919b0744f90d9967e80c67a4ab5503ac16d54f62eb0', librarySha256: '9526db9ee35e7eb4a5a58ac1e53330032f22a13890bd4ee01818fb380a57a362', runtime: 'LiteRT-LM@0.18.0', dimensions: 768, normalization: 'unit-l2', preprocessing: 'sips-max512-png-litert-v1', prompt: 'search-result-query-title-none-v1', protocol: 1, backend: 'cpu-2threads-f32', textBudget: 'utf8-1016-v1' };
7
+ /** The model's largest text signature is 1024 tokens; byte-fallback tokenization never yields more tokens than UTF-8 bytes. */
8
+ export const embeddingTextBytes = 1016;
7
9
  export class EmbeddingError extends Error {
8
10
  code;
9
11
  constructor(code) {
@@ -42,7 +44,7 @@ export function encodeEmbeddingRequest(inputs) {
42
44
  fail();
43
45
  payload = Buffer.from((input.kind === 'query' ? 'task: search result | query: ' : 'title: none | text: ') + input.text);
44
46
  kind = 1;
45
- if (payload.length > 8192)
47
+ if (payload.length > embeddingTextBytes)
46
48
  fail('limit-exceeded');
47
49
  }
48
50
  const header = Buffer.alloc(5);
@@ -27,14 +27,14 @@
27
27
  }
28
28
  ],
29
29
  "build": {
30
- "asrSourceSha256": "945667d78c598c30293f666d6605c9af26818b8d313c2219ac95985807c00ffc",
30
+ "asrSourceSha256": "2f8ad0eec7e46e259cba34dd3a37b09a673b553f60a63a37e54c91138db621bc",
31
31
  "clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
32
- "embeddingSourceSha256": "e31b9a70865c3c7195d201233a2a240fc7ef2aa40b4ae63291d1277262456c69",
32
+ "embeddingSourceSha256": "cbeef73ab8e45c4352444ff0c7c74f3850f3a249ca54f86c8ed89461013ecc70",
33
33
  "swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
34
34
  },
35
35
  "minimumMacOS": "26.0",
36
- "packageVersion": "0.2.0",
36
+ "packageVersion": "0.2.2",
37
37
  "platform": "darwin",
38
38
  "schemaVersion": 1,
39
- "sourceCommit": "439d0bd72f834a26a248ce2e9fee1a6a6bffaf57"
39
+ "sourceCommit": "3e5d95f573c57ed0e3e2a2d2c496d96258bb68dd"
40
40
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hraness/sponge",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A local library for captured articles and verified offline assets.",
5
5
  "repository": {
6
6
  "type": "git",