@hraness/sponge 0.2.4 → 0.2.6
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.
- package/README.md +54 -0
- package/dist/migrations/005.d.ts +3 -0
- package/dist/migrations/005.js +19 -0
- package/dist/migrations/006.d.ts +3 -0
- package/dist/migrations/006.js +9 -0
- package/dist/migrations/007.d.ts +3 -0
- package/dist/migrations/007.js +10 -0
- package/dist/src/annotations/quote.d.ts +32 -0
- package/dist/src/annotations/quote.js +135 -0
- package/dist/src/annotations/store.d.ts +50 -0
- package/dist/src/annotations/store.js +89 -0
- package/dist/src/app/source.d.ts +2 -0
- package/dist/src/app/source.js +13 -17
- package/dist/src/cli/arguments.js +24 -7
- package/dist/src/cli/digest.d.ts +10 -0
- package/dist/src/cli/digest.js +63 -0
- package/dist/src/cli/import.d.ts +11 -0
- package/dist/src/cli/import.js +10 -0
- package/dist/src/cli/indexing.d.ts +3 -0
- package/dist/src/cli/indexing.js +5 -2
- package/dist/src/cli/main.js +94 -8
- package/dist/src/cli/runtimes.d.ts +10 -0
- package/dist/src/cli/runtimes.js +32 -0
- package/dist/src/digest/editorial.d.ts +54 -0
- package/dist/src/digest/editorial.js +87 -0
- package/dist/src/digest/endpoint.d.ts +20 -0
- package/dist/src/digest/endpoint.js +103 -0
- package/dist/src/digest/pipeline.d.ts +148 -0
- package/dist/src/digest/pipeline.js +399 -0
- package/dist/src/digest/schema.d.ts +126 -0
- package/dist/src/digest/schema.js +138 -0
- package/dist/src/digest/store.d.ts +57 -0
- package/dist/src/digest/store.js +79 -0
- package/dist/src/identity/query-policy.d.ts +21 -0
- package/dist/src/identity/query-policy.js +55 -0
- package/dist/src/identity/uri.d.ts +30 -0
- package/dist/src/identity/uri.js +94 -0
- package/dist/src/identity/visibility.d.ts +24 -0
- package/dist/src/identity/visibility.js +75 -0
- package/dist/src/importers/apply.d.ts +10 -0
- package/dist/src/importers/apply.js +25 -22
- package/dist/src/importers/jungle-digest.d.ts +107 -0
- package/dist/src/importers/jungle-digest.js +196 -0
- package/dist/src/importers/jungle-store.d.ts +45 -0
- package/dist/src/importers/jungle-store.js +96 -0
- package/dist/src/importers/jungle.d.ts +70 -0
- package/dist/src/importers/jungle.js +142 -0
- package/dist/src/indexing/index.d.ts +7 -0
- package/dist/src/indexing/index.js +31 -8
- package/dist/src/indexing/prune-run.d.ts +22 -0
- package/dist/src/indexing/prune-run.js +67 -0
- package/dist/src/indexing/prune.d.ts +30 -0
- package/dist/src/indexing/prune.js +95 -0
- package/dist/src/indexing/runner.js +5 -5
- package/dist/src/inference/embedding/index.d.ts +25 -0
- package/dist/src/inference/embedding/index.js +11 -0
- package/dist/src/models/artifacts.js +175 -33
- package/dist/src/portability/backup.js +3 -1
- package/dist/src/portability/database.js +3 -0
- package/dist/src/publish/component.d.ts +8 -0
- package/dist/src/publish/component.js +114 -0
- package/dist/src/publish/feed.d.ts +24 -0
- package/dist/src/publish/feed.js +77 -0
- package/dist/src/publish/identity.d.ts +6 -0
- package/dist/src/publish/identity.js +68 -0
- package/dist/src/publish/index.d.ts +34 -0
- package/dist/src/publish/index.js +137 -0
- package/dist/src/publish/projection.d.ts +29 -0
- package/dist/src/publish/projection.js +43 -0
- package/dist/src/search/hybrid.js +2 -2
- package/dist/src/search/lexical.d.ts +11 -0
- package/dist/src/search/lexical.js +6 -0
- package/dist/src/search/query.d.ts +7 -3
- package/dist/src/search/query.js +52 -33
- package/dist/src/search/vector.d.ts +1 -1
- package/dist/src/search/vector.js +6 -5
- package/dist/src/store/changes.d.ts +42 -0
- package/dist/src/store/changes.js +178 -0
- package/dist/src/store/foundation.d.ts +7 -0
- package/dist/src/store/foundation.js +7 -0
- package/dist/src/store/index.d.ts +69 -0
- package/dist/src/store/index.js +134 -51
- package/dist/src/worker/runtime-gc.d.ts +30 -0
- package/dist/src/worker/runtime-gc.js +296 -0
- package/native/runtime-manifest.json +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Sync and publishing foundation, split by concern; this barrel keeps existing imports working. See docs/decisions/sync-and-publishing.md. */
|
|
2
|
+
export { applyChange, append, listChanges, mutate, seedChanges, setReviewPath, sourceRefId, maxChangeBytes } from './changes.js';
|
|
3
|
+
export { backfillUris, itemUris, itemsForUri, setVisibility, syncUris, visibility } from '../identity/visibility.js';
|
|
4
|
+
export { addComment, addHighlight, deleteAnnotation, importNote, listAnnotations, markdownFor, readAnnotation, setReadingState, updateAnnotation } from '../annotations/store.js';
|
|
5
|
+
export { acceptedDigest, canonicalDigest, currentDigest, derivationId, findDigest, putDigest, readDigest, reviewDigest } from '../digest/store.js';
|
|
6
|
+
export { currentEditorial, listEditorials, publicEditorial, putEditorial, readEditorial, reviewEditorial } from '../digest/editorial.js';
|
|
7
|
+
export { publicProjection } from '../publish/projection.js';
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import * as foundation from './foundation.js';
|
|
3
|
+
import * as jungle from '../importers/jungle-store.js';
|
|
2
4
|
import type { JobCheckpoint } from '../../contracts/index.js';
|
|
3
5
|
import { type AttemptToken, type OwnedAttempt, type BlobReference, type CommitRevision, type OwnerToken, type ReadableItem, type SourceSubmission, type StoreOptions, type Submission } from './types.js';
|
|
4
6
|
import * as indexing from '../indexing/index.js';
|
|
7
|
+
import * as prune from '../indexing/prune.js';
|
|
5
8
|
import { hybridSearch } from '../search/hybrid.js';
|
|
6
9
|
export * from './types.js';
|
|
10
|
+
export declare const libraryVersion = 7;
|
|
7
11
|
export declare const libraryMigrations: readonly string[];
|
|
8
12
|
type Row = Record<string, any>;
|
|
9
13
|
export declare function openLibrary(path: string, options?: StoreOptions): Promise<LibraryStore>;
|
|
@@ -29,20 +33,76 @@ export declare class LibraryStore {
|
|
|
29
33
|
writeIndexBatch(id: string, fingerprint: string, batch: Parameters<typeof indexing.writeBatch>[3]): void;
|
|
30
34
|
activateIndex(id: string): indexing.GenerationStatus;
|
|
31
35
|
cancelIndex(id: string): void;
|
|
36
|
+
/** Retired-generation pruning; see src/indexing/prune.ts. The plan probes rollback inside a rolled-back savepoint. */
|
|
37
|
+
pruneIndexPlan(keep: number): {
|
|
38
|
+
generations: prune.PruneEntry[];
|
|
39
|
+
stats: prune.PageStats;
|
|
40
|
+
};
|
|
41
|
+
pruneIndexStep(owner: OwnerToken, id: string, keep: number): number;
|
|
42
|
+
/** VACUUM cannot run in a transaction: fences are checked under the owner token immediately before it. */
|
|
43
|
+
vacuumLibrary(owner: OwnerToken): prune.PageStats;
|
|
32
44
|
searchHybrid(query: string, options?: Parameters<typeof hybridSearch>[2]): Promise<import("../search/hybrid.js").SearchResult>;
|
|
45
|
+
/** Export's view: the one annotation reader, without the redundant item id, plus reading state. */
|
|
33
46
|
readAnnotations(itemId: string): {
|
|
34
47
|
annotations: {
|
|
35
48
|
id: string;
|
|
49
|
+
kind: "note" | "highlight" | "comment";
|
|
36
50
|
revisionId: string | null;
|
|
51
|
+
parentId: string | null;
|
|
37
52
|
anchor: unknown;
|
|
38
53
|
body: string;
|
|
39
54
|
createdAt: string;
|
|
55
|
+
updatedAt: string | null;
|
|
56
|
+
anchored?: boolean;
|
|
40
57
|
}[];
|
|
41
58
|
readingState: {
|
|
42
59
|
state: unknown;
|
|
43
60
|
updatedAt: string;
|
|
44
61
|
} | null;
|
|
45
62
|
};
|
|
63
|
+
/** Sync and publishing foundation; see docs/decisions/sync-and-publishing.md. */
|
|
64
|
+
identity(itemId: string): {
|
|
65
|
+
uris: import("../identity/uri.js").ItemUri[];
|
|
66
|
+
visibility: foundation.VisibilityState;
|
|
67
|
+
};
|
|
68
|
+
itemsForUri(uri: string): string[];
|
|
69
|
+
setVisibility(itemId: string, value: foundation.Visibility | null): foundation.VisibilityState;
|
|
70
|
+
addHighlight(input: Parameters<typeof foundation.addHighlight>[1]): foundation.Annotation;
|
|
71
|
+
addComment(input: Parameters<typeof foundation.addComment>[1]): foundation.Annotation;
|
|
72
|
+
updateAnnotation(id: string, body: string): foundation.Annotation;
|
|
73
|
+
deleteAnnotation(id: string): void;
|
|
74
|
+
listAnnotations(itemId: string): (foundation.Annotation & {
|
|
75
|
+
anchored?: boolean;
|
|
76
|
+
})[];
|
|
77
|
+
putDigest(input: Parameters<typeof foundation.putDigest>[1]): {
|
|
78
|
+
id: string;
|
|
79
|
+
duplicate: boolean;
|
|
80
|
+
};
|
|
81
|
+
currentDigest(itemId: string): foundation.Derivation | undefined;
|
|
82
|
+
readDigest(id: string): foundation.Derivation | undefined;
|
|
83
|
+
findDigest(revisionId: string, inputHash: string): foundation.Derivation | undefined;
|
|
84
|
+
reviewDigest(id: string, review: foundation.DigestReview): foundation.Derivation;
|
|
85
|
+
currentEditorial(itemId: string): foundation.Editorial | undefined;
|
|
86
|
+
readEditorial(id: string): foundation.Editorial | undefined;
|
|
87
|
+
reviewEditorial(id: string, status: foundation.ReviewStatus | null): foundation.Editorial;
|
|
88
|
+
/** The item an importer realm holds for one source key, with its current revision; deleted items are reported, not hidden. */
|
|
89
|
+
importedItem(realm: string, sourceKey: string): {
|
|
90
|
+
itemId: string;
|
|
91
|
+
deleted: boolean;
|
|
92
|
+
revisionId: string | null;
|
|
93
|
+
} | null;
|
|
94
|
+
importedRevision(itemId: string, bundleHash: string): string;
|
|
95
|
+
revisionMarkdown(revisionId: string): string;
|
|
96
|
+
planJungleNote(input: jungle.JungleNoteInput): jungle.JunglePlan;
|
|
97
|
+
applyJungleNote(input: jungle.JungleNoteInput): jungle.JunglePlan;
|
|
98
|
+
listChanges(after?: number, limit?: number): foundation.LoggedChange[];
|
|
99
|
+
publicProjection(): {
|
|
100
|
+
entries: foundation.PublicEntry[];
|
|
101
|
+
skipped: {
|
|
102
|
+
itemId: string;
|
|
103
|
+
reason: string;
|
|
104
|
+
}[];
|
|
105
|
+
};
|
|
46
106
|
lookupImportReceipt(namespace: string, sourceKey: string, sourceHash: string): {
|
|
47
107
|
itemId: string;
|
|
48
108
|
outcome: string;
|
|
@@ -112,6 +172,15 @@ export declare class LibraryStore {
|
|
|
112
172
|
finishBackup(id: string): void;
|
|
113
173
|
/** Hold the SQLite writer fence across synchronous GC inspection and removal. */
|
|
114
174
|
withGcFence<T>(expectedEpoch: number, callback: (references: readonly BlobReference[]) => T): T;
|
|
175
|
+
/** Runtime GC pins: recorded model artifacts and embedding helpers of active or building generations. */
|
|
176
|
+
runtimeReferences(): {
|
|
177
|
+
modelArtifacts: {
|
|
178
|
+
id: string;
|
|
179
|
+
manifest: string;
|
|
180
|
+
}[];
|
|
181
|
+
embeddingHelpers: string[];
|
|
182
|
+
unmappedSpaces: number;
|
|
183
|
+
};
|
|
115
184
|
/** GC must honor both durable staging pins and active backup fences. */
|
|
116
185
|
pendingCleanupCount(): number;
|
|
117
186
|
hasGcFence(): boolean;
|
package/dist/src/store/index.js
CHANGED
|
@@ -5,18 +5,26 @@ import { randomUUID, createHash } from 'node:crypto';
|
|
|
5
5
|
import { sql as sql1 } from '../../migrations/001.js';
|
|
6
6
|
import { sql as sql2 } from '../../migrations/002.js';
|
|
7
7
|
import { sql as sql3 } from '../../migrations/003.js';
|
|
8
|
-
import { sql as sql4
|
|
8
|
+
import { sql as sql4 } from '../../migrations/004.js';
|
|
9
|
+
import { sql as sql5 } from '../../migrations/005.js';
|
|
10
|
+
import { sql as sql6 } from '../../migrations/006.js';
|
|
11
|
+
import { sql as sql7, version } from '../../migrations/007.js';
|
|
12
|
+
import * as foundation from './foundation.js';
|
|
13
|
+
import * as jungle from '../importers/jungle-store.js';
|
|
9
14
|
import * as indexJobs from '../indexing/jobs.js';
|
|
10
15
|
import { limits } from '../../contracts/index.js';
|
|
11
16
|
import { validateMetadataClaim, validateMetadataWarnings } from '../../contracts/validate.js';
|
|
12
17
|
import { confinedFile, digest, hash, integer, regularFile, safeDirectory, syncDirectory, text, verifyFile, pathExists, readVerifiedFile } from './files.js';
|
|
13
18
|
import { StoreError } from './types.js';
|
|
14
19
|
import * as indexing from '../indexing/index.js';
|
|
20
|
+
import * as prune from '../indexing/prune.js';
|
|
15
21
|
import { hybridSearch } from '../search/hybrid.js';
|
|
22
|
+
import { indexReadable, unindexReadable, lexicalSearch } from '../search/lexical.js';
|
|
16
23
|
import { initializeVectorExtension } from '../search/extension.js';
|
|
17
24
|
export * from './types.js';
|
|
18
25
|
const now = () => new Date().toISOString();
|
|
19
|
-
const migrations = [sql1, sql2, sql3, sql4];
|
|
26
|
+
const migrations = [sql1, sql2, sql3, sql4, sql5, sql6, sql7];
|
|
27
|
+
export const libraryVersion = version;
|
|
20
28
|
export const libraryMigrations = Object.freeze([...migrations]);
|
|
21
29
|
export async function openLibrary(path, options = {}) {
|
|
22
30
|
if (path === parse(path).root)
|
|
@@ -90,35 +98,51 @@ export async function openLibrary(path, options = {}) {
|
|
|
90
98
|
}
|
|
91
99
|
if (actual > 0 && db.prepare('SELECT schema_version FROM library_info WHERE id=1').get()?.schema_version !== actual)
|
|
92
100
|
throw new StoreError('integrity');
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
let backupPath;
|
|
102
|
+
try {
|
|
103
|
+
if (actual < version) {
|
|
104
|
+
// SQLite backup incorporates WAL; never copy the main file alone.
|
|
105
|
+
backupPath = join(root, 'backups', `before-schema-${version}-${randomUUID()}.sqlite`);
|
|
106
|
+
await backup(db, backupPath);
|
|
107
|
+
chmodSync(backupPath, 0o600);
|
|
108
|
+
const fd = openSync(backupPath, constants.O_RDONLY);
|
|
109
|
+
try {
|
|
110
|
+
fsyncSync(fd);
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
syncDirectory(dirname(backupPath));
|
|
101
116
|
}
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
for (let target = actual + 1; target <= version; target++) {
|
|
118
|
+
db.exec('BEGIN IMMEDIATE');
|
|
119
|
+
try {
|
|
120
|
+
db.exec(migrations[target - 1]);
|
|
121
|
+
// Code steps are total per item: one hostile row never makes the library unopenable.
|
|
122
|
+
if (target === 5) {
|
|
123
|
+
foundation.backfillUris(db);
|
|
124
|
+
foundation.seedChanges(db);
|
|
125
|
+
}
|
|
126
|
+
if (target === 1)
|
|
127
|
+
db.prepare('INSERT INTO library_info(id,uuid,schema_version,created_by) VALUES(1,?,?,?)').run(randomUUID(), target, options.appVersion ?? 'development');
|
|
128
|
+
else
|
|
129
|
+
db.prepare('UPDATE library_info SET schema_version=? WHERE id=1').run(target);
|
|
130
|
+
db.prepare('INSERT INTO schema_migrations VALUES(?,?,?)').run(target, digest(migrations[target - 1]), now());
|
|
131
|
+
db.exec(`PRAGMA user_version=${target}; COMMIT`);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
db.exec('ROLLBACK');
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
104
137
|
}
|
|
105
|
-
syncDirectory(dirname(backupPath));
|
|
106
138
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
db.prepare('INSERT INTO library_info(id,uuid,schema_version,created_by) VALUES(1,?,?,?)').run(randomUUID(), target, options.appVersion ?? 'development');
|
|
113
|
-
else
|
|
114
|
-
db.prepare('UPDATE library_info SET schema_version=? WHERE id=1').run(target);
|
|
115
|
-
db.prepare('INSERT INTO schema_migrations VALUES(?,?,?)').run(target, digest(migrations[target - 1]), now());
|
|
116
|
-
db.exec(`PRAGMA user_version=${target}; COMMIT`);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
db.exec('ROLLBACK');
|
|
120
|
-
throw error;
|
|
139
|
+
catch (error) {
|
|
140
|
+
// Nothing committed, so the library is byte-for-byte what the backup holds: drop the copy.
|
|
141
|
+
if (backupPath && Number(db.prepare('PRAGMA user_version').get()?.user_version) === actual) {
|
|
142
|
+
rmSync(backupPath, { force: true });
|
|
143
|
+
syncDirectory(dirname(backupPath));
|
|
121
144
|
}
|
|
145
|
+
throw error;
|
|
122
146
|
}
|
|
123
147
|
}
|
|
124
148
|
finally {
|
|
@@ -185,8 +209,43 @@ export class LibraryStore {
|
|
|
185
209
|
writeIndexBatch(id, fingerprint, batch) { indexing.writeBatch(this.db, id, fingerprint, batch); }
|
|
186
210
|
activateIndex(id) { return indexing.activateGeneration(this.db, id); }
|
|
187
211
|
cancelIndex(id) { indexing.cancelGeneration(this.db, id); }
|
|
212
|
+
/** Retired-generation pruning; see src/indexing/prune.ts. The plan probes rollback inside a rolled-back savepoint. */
|
|
213
|
+
pruneIndexPlan(keep) { return this.transaction(() => ({ generations: prune.planPrune(this.db, keep), stats: prune.pageStats(this.db) })); }
|
|
214
|
+
pruneIndexStep(owner, id, keep) { text(id, 256); return this.transaction(() => { this.checkOwner(owner); if (this.pendingCleanupCount() || this.hasGcFence())
|
|
215
|
+
throw new StoreError('owner-busy'); return prune.pruneStep(this.db, id, keep); }); }
|
|
216
|
+
/** VACUUM cannot run in a transaction: fences are checked under the owner token immediately before it. */
|
|
217
|
+
vacuumLibrary(owner) { const stats = this.transaction(() => { this.checkOwner(owner); if (this.pendingCleanupCount() || this.hasGcFence())
|
|
218
|
+
throw new StoreError('owner-busy'); return prune.pageStats(this.db); }); prune.vacuumSpace(this.root, stats.bytes); this.db.exec('VACUUM'); this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); return prune.pageStats(this.db); }
|
|
188
219
|
searchHybrid(query, options = {}) { return hybridSearch(this.db, query, options); }
|
|
189
|
-
|
|
220
|
+
/** Export's view: the one annotation reader, without the redundant item id, plus reading state. */
|
|
221
|
+
readAnnotations(itemId) { text(itemId, 256); const annotations = foundation.listAnnotations(this.db, itemId).map(({ itemId: _, ...a }) => a); const reading = this.db.prepare('SELECT state_json,updated_at FROM reading_state WHERE item_id=?').get(itemId); return { annotations, readingState: reading ? { state: JSON.parse(String(reading.state_json)), updatedAt: String(reading.updated_at) } : null }; }
|
|
222
|
+
/** Sync and publishing foundation; see docs/decisions/sync-and-publishing.md. */
|
|
223
|
+
identity(itemId) { text(itemId, 256); return { uris: foundation.itemUris(this.db, itemId), visibility: foundation.visibility(this.db, itemId) }; }
|
|
224
|
+
itemsForUri(uri) { return foundation.itemsForUri(this.db, uri); }
|
|
225
|
+
setVisibility(itemId, value) { text(itemId, 256); return this.transaction(() => foundation.setVisibility(this.db, itemId, value)); }
|
|
226
|
+
addHighlight(input) { text(input.itemId, 256); return this.transaction(() => foundation.addHighlight(this.db, input)); }
|
|
227
|
+
addComment(input) { text(input.itemId, 256); if (input.parentId !== undefined)
|
|
228
|
+
text(input.parentId, 256); return this.transaction(() => foundation.addComment(this.db, input)); }
|
|
229
|
+
updateAnnotation(id, body) { text(id, 256); return this.transaction(() => foundation.updateAnnotation(this.db, id, body)); }
|
|
230
|
+
deleteAnnotation(id) { text(id, 256); this.transaction(() => foundation.deleteAnnotation(this.db, id)); }
|
|
231
|
+
listAnnotations(itemId) { text(itemId, 256); return foundation.listAnnotations(this.db, itemId); }
|
|
232
|
+
putDigest(input) { text(input.itemId, 256); text(input.revisionId, 256); return this.transaction(() => foundation.putDigest(this.db, input)); }
|
|
233
|
+
currentDigest(itemId) { text(itemId, 256); return foundation.currentDigest(this.db, itemId); }
|
|
234
|
+
readDigest(id) { text(id, 256); return foundation.readDigest(this.db, id); }
|
|
235
|
+
findDigest(revisionId, inputHash) { text(revisionId, 256); return foundation.findDigest(this.db, revisionId, inputHash); }
|
|
236
|
+
reviewDigest(id, review) { text(id, 256); return this.transaction(() => foundation.reviewDigest(this.db, id, review)); }
|
|
237
|
+
currentEditorial(itemId) { text(itemId, 256); return foundation.currentEditorial(this.db, itemId); }
|
|
238
|
+
readEditorial(id) { text(id, 256); return foundation.readEditorial(this.db, id); }
|
|
239
|
+
reviewEditorial(id, status) { text(id, 256); return this.transaction(() => foundation.reviewEditorial(this.db, id, status)); }
|
|
240
|
+
/** The item an importer realm holds for one source key, with its current revision; deleted items are reported, not hidden. */
|
|
241
|
+
importedItem(realm, sourceKey) { text(realm, 256); text(sourceKey, 8192); const r = this.db.prepare('SELECT i.id,i.deleted_at,i.current_revision FROM source_refs s JOIN items i ON i.id=s.item_id WHERE s.source_key=? AND s.realm_key=?').get(sourceKey, realm); return r ? { itemId: String(r.id), deleted: r.deleted_at !== null, revisionId: r.current_revision === null ? null : String(r.current_revision) } : null; }
|
|
242
|
+
importedRevision(itemId, bundleHash) { text(itemId, 256); hash(bundleHash); const r = this.db.prepare('SELECT id FROM revisions WHERE item_id=? AND bundle_hash=?').get(itemId, bundleHash); if (!r)
|
|
243
|
+
throw new StoreError('unavailable'); return String(r.id); }
|
|
244
|
+
revisionMarkdown(revisionId) { text(revisionId, 256); return foundation.markdownFor(this.db, revisionId); }
|
|
245
|
+
planJungleNote(input) { text(input.itemId, 256); text(input.revisionId, 256); return jungle.planJungleNote(this.db, input); }
|
|
246
|
+
applyJungleNote(input) { text(input.itemId, 256); text(input.revisionId, 256); return this.transaction(() => jungle.applyJungleNote(this.db, input)); }
|
|
247
|
+
listChanges(after = 0, limit = 500) { return foundation.listChanges(this.db, after, limit); }
|
|
248
|
+
publicProjection() { return foundation.publicProjection(this.db); }
|
|
190
249
|
lookupImportReceipt(namespace, sourceKey, sourceHash) {
|
|
191
250
|
text(namespace, 256);
|
|
192
251
|
text(sourceKey, 8192);
|
|
@@ -366,10 +425,14 @@ export class LibraryStore {
|
|
|
366
425
|
if (source && source.kind !== input.kind)
|
|
367
426
|
throw new StoreError('invalid-input');
|
|
368
427
|
if (!source) {
|
|
369
|
-
|
|
370
|
-
this.db.prepare('
|
|
371
|
-
|
|
372
|
-
|
|
428
|
+
// Devices of one library agree on the id; outsiders cannot derive it from a URL.
|
|
429
|
+
const library = String(this.db.prepare('SELECT uuid FROM library_info WHERE id=1').get()?.uuid);
|
|
430
|
+
const itemId = digest(JSON.stringify(['item-v1', library, realm, input.sourceKey]));
|
|
431
|
+
if (this.db.prepare('SELECT 1 FROM items WHERE id=?').get(itemId))
|
|
432
|
+
throw new StoreError('integrity');
|
|
433
|
+
foundation.mutate(this.db, 'item', itemId, 'created', { kind: input.kind, sourceKey: input.sourceKey, realm, locator: input.locator, createdAt: now() });
|
|
434
|
+
foundation.syncUris(this.db, itemId);
|
|
435
|
+
source = { id: foundation.sourceRefId(itemId), item_id: itemId };
|
|
373
436
|
}
|
|
374
437
|
const active = this.db.prepare("SELECT id FROM jobs WHERE item_id=? AND state IN ('queued','running','delayed','cleanup-pending') ORDER BY created_at LIMIT 1").get(source.item_id);
|
|
375
438
|
const jobId = active ? String(active.id) : randomUUID();
|
|
@@ -593,8 +656,9 @@ export class LibraryStore {
|
|
|
593
656
|
throw new StoreError('integrity');
|
|
594
657
|
}
|
|
595
658
|
const existing = this.db.prepare('SELECT id,payload_hash FROM revisions WHERE item_id=? AND bundle_hash=?').get(input.itemId, input.bundleHash);
|
|
596
|
-
const item = this.db.prepare('SELECT current_revision FROM items WHERE id=?').get(input.itemId);
|
|
597
|
-
|
|
659
|
+
const item = this.db.prepare('SELECT current_revision,title FROM items WHERE id=?').get(input.itemId);
|
|
660
|
+
// Content-addressed: the same capture on two devices is the same revision object.
|
|
661
|
+
const revisionId = existing ? String(existing.id) : digest(JSON.stringify(['revision-v1', input.itemId, input.bundleHash]));
|
|
598
662
|
if (!existing) {
|
|
599
663
|
this.db.prepare('INSERT INTO revisions(id,item_id,parent_id,bundle_hash,extractor_name,extractor_version,captured_at,status,warnings_json,payload_hash,metadata_warnings_json) VALUES(?,?,?,?,?,?,?,?,?,?,?)').run(revisionId, input.itemId, item.current_revision ?? null, input.bundleHash, input.extractor.name, input.extractor.version, now(), input.status, JSON.stringify(input.warnings ?? []), payloadHash, JSON.stringify(metadataWarnings));
|
|
600
664
|
for (const [ordinal, asset] of input.assets.entries()) {
|
|
@@ -617,33 +681,30 @@ export class LibraryStore {
|
|
|
617
681
|
throw new StoreError('integrity');
|
|
618
682
|
}
|
|
619
683
|
if (item.current_revision !== revisionId) {
|
|
620
|
-
this.db
|
|
621
|
-
|
|
684
|
+
foundation.mutate(this.db, 'item', input.itemId, 'currentRevision', revisionId);
|
|
685
|
+
if (item.title !== (input.title ?? ''))
|
|
686
|
+
foundation.mutate(this.db, 'item', input.itemId, 'title', input.title ?? '');
|
|
622
687
|
const stored = this.db.prepare("SELECT markdown FROM representations WHERE revision_id=? AND kind='markdown'").get(revisionId);
|
|
623
|
-
this.db
|
|
688
|
+
indexReadable(this.db, input.itemId, revisionId, input.title ?? '', String(stored.markdown));
|
|
689
|
+
foundation.syncUris(this.db, input.itemId);
|
|
624
690
|
this.bumpEpoch();
|
|
625
691
|
}
|
|
626
692
|
if (imported) {
|
|
627
693
|
const receipt = this.lookupImportReceipt(imported.namespace, imported.sourceKey, imported.sourceHash);
|
|
628
694
|
if (receipt && receipt.itemId !== input.itemId)
|
|
629
695
|
throw new StoreError('integrity');
|
|
630
|
-
for (const note of imported.notes)
|
|
631
|
-
|
|
632
|
-
const prior = this.db.prepare('SELECT item_id,body FROM annotations WHERE id=?').get(id);
|
|
633
|
-
if (prior && (prior.item_id !== input.itemId || prior.body !== note.body))
|
|
634
|
-
throw new StoreError('integrity');
|
|
635
|
-
if (!prior)
|
|
636
|
-
this.db.prepare('INSERT INTO annotations VALUES(?,?,?,?,?,?)').run(id, input.itemId, revisionId, JSON.stringify({ kind: 'imported-note', sourceId: note.id }), note.body, now());
|
|
637
|
-
}
|
|
696
|
+
for (const note of imported.notes)
|
|
697
|
+
foundation.importNote(this.db, { itemId: input.itemId, revisionId, namespace: imported.namespace, sourceKey: imported.sourceKey, noteId: note.id, body: note.body });
|
|
638
698
|
const state = JSON.stringify({ ...imported.readingState, sourcePrivacy: imported.privacy });
|
|
639
699
|
const prior = this.db.prepare('SELECT state_json FROM reading_state WHERE item_id=?').get(input.itemId);
|
|
640
700
|
// Existing reading choices remain authoritative; import never overwrites them.
|
|
641
701
|
if (!prior)
|
|
642
|
-
this.db
|
|
702
|
+
foundation.setReadingState(this.db, input.itemId, JSON.parse(state));
|
|
643
703
|
else {
|
|
644
704
|
const existing = JSON.parse(String(prior.state_json));
|
|
645
705
|
const sourcePrivacy = existing.sourcePrivacy === 'published' && imported.privacy === 'published' ? 'published' : 'private';
|
|
646
|
-
|
|
706
|
+
if (sourcePrivacy !== existing.sourcePrivacy)
|
|
707
|
+
foundation.setReadingState(this.db, input.itemId, { ...existing, sourcePrivacy });
|
|
647
708
|
}
|
|
648
709
|
this.db.prepare('INSERT OR IGNORE INTO import_receipts VALUES(?,?,?,?,?,?)').run(imported.namespace, imported.sourceKey, imported.sourceHash, imported.importerVersion, input.itemId, 'imported');
|
|
649
710
|
}
|
|
@@ -675,13 +736,14 @@ export class LibraryStore {
|
|
|
675
736
|
const path = confinedFile(this.root, join(this.root, reference.relativePath));
|
|
676
737
|
return readVerifiedFile(path, reference.sha256, reference.bytes);
|
|
677
738
|
}
|
|
678
|
-
search(query, count = 20) { text(query, 4096); integer(count, 100);
|
|
739
|
+
search(query, count = 20) { text(query, 4096); integer(count, 100); return lexicalSearch(this.db, query, count).map(({ itemId, title, revisionId }) => ({ itemId, title, revisionId })); }
|
|
679
740
|
delete(itemId) {
|
|
680
741
|
text(itemId, 256);
|
|
681
742
|
this.transaction(() => {
|
|
682
|
-
const row = this.db.prepare('
|
|
683
|
-
if (row
|
|
684
|
-
this.db
|
|
743
|
+
const row = this.db.prepare('SELECT 1 FROM items WHERE id=? AND deleted_at IS NULL').get(itemId);
|
|
744
|
+
if (row) {
|
|
745
|
+
foundation.mutate(this.db, 'item', itemId, 'deleted', now());
|
|
746
|
+
unindexReadable(this.db, itemId);
|
|
685
747
|
this.db.prepare("UPDATE jobs SET cancel_requested=1,state=CASE WHEN state IN ('queued','delayed','needs-auth','needs-permission','unsupported','failed') THEN 'cancelled' ELSE state END WHERE item_id=? AND state NOT IN ('succeeded','failed','cancelled')").run(itemId);
|
|
686
748
|
this.bumpEpoch();
|
|
687
749
|
}
|
|
@@ -721,6 +783,27 @@ export class LibraryStore {
|
|
|
721
783
|
return callback(references);
|
|
722
784
|
});
|
|
723
785
|
}
|
|
786
|
+
/** Runtime GC pins: recorded model artifacts and embedding helpers of active or building generations. */
|
|
787
|
+
runtimeReferences() {
|
|
788
|
+
const modelArtifacts = this.db.prepare('SELECT id,manifest_json FROM model_artifacts ORDER BY id').all().map(r => ({ id: String(r.id), manifest: String(r.manifest_json) }));
|
|
789
|
+
const helpers = new Set();
|
|
790
|
+
let unmapped = 0;
|
|
791
|
+
for (const row of this.db.prepare("SELECT DISTINCT s.identity_json FROM embedding_spaces s JOIN index_generations g ON g.fingerprint=s.fingerprint WHERE g.state='building' OR g.id IN (SELECT generation_id FROM active_indexes)").all()) {
|
|
792
|
+
let identity;
|
|
793
|
+
try {
|
|
794
|
+
identity = JSON.parse(String(row.identity_json));
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
throw new StoreError('integrity');
|
|
798
|
+
}
|
|
799
|
+
const sha = identity?.helperSha256;
|
|
800
|
+
if (typeof sha === 'string' && /^[a-f0-9]{64}$/.test(sha))
|
|
801
|
+
helpers.add(sha);
|
|
802
|
+
else
|
|
803
|
+
unmapped++;
|
|
804
|
+
}
|
|
805
|
+
return { modelArtifacts, embeddingHelpers: [...helpers].sort(), unmappedSpaces: unmapped };
|
|
806
|
+
}
|
|
724
807
|
/** GC must honor both durable staging pins and active backup fences. */
|
|
725
808
|
pendingCleanupCount() { return Number(this.db.prepare("SELECT (SELECT count(*) FROM attempts WHERE cleanup='pending')+(SELECT count(*) FROM index_attempts WHERE cleanup='pending') AS n").get()?.n); }
|
|
726
809
|
hasGcFence() { return Number(this.db.prepare("SELECT (SELECT count(*) FROM staging_pins)+(SELECT count(*) FROM backup_fences WHERE state='active')+(SELECT count(*) FROM index_attempts WHERE cleanup='pending')+(SELECT count(*) FROM index_jobs WHERE state IN ('running','cleanup-pending')) n").get()?.n) > 0; }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type LibraryStore } from '../store/index.js';
|
|
2
|
+
export type RuntimeKind = 'capture' | 'model' | 'worker';
|
|
3
|
+
export interface RuntimeEntry {
|
|
4
|
+
kind: RuntimeKind;
|
|
5
|
+
name: string;
|
|
6
|
+
bytes: number;
|
|
7
|
+
}
|
|
8
|
+
export interface RuntimeKept {
|
|
9
|
+
kind: RuntimeKind;
|
|
10
|
+
name: string;
|
|
11
|
+
reason: 'referenced' | 'in-use' | 'symlink' | 'foreign' | 'catalog-unavailable' | 'current-missing';
|
|
12
|
+
}
|
|
13
|
+
export interface RuntimeGcReport {
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
removed: RuntimeEntry[];
|
|
16
|
+
kept: RuntimeKept[];
|
|
17
|
+
bytes: number;
|
|
18
|
+
}
|
|
19
|
+
/** What the installed Sponge can load. `models: null` (catalog unavailable) keeps every model directory. */
|
|
20
|
+
export interface RuntimeGcOptions {
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
captureIds: readonly string[];
|
|
23
|
+
models: {
|
|
24
|
+
fingerprints: readonly string[];
|
|
25
|
+
} | null;
|
|
26
|
+
}
|
|
27
|
+
/** Remove only superseded capture runtimes, model packages and settled worker journals.
|
|
28
|
+
* Applying holds worker custody, so no worker, capture, import verification, index run or query
|
|
29
|
+
* can start or be running; each model directory is additionally taken under its setup lock. */
|
|
30
|
+
export declare function collectRuntimes(store: LibraryStore, options: RuntimeGcOptions): Promise<RuntimeGcReport>;
|