@fortemi/core 2026.7.5 → 2026.7.8
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/dist/aiwg-index.d.ts +22 -1
- package/dist/aiwg-index.js +20 -12
- package/dist/aiwg-index.js.map +1 -1
- package/dist/index.d.ts +969 -146
- package/dist/index.js +2351 -220
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { v7 } from 'uuid';
|
|
1
|
+
import { v7, v5 } from 'uuid';
|
|
2
2
|
import { sha256 } from '@noble/hashes/sha256';
|
|
3
3
|
import { blake3 } from '@noble/hashes/blake3';
|
|
4
4
|
import { bytesToHex } from '@noble/hashes/utils';
|
|
@@ -743,6 +743,80 @@ var migration0016 = {
|
|
|
743
743
|
`
|
|
744
744
|
};
|
|
745
745
|
|
|
746
|
+
// src/migrations/0017_attachment_blob_parity.ts
|
|
747
|
+
var migration0017 = {
|
|
748
|
+
version: 17,
|
|
749
|
+
name: "0017_attachment_blob_parity",
|
|
750
|
+
sql: `
|
|
751
|
+
-- \u2500\u2500 attachment_blob parity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
752
|
+
ALTER TABLE attachment_blob
|
|
753
|
+
ADD COLUMN IF NOT EXISTS content_type TEXT NOT NULL DEFAULT 'application/octet-stream';
|
|
754
|
+
|
|
755
|
+
ALTER TABLE attachment_blob
|
|
756
|
+
ADD COLUMN IF NOT EXISTS storage_type TEXT NOT NULL DEFAULT 'bytecask';
|
|
757
|
+
|
|
758
|
+
-- DERIVED projection state (ADR-013): rebuilt from canonical manifests;
|
|
759
|
+
-- no trigger maintains it and nothing may treat it as lifecycle truth.
|
|
760
|
+
ALTER TABLE attachment_blob
|
|
761
|
+
ADD COLUMN IF NOT EXISTS reference_count INTEGER NOT NULL DEFAULT 0;
|
|
762
|
+
|
|
763
|
+
-- Backfill content_type from the attachment-level MIME denormalization (0010).
|
|
764
|
+
UPDATE attachment_blob ab
|
|
765
|
+
SET content_type = a.mime_type
|
|
766
|
+
FROM attachment a
|
|
767
|
+
WHERE a.blob_id = ab.id
|
|
768
|
+
AND a.mime_type IS NOT NULL
|
|
769
|
+
AND ab.content_type = 'application/octet-stream';
|
|
770
|
+
|
|
771
|
+
-- Backfill the derived reference count from live manifests.
|
|
772
|
+
UPDATE attachment_blob ab
|
|
773
|
+
SET reference_count = (
|
|
774
|
+
SELECT COUNT(*) FROM attachment a
|
|
775
|
+
WHERE a.blob_id = ab.id AND a.deleted_at IS NULL
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
-- Orphan scan support (projection-side reporting only).
|
|
779
|
+
CREATE INDEX IF NOT EXISTS idx_attachment_blob_orphan
|
|
780
|
+
ON attachment_blob(reference_count) WHERE reference_count = 0;
|
|
781
|
+
|
|
782
|
+
-- \u2500\u2500 attachment parity additions (additive, nullable-first) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
783
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS original_filename TEXT;
|
|
784
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'uploaded'
|
|
785
|
+
CHECK (status IN ('uploaded', 'queued', 'processing', 'completed', 'failed', 'quarantined'));
|
|
786
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS processing_error TEXT;
|
|
787
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS extraction_strategy TEXT;
|
|
788
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS extraction_config JSONB DEFAULT '{}';
|
|
789
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS extracted_metadata JSONB;
|
|
790
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS ai_description TEXT;
|
|
791
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS has_preview BOOLEAN NOT NULL DEFAULT FALSE;
|
|
792
|
+
ALTER TABLE attachment ADD COLUMN IF NOT EXISTS preview_blob_id TEXT REFERENCES attachment_blob(id);
|
|
793
|
+
|
|
794
|
+
-- Backfill: browser attachments that already carry extracted text were
|
|
795
|
+
-- fully processed at attach time \u2014 mark them completed so the
|
|
796
|
+
-- status-gated searchable-text join (server parity) keeps including them.
|
|
797
|
+
UPDATE attachment SET status = 'completed'
|
|
798
|
+
WHERE extracted_text IS NOT NULL AND extracted_text <> '';
|
|
799
|
+
|
|
800
|
+
-- \u2500\u2500 attachment_embedding (server parity; CLIP column reserved) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
801
|
+
CREATE TABLE IF NOT EXISTS attachment_embedding (
|
|
802
|
+
id TEXT PRIMARY KEY,
|
|
803
|
+
attachment_id TEXT NOT NULL REFERENCES attachment(id),
|
|
804
|
+
embedding_set_id TEXT REFERENCES embedding_set(id),
|
|
805
|
+
chunk_index INTEGER NOT NULL DEFAULT 0,
|
|
806
|
+
text TEXT NOT NULL,
|
|
807
|
+
vector vector(768),
|
|
808
|
+
clip_vector vector(512),
|
|
809
|
+
model TEXT NOT NULL,
|
|
810
|
+
embedding_type TEXT NOT NULL DEFAULT 'text',
|
|
811
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
812
|
+
UNIQUE (attachment_id, embedding_set_id, chunk_index)
|
|
813
|
+
);
|
|
814
|
+
|
|
815
|
+
CREATE INDEX IF NOT EXISTS idx_attachment_embedding_attachment
|
|
816
|
+
ON attachment_embedding(attachment_id);
|
|
817
|
+
`
|
|
818
|
+
};
|
|
819
|
+
|
|
746
820
|
// src/migrations/index.ts
|
|
747
821
|
var allMigrations = [
|
|
748
822
|
migration0001,
|
|
@@ -760,7 +834,8 @@ var allMigrations = [
|
|
|
760
834
|
migration0013,
|
|
761
835
|
migration0014,
|
|
762
836
|
migration0015,
|
|
763
|
-
migration0016
|
|
837
|
+
migration0016,
|
|
838
|
+
migration0017
|
|
764
839
|
];
|
|
765
840
|
|
|
766
841
|
// src/data-archive.ts
|
|
@@ -888,7 +963,7 @@ var NotesRepository = class {
|
|
|
888
963
|
* All writes happen in a single transaction.
|
|
889
964
|
*/
|
|
890
965
|
async create(input) {
|
|
891
|
-
const noteId = generateId();
|
|
966
|
+
const noteId = input.id ?? generateId();
|
|
892
967
|
const originalId = generateId();
|
|
893
968
|
const contentHash = computeHash(new TextEncoder().encode(input.content));
|
|
894
969
|
await this.db.transaction(async (tx) => {
|
|
@@ -1282,7 +1357,7 @@ var ATTACHMENT_TEXT_JOIN = `
|
|
|
1282
1357
|
string_agg(extracted_text, ' ' ORDER BY position, created_at)
|
|
1283
1358
|
FILTER (WHERE extracted_text IS NOT NULL AND extracted_text <> '') as extracted_text
|
|
1284
1359
|
FROM attachment
|
|
1285
|
-
WHERE deleted_at IS NULL
|
|
1360
|
+
WHERE deleted_at IS NULL AND status = 'completed'
|
|
1286
1361
|
GROUP BY note_id
|
|
1287
1362
|
) ax ON ax.note_id = n.id`;
|
|
1288
1363
|
var COMBINED_TEXT_VECTOR_SQL = `to_tsvector('english', (coalesce(c.content, '') || ' ' || coalesce(ax.extracted_text, '')))`;
|
|
@@ -1797,7 +1872,7 @@ var ATTACHMENT_TEXT_JOIN2 = `
|
|
|
1797
1872
|
string_agg(extracted_text, ' ' ORDER BY position, created_at)
|
|
1798
1873
|
FILTER (WHERE extracted_text IS NOT NULL AND extracted_text <> '') as extracted_text
|
|
1799
1874
|
FROM attachment
|
|
1800
|
-
WHERE deleted_at IS NULL
|
|
1875
|
+
WHERE deleted_at IS NULL AND status = 'completed'
|
|
1801
1876
|
GROUP BY note_id
|
|
1802
1877
|
) ax ON ax.note_id = n.id`;
|
|
1803
1878
|
var COMBINED_TEXT_SQL = `trim(both from (coalesce(c.content, '') || ' ' || coalesce(ax.extracted_text, '')))`;
|
|
@@ -3609,150 +3684,326 @@ function matchRoute(routes, request, url) {
|
|
|
3609
3684
|
return null;
|
|
3610
3685
|
}
|
|
3611
3686
|
|
|
3612
|
-
// src/blob-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3687
|
+
// src/shard/blob-sidecar.ts
|
|
3688
|
+
var SIDECAR_PREFIX = "blobs/";
|
|
3689
|
+
function blobChecksumToHex(checksum) {
|
|
3690
|
+
const sep = checksum.indexOf(":");
|
|
3691
|
+
return sep >= 0 ? checksum.slice(sep + 1) : checksum;
|
|
3692
|
+
}
|
|
3693
|
+
function sidecarEntryName(checksum) {
|
|
3694
|
+
return SIDECAR_PREFIX + blobChecksumToHex(checksum);
|
|
3695
|
+
}
|
|
3696
|
+
function isSidecarEntry(name) {
|
|
3697
|
+
if (!name.startsWith(SIDECAR_PREFIX)) return false;
|
|
3698
|
+
const rest = name.slice(SIDECAR_PREFIX.length);
|
|
3699
|
+
return rest.length > 0 && !rest.includes("/");
|
|
3619
3700
|
}
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3701
|
+
function collectSidecarBlobs(files) {
|
|
3702
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
3703
|
+
for (const [name, bytes] of files) {
|
|
3704
|
+
if (isSidecarEntry(name)) {
|
|
3705
|
+
blobs.set(name.slice(SIDECAR_PREFIX.length), bytes);
|
|
3706
|
+
}
|
|
3623
3707
|
}
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3708
|
+
return blobs;
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// src/blob-store-legacy.ts
|
|
3712
|
+
function legacyName(archiveName) {
|
|
3713
|
+
return `fortemi-${archiveName}-blobs`;
|
|
3714
|
+
}
|
|
3715
|
+
function idbRequest(req) {
|
|
3716
|
+
return new Promise((resolve, reject) => {
|
|
3717
|
+
req.onsuccess = () => resolve(req.result);
|
|
3718
|
+
req.onerror = () => reject(req.error);
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
async function legacyIdbExists(factory, name) {
|
|
3722
|
+
if (typeof factory.databases === "function") {
|
|
3723
|
+
const dbs = await factory.databases();
|
|
3724
|
+
return dbs.some((db) => db.name === name);
|
|
3627
3725
|
}
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3726
|
+
return false;
|
|
3727
|
+
}
|
|
3728
|
+
async function migrateLegacyIdb(archiveName, target, factory) {
|
|
3729
|
+
const name = legacyName(archiveName);
|
|
3730
|
+
if (!await legacyIdbExists(factory, name)) return 0;
|
|
3731
|
+
const db = await new Promise((resolve, reject) => {
|
|
3732
|
+
const req = factory.open(name);
|
|
3733
|
+
req.onsuccess = () => resolve(req.result);
|
|
3734
|
+
req.onerror = () => reject(req.error);
|
|
3735
|
+
});
|
|
3736
|
+
try {
|
|
3737
|
+
if (!db.objectStoreNames.contains("blobs")) return 0;
|
|
3738
|
+
const tx = db.transaction("blobs", "readonly");
|
|
3739
|
+
const values = await idbRequest(tx.objectStore("blobs").getAll());
|
|
3740
|
+
let migrated = 0;
|
|
3741
|
+
for (const value of values) {
|
|
3742
|
+
if (value instanceof Uint8Array) {
|
|
3743
|
+
await target.put(value);
|
|
3744
|
+
migrated += 1;
|
|
3745
|
+
} else if (value instanceof ArrayBuffer) {
|
|
3746
|
+
await target.put(new Uint8Array(value));
|
|
3747
|
+
migrated += 1;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
db.close();
|
|
3751
|
+
await idbRequest(factory.deleteDatabase(name));
|
|
3752
|
+
return migrated;
|
|
3753
|
+
} finally {
|
|
3754
|
+
db.close();
|
|
3641
3755
|
}
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
await writable.write(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength));
|
|
3647
|
-
await writable.close();
|
|
3756
|
+
}
|
|
3757
|
+
async function migrateLegacyOpfs(archiveName, target) {
|
|
3758
|
+
if (typeof navigator === "undefined" || typeof navigator.storage?.getDirectory !== "function") {
|
|
3759
|
+
return 0;
|
|
3648
3760
|
}
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3761
|
+
const root = await navigator.storage.getDirectory();
|
|
3762
|
+
let legacyDir;
|
|
3763
|
+
try {
|
|
3764
|
+
legacyDir = await root.getDirectoryHandle(legacyName(archiveName), {
|
|
3765
|
+
create: false
|
|
3766
|
+
});
|
|
3767
|
+
} catch {
|
|
3768
|
+
return 0;
|
|
3655
3769
|
}
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3770
|
+
let migrated = 0;
|
|
3771
|
+
for await (const [, d1] of legacyDir.entries()) {
|
|
3772
|
+
if (d1.kind !== "directory") continue;
|
|
3773
|
+
for await (const [, d2] of d1.entries()) {
|
|
3774
|
+
if (d2.kind !== "directory") continue;
|
|
3775
|
+
for await (const [, fh] of d2.entries()) {
|
|
3776
|
+
if (fh.kind !== "file") continue;
|
|
3777
|
+
const file = await fh.getFile();
|
|
3778
|
+
await target.put(new Uint8Array(await file.arrayBuffer()));
|
|
3779
|
+
migrated += 1;
|
|
3666
3780
|
}
|
|
3667
|
-
throw err;
|
|
3668
3781
|
}
|
|
3669
3782
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
return fh !== null;
|
|
3673
|
-
}
|
|
3674
|
-
};
|
|
3675
|
-
var IDB_STORE = "blobs";
|
|
3676
|
-
var IDB_VERSION = 1;
|
|
3677
|
-
function openDb(dbName) {
|
|
3678
|
-
return new Promise((resolve, reject) => {
|
|
3679
|
-
const req = indexedDB.open(dbName, IDB_VERSION);
|
|
3680
|
-
req.onupgradeneeded = () => {
|
|
3681
|
-
req.result.createObjectStore(IDB_STORE);
|
|
3682
|
-
};
|
|
3683
|
-
req.onsuccess = () => resolve(req.result);
|
|
3684
|
-
req.onerror = () => reject(req.error);
|
|
3685
|
-
});
|
|
3783
|
+
await root.removeEntry(legacyName(archiveName), { recursive: true });
|
|
3784
|
+
return migrated;
|
|
3686
3785
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3786
|
+
async function migrateLegacyBlobStore(archiveName, target, indexedDbFactory) {
|
|
3787
|
+
let migrated = 0;
|
|
3788
|
+
let failed = false;
|
|
3789
|
+
const factory = indexedDbFactory ?? globalThis.indexedDB;
|
|
3790
|
+
if (factory) {
|
|
3791
|
+
try {
|
|
3792
|
+
migrated += await migrateLegacyIdb(archiveName, target, factory);
|
|
3793
|
+
} catch {
|
|
3794
|
+
failed = true;
|
|
3696
3795
|
}
|
|
3697
|
-
return this._db;
|
|
3698
3796
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
tx.objectStore(IDB_STORE).put(data, hash);
|
|
3704
|
-
tx.oncomplete = () => resolve();
|
|
3705
|
-
tx.onerror = () => reject(tx.error);
|
|
3706
|
-
});
|
|
3797
|
+
try {
|
|
3798
|
+
migrated += await migrateLegacyOpfs(archiveName, target);
|
|
3799
|
+
} catch {
|
|
3800
|
+
failed = true;
|
|
3707
3801
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3802
|
+
return { migrated, failed };
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
// src/blob-store.ts
|
|
3806
|
+
var MemoryBlobStore = class {
|
|
3807
|
+
constructor(now = Date.now) {
|
|
3808
|
+
this.now = now;
|
|
3809
|
+
}
|
|
3810
|
+
entries = /* @__PURE__ */ new Map();
|
|
3811
|
+
async put(bytes) {
|
|
3812
|
+
const checksum = computeBlobHash(bytes);
|
|
3813
|
+
const existing = this.entries.get(checksum);
|
|
3814
|
+
if (existing) {
|
|
3815
|
+
existing.refcount += 1;
|
|
3816
|
+
} else {
|
|
3817
|
+
this.entries.set(checksum, { bytes, createdAt: this.now(), refcount: 1 });
|
|
3818
|
+
}
|
|
3819
|
+
return checksum;
|
|
3820
|
+
}
|
|
3821
|
+
async read(checksum) {
|
|
3822
|
+
return this.entries.get(checksum)?.bytes ?? null;
|
|
3823
|
+
}
|
|
3824
|
+
async has(checksum) {
|
|
3825
|
+
return this.entries.has(checksum);
|
|
3826
|
+
}
|
|
3827
|
+
async reconcile(liveChecksums, opts) {
|
|
3828
|
+
const live = /* @__PURE__ */ new Map();
|
|
3829
|
+
for (const checksum of liveChecksums) {
|
|
3830
|
+
live.set(checksum, (live.get(checksum) ?? 0) + 1);
|
|
3831
|
+
}
|
|
3832
|
+
const missing = [...live.keys()].filter((c) => !this.entries.has(c)).sort();
|
|
3833
|
+
const unreferenced = [];
|
|
3834
|
+
let referenced = 0;
|
|
3835
|
+
let removed = 0;
|
|
3836
|
+
let bytesFreed = 0;
|
|
3837
|
+
for (const [checksum, entry] of [...this.entries].sort()) {
|
|
3838
|
+
const refcount = live.get(checksum) ?? 0;
|
|
3839
|
+
entry.refcount = refcount;
|
|
3840
|
+
if (refcount > 0) {
|
|
3841
|
+
referenced += 1;
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
unreferenced.push(checksum);
|
|
3845
|
+
if (opts?.removeUnreferenced) {
|
|
3846
|
+
this.entries.delete(checksum);
|
|
3847
|
+
removed += 1;
|
|
3848
|
+
bytesFreed += entry.bytes.byteLength;
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
return { referenced, missing, unreferenced, removed, bytesFreed };
|
|
3716
3852
|
}
|
|
3717
|
-
async
|
|
3718
|
-
const
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3853
|
+
async gc(opts) {
|
|
3854
|
+
const minAgeMs = opts?.minAgeMs ?? 0;
|
|
3855
|
+
const cutoff = this.now() - minAgeMs;
|
|
3856
|
+
let collected = 0;
|
|
3857
|
+
let bytesFreed = 0;
|
|
3858
|
+
for (const [checksum, entry] of this.entries) {
|
|
3859
|
+
if (entry.refcount === 0 && entry.createdAt <= cutoff) {
|
|
3860
|
+
this.entries.delete(checksum);
|
|
3861
|
+
collected += 1;
|
|
3862
|
+
bytesFreed += entry.bytes.byteLength;
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
return { collected, bytesFreed };
|
|
3725
3866
|
}
|
|
3726
|
-
async
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
const req = tx.objectStore(IDB_STORE).count(hash);
|
|
3731
|
-
req.onsuccess = () => resolve(req.result > 0);
|
|
3732
|
-
req.onerror = () => reject(req.error);
|
|
3733
|
-
});
|
|
3867
|
+
async diagnostics() {
|
|
3868
|
+
return { backend: "memory", probe: null };
|
|
3869
|
+
}
|
|
3870
|
+
async close() {
|
|
3734
3871
|
}
|
|
3735
3872
|
};
|
|
3736
|
-
var
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3873
|
+
var CHECKSUM_PREFIX = "blake3:";
|
|
3874
|
+
function toChecksum(hex) {
|
|
3875
|
+
return CHECKSUM_PREFIX + hex;
|
|
3876
|
+
}
|
|
3877
|
+
var BytecaskBlobStore = class {
|
|
3878
|
+
constructor(facade, adapter, index, probe, now = Date.now) {
|
|
3879
|
+
this.facade = facade;
|
|
3880
|
+
this.adapter = adapter;
|
|
3881
|
+
this.index = index;
|
|
3882
|
+
this.probe = probe;
|
|
3883
|
+
this.now = now;
|
|
3884
|
+
}
|
|
3885
|
+
async put(bytes) {
|
|
3886
|
+
return toChecksum(await this.facade.put(bytes));
|
|
3887
|
+
}
|
|
3888
|
+
async read(checksum) {
|
|
3889
|
+
return this.facade.get(blobChecksumToHex(checksum));
|
|
3890
|
+
}
|
|
3891
|
+
async has(checksum) {
|
|
3892
|
+
return this.adapter.exists(blobChecksumToHex(checksum));
|
|
3893
|
+
}
|
|
3894
|
+
async reconcile(liveChecksums, opts) {
|
|
3895
|
+
const live = /* @__PURE__ */ new Set();
|
|
3896
|
+
for (const checksum of liveChecksums) live.add(blobChecksumToHex(checksum));
|
|
3897
|
+
const physical = /* @__PURE__ */ new Set();
|
|
3898
|
+
for await (const hash of this.adapter.list()) physical.add(hash);
|
|
3899
|
+
const entryList = [...await this.index.values()];
|
|
3900
|
+
const entries = new Map(entryList.map((e) => [e.hash, e]));
|
|
3901
|
+
for (const hash of [...entries.keys()]) {
|
|
3902
|
+
if (!physical.has(hash)) {
|
|
3903
|
+
await this.index.delete(hash);
|
|
3904
|
+
entries.delete(hash);
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
const missing = [...live].filter((hash) => !physical.has(hash)).sort().map(toChecksum);
|
|
3908
|
+
const unreferenced = [];
|
|
3909
|
+
let referenced = 0;
|
|
3910
|
+
let removed = 0;
|
|
3911
|
+
let bytesFreed = 0;
|
|
3912
|
+
for (const hash of [...physical].sort()) {
|
|
3913
|
+
const existing = entries.get(hash);
|
|
3914
|
+
const size = existing?.size ?? (await this.adapter.read(hash))?.byteLength ?? 0;
|
|
3915
|
+
if (live.has(hash)) {
|
|
3916
|
+
await this.index.set({
|
|
3917
|
+
hash,
|
|
3918
|
+
size,
|
|
3919
|
+
createdAt: existing?.createdAt ?? this.now(),
|
|
3920
|
+
refcount: 1
|
|
3921
|
+
});
|
|
3922
|
+
referenced += 1;
|
|
3923
|
+
continue;
|
|
3924
|
+
}
|
|
3925
|
+
unreferenced.push(toChecksum(hash));
|
|
3926
|
+
if (opts?.removeUnreferenced) {
|
|
3927
|
+
await this.adapter.remove(hash);
|
|
3928
|
+
await this.index.delete(hash);
|
|
3929
|
+
removed += 1;
|
|
3930
|
+
bytesFreed += size;
|
|
3931
|
+
} else {
|
|
3932
|
+
await this.index.set({
|
|
3933
|
+
hash,
|
|
3934
|
+
size,
|
|
3935
|
+
createdAt: existing?.createdAt ?? this.now(),
|
|
3936
|
+
refcount: 0
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
return { referenced, missing, unreferenced, removed, bytesFreed };
|
|
3740
3941
|
}
|
|
3741
|
-
async
|
|
3742
|
-
|
|
3942
|
+
async gc(opts) {
|
|
3943
|
+
const result = await this.facade.gc({ minAgeMs: opts?.minAgeMs });
|
|
3944
|
+
return { collected: result.collected, bytesFreed: result.bytesFreed };
|
|
3743
3945
|
}
|
|
3744
|
-
async
|
|
3745
|
-
this.
|
|
3946
|
+
async diagnostics() {
|
|
3947
|
+
return { backend: this.adapter.kind, probe: this.probe };
|
|
3746
3948
|
}
|
|
3747
|
-
async
|
|
3748
|
-
|
|
3949
|
+
async close() {
|
|
3950
|
+
await this.facade.close();
|
|
3749
3951
|
}
|
|
3750
3952
|
};
|
|
3751
|
-
function createBlobStore(archiveName) {
|
|
3752
|
-
|
|
3753
|
-
|
|
3953
|
+
async function createBlobStore(archiveName, options) {
|
|
3954
|
+
const bytecask = await import('@bytecask/core');
|
|
3955
|
+
if (options?.backend === "opfs") {
|
|
3956
|
+
throw new Error(
|
|
3957
|
+
"BlobStore: the OPFS tier requires @bytecask/core >= 2026.7.2 (worker-backed adapter); use the default IndexedDB tier until the upstream factory switch lands."
|
|
3958
|
+
);
|
|
3959
|
+
}
|
|
3960
|
+
const idbFactory = options?.indexedDB ?? globalThis.indexedDB;
|
|
3961
|
+
const attempted = [];
|
|
3962
|
+
let adapter;
|
|
3963
|
+
const wantsIdb = options?.backend !== "memory";
|
|
3964
|
+
if (wantsIdb && idbFactory) {
|
|
3965
|
+
adapter = new bytecask.IdbAdapter({
|
|
3966
|
+
databaseName: `fortemi-${archiveName}-bytecask`,
|
|
3967
|
+
factory: idbFactory
|
|
3968
|
+
});
|
|
3969
|
+
attempted.push({ backend: "idb", ok: true });
|
|
3970
|
+
} else {
|
|
3971
|
+
if (wantsIdb) {
|
|
3972
|
+
attempted.push({ backend: "idb", ok: false, reason: "indexedDB unavailable" });
|
|
3973
|
+
if (options?.backend === "idb") {
|
|
3974
|
+
throw new Error("BlobStore: IndexedDB tier requested but indexedDB is unavailable");
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
adapter = new bytecask.MemoryAdapter();
|
|
3978
|
+
attempted.push({ backend: "memory", ok: true });
|
|
3754
3979
|
}
|
|
3755
|
-
|
|
3980
|
+
const index = new bytecask.MemoryIndexStore();
|
|
3981
|
+
const facade = bytecask.createMemoryBlobStore({ adapter, index });
|
|
3982
|
+
const probe = { backend: adapter.kind, attempted };
|
|
3983
|
+
const wrapped = new BytecaskBlobStore(facade, adapter, index, probe);
|
|
3984
|
+
if (options?.migrateLegacy !== false) {
|
|
3985
|
+
await migrateLegacyBlobStore(archiveName, wrapped, options?.indexedDB);
|
|
3986
|
+
}
|
|
3987
|
+
return wrapped;
|
|
3988
|
+
}
|
|
3989
|
+
function createLazyBlobStore(archiveName, options) {
|
|
3990
|
+
let inner = null;
|
|
3991
|
+
const open = () => {
|
|
3992
|
+
inner ??= createBlobStore(archiveName, options);
|
|
3993
|
+
return inner;
|
|
3994
|
+
};
|
|
3995
|
+
return {
|
|
3996
|
+
put: async (bytes) => (await open()).put(bytes),
|
|
3997
|
+
read: async (checksum) => (await open()).read(checksum),
|
|
3998
|
+
has: async (checksum) => (await open()).has(checksum),
|
|
3999
|
+
reconcile: async (live, opts) => (await open()).reconcile(live, opts),
|
|
4000
|
+
gc: async (opts) => (await open()).gc(opts),
|
|
4001
|
+
diagnostics: async () => (await open()).diagnostics(),
|
|
4002
|
+
close: async () => {
|
|
4003
|
+
if (inner) await (await inner).close();
|
|
4004
|
+
inner = null;
|
|
4005
|
+
}
|
|
4006
|
+
};
|
|
3756
4007
|
}
|
|
3757
4008
|
|
|
3758
4009
|
// src/repositories/graph-repository.ts
|
|
@@ -5412,16 +5663,21 @@ var AttachmentsRepository = class {
|
|
|
5412
5663
|
/**
|
|
5413
5664
|
* Attach a binary file to a note.
|
|
5414
5665
|
*
|
|
5415
|
-
*
|
|
5416
|
-
*
|
|
5417
|
-
*
|
|
5418
|
-
*
|
|
5419
|
-
*
|
|
5666
|
+
* Bytes are written first (`put()` is idempotent — content addressing makes
|
|
5667
|
+
* replays safe), then the metadata rows commit (ADR-013 D5). A crash
|
|
5668
|
+
* between the two leaves an unreferenced blob that reconcile/gc sweeps —
|
|
5669
|
+
* never a manifest without recoverable state. The store-computed BLAKE3
|
|
5670
|
+
* `content_hash` (`blake3:<hex>`) matches the server convention and is the
|
|
5671
|
+
* key used by the portable Knowledge-Shard byte sidecar.
|
|
5672
|
+
*
|
|
5673
|
+
* If a blob row with the same content hash already exists, it is reused
|
|
5674
|
+
* (deduplication) — re-putting the bytes also heals a previously
|
|
5675
|
+
* reference-only blob whose bytes went missing.
|
|
5420
5676
|
*
|
|
5421
5677
|
* Returns the newly created AttachmentRow.
|
|
5422
5678
|
*/
|
|
5423
5679
|
async attach(input) {
|
|
5424
|
-
const contentHash =
|
|
5680
|
+
const contentHash = await this.blobStore.put(input.data);
|
|
5425
5681
|
const sizeBytes = input.data.length;
|
|
5426
5682
|
let blobId;
|
|
5427
5683
|
const existing = await this.db.query(
|
|
@@ -5432,7 +5688,6 @@ var AttachmentsRepository = class {
|
|
|
5432
5688
|
blobId = existing.rows[0].id;
|
|
5433
5689
|
} else {
|
|
5434
5690
|
blobId = generateId();
|
|
5435
|
-
await this.blobStore.write(contentHash, input.data);
|
|
5436
5691
|
await this.db.query(
|
|
5437
5692
|
`INSERT INTO attachment_blob (id, content_hash, size_bytes) VALUES ($1, $2, $3)`,
|
|
5438
5693
|
[blobId, contentHash, sizeBytes]
|
|
@@ -5440,8 +5695,8 @@ var AttachmentsRepository = class {
|
|
|
5440
5695
|
}
|
|
5441
5696
|
const attachmentId = generateId();
|
|
5442
5697
|
await this.db.query(
|
|
5443
|
-
`INSERT INTO attachment (id, note_id, blob_id, filename, display_name, mime_type, extracted_text)
|
|
5444
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
5698
|
+
`INSERT INTO attachment (id, note_id, blob_id, filename, display_name, mime_type, extracted_text, status)
|
|
5699
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
|
5445
5700
|
[
|
|
5446
5701
|
attachmentId,
|
|
5447
5702
|
input.noteId,
|
|
@@ -5449,7 +5704,8 @@ var AttachmentsRepository = class {
|
|
|
5449
5704
|
input.filename,
|
|
5450
5705
|
input.displayName ?? null,
|
|
5451
5706
|
input.mimeType ?? null,
|
|
5452
|
-
input.extractedText ?? null
|
|
5707
|
+
input.extractedText ?? null,
|
|
5708
|
+
input.extractedText ? "completed" : "uploaded"
|
|
5453
5709
|
]
|
|
5454
5710
|
);
|
|
5455
5711
|
return this.get(attachmentId);
|
|
@@ -5468,16 +5724,23 @@ var AttachmentsRepository = class {
|
|
|
5468
5724
|
}
|
|
5469
5725
|
/**
|
|
5470
5726
|
* Retrieve the raw binary data for an attachment.
|
|
5471
|
-
* Returns null
|
|
5727
|
+
* Returns null when the bytes are not present — the attachment is then in
|
|
5728
|
+
* the recoverable reference-only state (metadata intact, bytes
|
|
5729
|
+
* re-hydratable from a shard sidecar or a re-attach of the same content).
|
|
5472
5730
|
*/
|
|
5473
5731
|
async getBlob(attachmentId) {
|
|
5474
|
-
const
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5732
|
+
const checksum = await this.blobChecksumOf(attachmentId);
|
|
5733
|
+
if (checksum === null) return null;
|
|
5734
|
+
return this.blobStore.read(checksum);
|
|
5735
|
+
}
|
|
5736
|
+
/**
|
|
5737
|
+
* True when the attachment's bytes are physically present in the BlobStore.
|
|
5738
|
+
* False means reference-only (recoverable), not an error.
|
|
5739
|
+
*/
|
|
5740
|
+
async hasBlob(attachmentId) {
|
|
5741
|
+
const checksum = await this.blobChecksumOf(attachmentId);
|
|
5742
|
+
if (checksum === null) return false;
|
|
5743
|
+
return this.blobStore.has(checksum);
|
|
5481
5744
|
}
|
|
5482
5745
|
/**
|
|
5483
5746
|
* List active (non-deleted) attachments for a note.
|
|
@@ -5494,7 +5757,9 @@ var AttachmentsRepository = class {
|
|
|
5494
5757
|
}
|
|
5495
5758
|
/**
|
|
5496
5759
|
* Soft-delete an attachment by setting deleted_at to the current timestamp.
|
|
5497
|
-
* The
|
|
5760
|
+
* The blob row is untouched and no bytes are removed inline — physical
|
|
5761
|
+
* removal happens only through deferred `reconcileBlobs()`/`gcBlobs()`
|
|
5762
|
+
* against the canonical live set (ADR-013 D4).
|
|
5498
5763
|
*/
|
|
5499
5764
|
async delete(id) {
|
|
5500
5765
|
await this.db.query(
|
|
@@ -5502,6 +5767,44 @@ var AttachmentsRepository = class {
|
|
|
5502
5767
|
[id]
|
|
5503
5768
|
);
|
|
5504
5769
|
}
|
|
5770
|
+
/**
|
|
5771
|
+
* The authoritative live-checksum set: content hashes referenced by at
|
|
5772
|
+
* least one non-deleted attachment. This — not any refcount — decides
|
|
5773
|
+
* which bytes are reachable.
|
|
5774
|
+
*/
|
|
5775
|
+
async liveBlobChecksums() {
|
|
5776
|
+
const result = await this.db.query(
|
|
5777
|
+
`SELECT DISTINCT ab.content_hash
|
|
5778
|
+
FROM attachment_blob ab
|
|
5779
|
+
JOIN attachment a ON a.blob_id = ab.id
|
|
5780
|
+
WHERE a.deleted_at IS NULL`
|
|
5781
|
+
);
|
|
5782
|
+
return result.rows.map((row) => row.content_hash);
|
|
5783
|
+
}
|
|
5784
|
+
/**
|
|
5785
|
+
* Reconcile the BlobStore against the canonical live set (startup, after
|
|
5786
|
+
* quota events, after interrupted writes). `missing` lists reference-only
|
|
5787
|
+
* checksums; `unreferenced` lists GC candidates.
|
|
5788
|
+
*/
|
|
5789
|
+
async reconcileBlobs(opts) {
|
|
5790
|
+
return this.blobStore.reconcile(await this.liveBlobChecksums(), opts);
|
|
5791
|
+
}
|
|
5792
|
+
/**
|
|
5793
|
+
* Deferred, age-thresholded physical removal of unreachable bytes.
|
|
5794
|
+
* Runs a reconcile first so GC always acts on current manifest truth.
|
|
5795
|
+
*/
|
|
5796
|
+
async gcBlobs(opts) {
|
|
5797
|
+
await this.reconcileBlobs();
|
|
5798
|
+
return this.blobStore.gc(opts);
|
|
5799
|
+
}
|
|
5800
|
+
async blobChecksumOf(attachmentId) {
|
|
5801
|
+
const att = await this.get(attachmentId);
|
|
5802
|
+
const blob = await this.db.query(
|
|
5803
|
+
`SELECT content_hash FROM attachment_blob WHERE id = $1`,
|
|
5804
|
+
[att.blob_id]
|
|
5805
|
+
);
|
|
5806
|
+
return blob.rows.length > 0 ? blob.rows[0].content_hash : null;
|
|
5807
|
+
}
|
|
5505
5808
|
};
|
|
5506
5809
|
|
|
5507
5810
|
// src/tools/manage-attachments.ts
|
|
@@ -5516,6 +5819,21 @@ var ManageAttachmentsInputSchema = z.object({
|
|
|
5516
5819
|
extracted_text: z.string().optional(),
|
|
5517
5820
|
display_name: z.string().optional()
|
|
5518
5821
|
});
|
|
5822
|
+
function toToolAttachment(row) {
|
|
5823
|
+
return {
|
|
5824
|
+
id: row.id,
|
|
5825
|
+
note_id: row.note_id,
|
|
5826
|
+
blob_id: row.blob_id,
|
|
5827
|
+
document_type_id: row.document_type_id,
|
|
5828
|
+
mime_type: row.mime_type,
|
|
5829
|
+
extracted_text: row.extracted_text,
|
|
5830
|
+
filename: row.filename,
|
|
5831
|
+
display_name: row.display_name,
|
|
5832
|
+
position: row.position,
|
|
5833
|
+
created_at: row.created_at,
|
|
5834
|
+
deleted_at: row.deleted_at
|
|
5835
|
+
};
|
|
5836
|
+
}
|
|
5519
5837
|
async function manageAttachments(db, blobStore, rawInput) {
|
|
5520
5838
|
const input = ManageAttachmentsInputSchema.parse(rawInput);
|
|
5521
5839
|
const repo = new AttachmentsRepository(db, blobStore);
|
|
@@ -5539,17 +5857,17 @@ async function manageAttachments(db, blobStore, rawInput) {
|
|
|
5539
5857
|
await enqueueJob(db, { noteId: input.note_id, jobType: "embedding" });
|
|
5540
5858
|
await enqueueJob(db, { noteId: input.note_id, jobType: "concept_tagging" });
|
|
5541
5859
|
}
|
|
5542
|
-
return { action: "attach", attachment, size_bytes: data.length };
|
|
5860
|
+
return { action: "attach", attachment: toToolAttachment(attachment), size_bytes: data.length };
|
|
5543
5861
|
}
|
|
5544
5862
|
case "list": {
|
|
5545
5863
|
if (!input.note_id) throw new Error("note_id required for list");
|
|
5546
5864
|
const attachments = await repo.list(input.note_id);
|
|
5547
|
-
return { action: "list", attachments };
|
|
5865
|
+
return { action: "list", attachments: attachments.map(toToolAttachment) };
|
|
5548
5866
|
}
|
|
5549
5867
|
case "get": {
|
|
5550
5868
|
if (!input.attachment_id) throw new Error("attachment_id required for get");
|
|
5551
5869
|
const attachment = await repo.get(input.attachment_id);
|
|
5552
|
-
return { action: "get", attachment };
|
|
5870
|
+
return { action: "get", attachment: toToolAttachment(attachment) };
|
|
5553
5871
|
}
|
|
5554
5872
|
case "get_blob": {
|
|
5555
5873
|
if (!input.attachment_id) throw new Error("attachment_id required for get_blob");
|
|
@@ -6223,13 +6541,13 @@ var OpenAICompatibleProvider = class {
|
|
|
6223
6541
|
throw new Error("Streaming not supported: response body is null");
|
|
6224
6542
|
}
|
|
6225
6543
|
const reader = response.body.getReader();
|
|
6226
|
-
const
|
|
6544
|
+
const decoder7 = new TextDecoder();
|
|
6227
6545
|
let buffer = "";
|
|
6228
6546
|
try {
|
|
6229
6547
|
while (true) {
|
|
6230
6548
|
const { done, value } = await reader.read();
|
|
6231
6549
|
if (done) break;
|
|
6232
|
-
buffer +=
|
|
6550
|
+
buffer += decoder7.decode(value, { stream: true });
|
|
6233
6551
|
const lines = buffer.split("\n");
|
|
6234
6552
|
buffer = lines.pop() ?? "";
|
|
6235
6553
|
for (const line of lines) {
|
|
@@ -7011,6 +7329,180 @@ async function validateChecksums(checksums, files) {
|
|
|
7011
7329
|
return { valid: failures.length === 0, failures };
|
|
7012
7330
|
}
|
|
7013
7331
|
|
|
7332
|
+
// src/shard/shard-signature.ts
|
|
7333
|
+
var SIGNATURE_ENTRY = "signature.json";
|
|
7334
|
+
var SIGNING_ENVELOPE_VERSION = "1";
|
|
7335
|
+
var SIGNING_ALGORITHM = "ed25519";
|
|
7336
|
+
var MAX_ENVELOPE_BYTES = 64 * 1024;
|
|
7337
|
+
var AllowlistTrustStore = class {
|
|
7338
|
+
keys;
|
|
7339
|
+
constructor(keys) {
|
|
7340
|
+
this.keys = new Map(keys.map((k) => [k.key_id, k]));
|
|
7341
|
+
}
|
|
7342
|
+
resolve(keyId) {
|
|
7343
|
+
return this.keys.get(keyId) ?? null;
|
|
7344
|
+
}
|
|
7345
|
+
/** Mark a key revoked without removing it (still resolvable, verdict `revoked`). */
|
|
7346
|
+
revoke(keyId) {
|
|
7347
|
+
const key = this.keys.get(keyId);
|
|
7348
|
+
if (key) this.keys.set(keyId, { ...key, revoked: true });
|
|
7349
|
+
}
|
|
7350
|
+
};
|
|
7351
|
+
function base64urlToBytes(s) {
|
|
7352
|
+
const padded = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
7353
|
+
const b64 = padded + "=".repeat((4 - padded.length % 4) % 4);
|
|
7354
|
+
const bin = atob(b64);
|
|
7355
|
+
const out = new Uint8Array(bin.length);
|
|
7356
|
+
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
|
7357
|
+
return out;
|
|
7358
|
+
}
|
|
7359
|
+
function bytesToBase64url(bytes) {
|
|
7360
|
+
let bin = "";
|
|
7361
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
7362
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
7363
|
+
}
|
|
7364
|
+
function canonicalPayloadBytes(payload) {
|
|
7365
|
+
const canonical = {
|
|
7366
|
+
blob_digests: [...payload.blob_digests].sort(),
|
|
7367
|
+
format_version: payload.format_version,
|
|
7368
|
+
manifest_digest: payload.manifest_digest,
|
|
7369
|
+
signer: {
|
|
7370
|
+
algorithm: payload.signer.algorithm,
|
|
7371
|
+
key_id: payload.signer.key_id,
|
|
7372
|
+
public_key: payload.signer.public_key
|
|
7373
|
+
}
|
|
7374
|
+
};
|
|
7375
|
+
return new TextEncoder().encode(JSON.stringify(canonical));
|
|
7376
|
+
}
|
|
7377
|
+
var ed25519Supported = null;
|
|
7378
|
+
async function isShardSigningSupported() {
|
|
7379
|
+
if (ed25519Supported !== null) return ed25519Supported;
|
|
7380
|
+
try {
|
|
7381
|
+
const subtle = globalThis.crypto?.subtle;
|
|
7382
|
+
if (!subtle) {
|
|
7383
|
+
ed25519Supported = false;
|
|
7384
|
+
return false;
|
|
7385
|
+
}
|
|
7386
|
+
await subtle.importKey("raw", toBufferSource(new Uint8Array(32)), { name: "Ed25519" }, false, ["verify"]);
|
|
7387
|
+
ed25519Supported = true;
|
|
7388
|
+
} catch {
|
|
7389
|
+
ed25519Supported = false;
|
|
7390
|
+
}
|
|
7391
|
+
return ed25519Supported;
|
|
7392
|
+
}
|
|
7393
|
+
function sidecarBlobDigests(files) {
|
|
7394
|
+
const digests = [];
|
|
7395
|
+
for (const name of files.keys()) {
|
|
7396
|
+
if (isSidecarEntry(name)) digests.push(name.slice(SIDECAR_PREFIX.length));
|
|
7397
|
+
}
|
|
7398
|
+
return digests.sort();
|
|
7399
|
+
}
|
|
7400
|
+
async function verifyShardSignature(input) {
|
|
7401
|
+
const { files, trustStore } = input;
|
|
7402
|
+
const sigBytes = files.get(SIGNATURE_ENTRY);
|
|
7403
|
+
if (!sigBytes) return { ok: false, reason: "unsigned" };
|
|
7404
|
+
if (sigBytes.byteLength > MAX_ENVELOPE_BYTES) {
|
|
7405
|
+
return { ok: false, reason: "malformed", detail: "signature envelope exceeds size cap" };
|
|
7406
|
+
}
|
|
7407
|
+
let envelope;
|
|
7408
|
+
try {
|
|
7409
|
+
const parsed = JSON.parse(new TextDecoder().decode(sigBytes));
|
|
7410
|
+
if (!isEnvelopeShape(parsed)) throw new Error("unexpected envelope shape");
|
|
7411
|
+
envelope = parsed;
|
|
7412
|
+
} catch (err) {
|
|
7413
|
+
return { ok: false, reason: "malformed", detail: err instanceof Error ? err.message : String(err) };
|
|
7414
|
+
}
|
|
7415
|
+
if (envelope.format_version !== SIGNING_ENVELOPE_VERSION) {
|
|
7416
|
+
return { ok: false, reason: "malformed", detail: `unsupported envelope version ${envelope.format_version}` };
|
|
7417
|
+
}
|
|
7418
|
+
if (envelope.signer.algorithm !== SIGNING_ALGORITHM) {
|
|
7419
|
+
return { ok: false, reason: "malformed", detail: `unsupported algorithm ${envelope.signer.algorithm}` };
|
|
7420
|
+
}
|
|
7421
|
+
const trusted = await trustStore.resolve(envelope.signer.key_id);
|
|
7422
|
+
if (!trusted) return { ok: false, reason: "unknown-signer", keyId: envelope.signer.key_id };
|
|
7423
|
+
if (trusted.revoked) return { ok: false, reason: "revoked", keyId: envelope.signer.key_id };
|
|
7424
|
+
if (trusted.public_key !== envelope.signer.public_key) {
|
|
7425
|
+
return { ok: false, reason: "bad-signature", keyId: envelope.signer.key_id };
|
|
7426
|
+
}
|
|
7427
|
+
if (!await isShardSigningSupported()) return { ok: false, reason: "unsupported" };
|
|
7428
|
+
const payload = {
|
|
7429
|
+
format_version: envelope.format_version,
|
|
7430
|
+
signer: envelope.signer,
|
|
7431
|
+
manifest_digest: envelope.manifest_digest,
|
|
7432
|
+
blob_digests: envelope.blob_digests
|
|
7433
|
+
};
|
|
7434
|
+
let signatureValid = false;
|
|
7435
|
+
try {
|
|
7436
|
+
const key = await globalThis.crypto.subtle.importKey(
|
|
7437
|
+
"raw",
|
|
7438
|
+
toBufferSource(base64urlToBytes(trusted.public_key)),
|
|
7439
|
+
{ name: "Ed25519" },
|
|
7440
|
+
false,
|
|
7441
|
+
["verify"]
|
|
7442
|
+
);
|
|
7443
|
+
const digest = await sha256Hex(canonicalPayloadBytes(payload));
|
|
7444
|
+
signatureValid = await globalThis.crypto.subtle.verify(
|
|
7445
|
+
"Ed25519",
|
|
7446
|
+
key,
|
|
7447
|
+
toBufferSource(base64urlToBytes(envelope.signature)),
|
|
7448
|
+
toBufferSource(new TextEncoder().encode(digest))
|
|
7449
|
+
);
|
|
7450
|
+
} catch (err) {
|
|
7451
|
+
return { ok: false, reason: "malformed", detail: err instanceof Error ? err.message : String(err) };
|
|
7452
|
+
}
|
|
7453
|
+
if (!signatureValid) return { ok: false, reason: "bad-signature", keyId: envelope.signer.key_id };
|
|
7454
|
+
const manifest = files.get("manifest.json");
|
|
7455
|
+
if (!manifest) {
|
|
7456
|
+
return { ok: false, reason: "content-mismatch", detail: "manifest.json missing" };
|
|
7457
|
+
}
|
|
7458
|
+
const manifestDigest = await sha256Hex(manifest);
|
|
7459
|
+
if (manifestDigest !== envelope.manifest_digest) {
|
|
7460
|
+
return { ok: false, reason: "content-mismatch", detail: "manifest digest does not match signature" };
|
|
7461
|
+
}
|
|
7462
|
+
const archiveBlobDigests = sidecarBlobDigests(files);
|
|
7463
|
+
const signedBlobDigests = [...envelope.blob_digests].sort();
|
|
7464
|
+
if (archiveBlobDigests.join(",") !== signedBlobDigests.join(",")) {
|
|
7465
|
+
return {
|
|
7466
|
+
ok: false,
|
|
7467
|
+
reason: "content-mismatch",
|
|
7468
|
+
detail: "sidecar blob digest set does not match signature"
|
|
7469
|
+
};
|
|
7470
|
+
}
|
|
7471
|
+
return { ok: true, keyId: envelope.signer.key_id };
|
|
7472
|
+
}
|
|
7473
|
+
function toBufferSource(bytes) {
|
|
7474
|
+
const copy = new Uint8Array(new ArrayBuffer(bytes.byteLength));
|
|
7475
|
+
copy.set(bytes);
|
|
7476
|
+
return copy;
|
|
7477
|
+
}
|
|
7478
|
+
function isEnvelopeShape(value) {
|
|
7479
|
+
if (typeof value !== "object" || value === null) return false;
|
|
7480
|
+
const v = value;
|
|
7481
|
+
const signer = v.signer;
|
|
7482
|
+
return typeof v.format_version === "string" && typeof v.manifest_digest === "string" && typeof v.signature === "string" && Array.isArray(v.blob_digests) && v.blob_digests.every((d) => typeof d === "string") && typeof signer === "object" && signer !== null && typeof signer.key_id === "string" && typeof signer.algorithm === "string" && typeof signer.public_key === "string";
|
|
7483
|
+
}
|
|
7484
|
+
async function signShard(input) {
|
|
7485
|
+
const manifest = input.files.get("manifest.json");
|
|
7486
|
+
if (!manifest) throw new Error("signShard: manifest.json missing from archive");
|
|
7487
|
+
const payload = {
|
|
7488
|
+
format_version: SIGNING_ENVELOPE_VERSION,
|
|
7489
|
+
signer: { key_id: input.keyId, algorithm: SIGNING_ALGORITHM, public_key: input.publicKey },
|
|
7490
|
+
manifest_digest: await sha256Hex(manifest),
|
|
7491
|
+
blob_digests: sidecarBlobDigests(input.files)
|
|
7492
|
+
};
|
|
7493
|
+
const digest = await sha256Hex(canonicalPayloadBytes(payload));
|
|
7494
|
+
const signature = await globalThis.crypto.subtle.sign(
|
|
7495
|
+
"Ed25519",
|
|
7496
|
+
input.privateKey,
|
|
7497
|
+
toBufferSource(new TextEncoder().encode(digest))
|
|
7498
|
+
);
|
|
7499
|
+
const envelope = {
|
|
7500
|
+
...payload,
|
|
7501
|
+
signature: bytesToBase64url(new Uint8Array(signature))
|
|
7502
|
+
};
|
|
7503
|
+
return new TextEncoder().encode(JSON.stringify(envelope, null, 2));
|
|
7504
|
+
}
|
|
7505
|
+
|
|
7014
7506
|
// src/shard/field-mapper.ts
|
|
7015
7507
|
function noteToShard(note) {
|
|
7016
7508
|
return {
|
|
@@ -7018,6 +7510,7 @@ function noteToShard(note) {
|
|
|
7018
7510
|
title: note.title,
|
|
7019
7511
|
original_content: note.original_content,
|
|
7020
7512
|
revised_content: note.revised_content,
|
|
7513
|
+
...note.ai_metadata != null ? { metadata: note.ai_metadata } : {},
|
|
7021
7514
|
collection_id: note.collection_id ?? null,
|
|
7022
7515
|
...note.attachments?.length ? { attachments: note.attachments } : {},
|
|
7023
7516
|
format: note.format,
|
|
@@ -7041,6 +7534,7 @@ function noteFromShard(shard) {
|
|
|
7041
7534
|
is_archived: shard.archived,
|
|
7042
7535
|
original_content: shard.original_content,
|
|
7043
7536
|
revised_content: shard.revised_content,
|
|
7537
|
+
ai_metadata: shard.metadata ?? null,
|
|
7044
7538
|
collection_id: shard.collection_id ?? null,
|
|
7045
7539
|
attachments,
|
|
7046
7540
|
tags: shard.tags,
|
|
@@ -7079,11 +7573,11 @@ function linkFromShard(shard) {
|
|
|
7079
7573
|
id: shard.id,
|
|
7080
7574
|
source_note_id: shard.from_note_id,
|
|
7081
7575
|
target_note_id: shard.to_note_id,
|
|
7082
|
-
to_url: shard.to_url,
|
|
7576
|
+
to_url: shard.to_url ?? null,
|
|
7083
7577
|
link_type: shard.kind,
|
|
7084
7578
|
confidence: shard.score,
|
|
7085
7579
|
created_at: shard.created_at,
|
|
7086
|
-
metadata: shard.metadata
|
|
7580
|
+
metadata: shard.metadata ?? null
|
|
7087
7581
|
};
|
|
7088
7582
|
}
|
|
7089
7583
|
function collectionToShard(collection, noteCount) {
|
|
@@ -7213,10 +7707,10 @@ function embeddingFromShard(shard) {
|
|
|
7213
7707
|
id: shard.id,
|
|
7214
7708
|
note_id: shard.note_id,
|
|
7215
7709
|
embedding_set_id: shard.embedding_set_id ?? null,
|
|
7216
|
-
chunk_index: shard.chunk_index,
|
|
7217
|
-
text: shard.text,
|
|
7710
|
+
chunk_index: shard.chunk_index ?? 0,
|
|
7711
|
+
text: shard.text ?? "",
|
|
7218
7712
|
vector: `[${shard.vector.join(",")}]`,
|
|
7219
|
-
model: shard.model,
|
|
7713
|
+
model: shard.model ?? null,
|
|
7220
7714
|
created_at: shard.created_at ?? null
|
|
7221
7715
|
};
|
|
7222
7716
|
}
|
|
@@ -7311,30 +7805,6 @@ function slugifyEmbeddingSet(value) {
|
|
|
7311
7805
|
return slug || "embedding-set";
|
|
7312
7806
|
}
|
|
7313
7807
|
|
|
7314
|
-
// src/shard/blob-sidecar.ts
|
|
7315
|
-
var SIDECAR_PREFIX = "blobs/";
|
|
7316
|
-
function blobChecksumToHex(checksum) {
|
|
7317
|
-
const sep = checksum.indexOf(":");
|
|
7318
|
-
return sep >= 0 ? checksum.slice(sep + 1) : checksum;
|
|
7319
|
-
}
|
|
7320
|
-
function sidecarEntryName(checksum) {
|
|
7321
|
-
return SIDECAR_PREFIX + blobChecksumToHex(checksum);
|
|
7322
|
-
}
|
|
7323
|
-
function isSidecarEntry(name) {
|
|
7324
|
-
if (!name.startsWith(SIDECAR_PREFIX)) return false;
|
|
7325
|
-
const rest = name.slice(SIDECAR_PREFIX.length);
|
|
7326
|
-
return rest.length > 0 && !rest.includes("/");
|
|
7327
|
-
}
|
|
7328
|
-
function collectSidecarBlobs(files) {
|
|
7329
|
-
const blobs = /* @__PURE__ */ new Map();
|
|
7330
|
-
for (const [name, bytes] of files) {
|
|
7331
|
-
if (isSidecarEntry(name)) {
|
|
7332
|
-
blobs.set(name.slice(SIDECAR_PREFIX.length), bytes);
|
|
7333
|
-
}
|
|
7334
|
-
}
|
|
7335
|
-
return blobs;
|
|
7336
|
-
}
|
|
7337
|
-
|
|
7338
7808
|
// src/shard/shard-export.ts
|
|
7339
7809
|
var encoder = new TextEncoder();
|
|
7340
7810
|
function jsonObject3(value) {
|
|
@@ -7356,6 +7826,7 @@ async function exportShard(db, options) {
|
|
|
7356
7826
|
n.created_at, n.updated_at, n.deleted_at,
|
|
7357
7827
|
o.content as original_content,
|
|
7358
7828
|
c.content as revised_content,
|
|
7829
|
+
c.ai_metadata,
|
|
7359
7830
|
$1::text as collection_id
|
|
7360
7831
|
FROM note n
|
|
7361
7832
|
LEFT JOIN note_original o ON o.note_id = n.id
|
|
@@ -7369,6 +7840,7 @@ async function exportShard(db, options) {
|
|
|
7369
7840
|
n.created_at, n.updated_at, n.deleted_at,
|
|
7370
7841
|
o.content as original_content,
|
|
7371
7842
|
c.content as revised_content,
|
|
7843
|
+
c.ai_metadata,
|
|
7372
7844
|
(
|
|
7373
7845
|
SELECT cn.collection_id
|
|
7374
7846
|
FROM collection_note cn
|
|
@@ -7388,6 +7860,7 @@ async function exportShard(db, options) {
|
|
|
7388
7860
|
n.created_at, n.updated_at, n.deleted_at,
|
|
7389
7861
|
o.content as original_content,
|
|
7390
7862
|
c.content as revised_content,
|
|
7863
|
+
c.ai_metadata,
|
|
7391
7864
|
(
|
|
7392
7865
|
SELECT cn.collection_id
|
|
7393
7866
|
FROM collection_note cn
|
|
@@ -7784,6 +8257,32 @@ function parseJsonArrayBytes(data) {
|
|
|
7784
8257
|
// src/shard/shard-import.ts
|
|
7785
8258
|
var decoder2 = new TextDecoder();
|
|
7786
8259
|
var DEFAULT_BATCH_SIZE = 250;
|
|
8260
|
+
async function enforceSignaturePolicy(files, options, warnings) {
|
|
8261
|
+
const policy = options?.verifySignature ?? (options?.trustStore ? "require" : void 0);
|
|
8262
|
+
if (!policy) return null;
|
|
8263
|
+
if (policy === "trusted-local-only") {
|
|
8264
|
+
warnings.push(
|
|
8265
|
+
"Signature verification skipped (trusted-local-only): the shard was imported without authenticating its publisher."
|
|
8266
|
+
);
|
|
8267
|
+
return null;
|
|
8268
|
+
}
|
|
8269
|
+
if (!options?.trustStore) {
|
|
8270
|
+
return `verifySignature: '${policy}' requires a trustStore to resolve signer keys.`;
|
|
8271
|
+
}
|
|
8272
|
+
const verdict = await verifyShardSignature({ files, trustStore: options.trustStore });
|
|
8273
|
+
if (verdict.ok) return null;
|
|
8274
|
+
if (verdict.reason === "unsigned") {
|
|
8275
|
+
if (policy === "prefer") {
|
|
8276
|
+
warnings.push(
|
|
8277
|
+
"Shard is unsigned; imported under verifySignature: prefer. Publisher provenance was NOT authenticated."
|
|
8278
|
+
);
|
|
8279
|
+
return null;
|
|
8280
|
+
}
|
|
8281
|
+
return "Shard is unsigned and verifySignature is required.";
|
|
8282
|
+
}
|
|
8283
|
+
const detail = "detail" in verdict ? `: ${verdict.detail}` : "keyId" in verdict ? ` (key ${verdict.keyId})` : "";
|
|
8284
|
+
return `Shard signature verification failed [${verdict.reason}]${detail}. No records or bytes were written.`;
|
|
8285
|
+
}
|
|
7787
8286
|
async function yieldToEventLoop2() {
|
|
7788
8287
|
const scheduler = globalThis.scheduler;
|
|
7789
8288
|
if (scheduler?.yield) {
|
|
@@ -7884,6 +8383,17 @@ async function importShard(db, data, options) {
|
|
|
7884
8383
|
duration_ms: performance.now() - start
|
|
7885
8384
|
};
|
|
7886
8385
|
}
|
|
8386
|
+
const sigError = await enforceSignaturePolicy(files, options, warnings);
|
|
8387
|
+
if (sigError) {
|
|
8388
|
+
return {
|
|
8389
|
+
success: false,
|
|
8390
|
+
counts,
|
|
8391
|
+
skipped,
|
|
8392
|
+
warnings,
|
|
8393
|
+
errors: [sigError],
|
|
8394
|
+
duration_ms: performance.now() - start
|
|
8395
|
+
};
|
|
8396
|
+
}
|
|
7887
8397
|
report?.({ phase: "validate", done: 0, total: 1 });
|
|
7888
8398
|
if (!manifest.checksums || typeof manifest.checksums !== "object") {
|
|
7889
8399
|
return {
|
|
@@ -8102,10 +8612,14 @@ async function importShard(db, data, options) {
|
|
|
8102
8612
|
);
|
|
8103
8613
|
}
|
|
8104
8614
|
await tx.query(
|
|
8105
|
-
`INSERT INTO note_revised_current (note_id, content)
|
|
8106
|
-
VALUES ($1, $2)
|
|
8107
|
-
ON CONFLICT (note_id) DO UPDATE SET content = $2`,
|
|
8108
|
-
[
|
|
8615
|
+
`INSERT INTO note_revised_current (note_id, content, ai_metadata)
|
|
8616
|
+
VALUES ($1, $2, $3::jsonb)
|
|
8617
|
+
ON CONFLICT (note_id) DO UPDATE SET content = $2, ai_metadata = $3::jsonb`,
|
|
8618
|
+
[
|
|
8619
|
+
note.id,
|
|
8620
|
+
note.revised_content ?? note.original_content,
|
|
8621
|
+
note.ai_metadata == null ? null : JSON.stringify(note.ai_metadata)
|
|
8622
|
+
]
|
|
8109
8623
|
);
|
|
8110
8624
|
} else {
|
|
8111
8625
|
await tx.query(
|
|
@@ -8129,9 +8643,13 @@ async function importShard(db, data, options) {
|
|
|
8129
8643
|
[generateId(), note.id, note.original_content, contentHash]
|
|
8130
8644
|
);
|
|
8131
8645
|
await tx.query(
|
|
8132
|
-
`INSERT INTO note_revised_current (note_id, content)
|
|
8133
|
-
VALUES ($1, $2) ${conflictClause}`,
|
|
8134
|
-
[
|
|
8646
|
+
`INSERT INTO note_revised_current (note_id, content, ai_metadata)
|
|
8647
|
+
VALUES ($1, $2, $3::jsonb) ${conflictClause}`,
|
|
8648
|
+
[
|
|
8649
|
+
note.id,
|
|
8650
|
+
note.revised_content ?? note.original_content,
|
|
8651
|
+
note.ai_metadata == null ? null : JSON.stringify(note.ai_metadata)
|
|
8652
|
+
]
|
|
8135
8653
|
);
|
|
8136
8654
|
}
|
|
8137
8655
|
for (const tag of note.tags) {
|
|
@@ -8636,8 +9154,8 @@ async function importShard(db, data, options) {
|
|
|
8636
9154
|
}
|
|
8637
9155
|
if (options?.blobStore && blobsToHydrate.size > 0) {
|
|
8638
9156
|
try {
|
|
8639
|
-
for (const [
|
|
8640
|
-
await options.blobStore.
|
|
9157
|
+
for (const [, bytes] of blobsToHydrate) {
|
|
9158
|
+
await options.blobStore.put(bytes);
|
|
8641
9159
|
}
|
|
8642
9160
|
} catch (err) {
|
|
8643
9161
|
warnings.push(
|
|
@@ -8655,13 +9173,14 @@ async function importShard(db, data, options) {
|
|
|
8655
9173
|
};
|
|
8656
9174
|
}
|
|
8657
9175
|
async function resolveEmbeddingSetIdForServerEmbedding(db, model, vector) {
|
|
9176
|
+
const modelName = model ?? "unknown";
|
|
8658
9177
|
const dimension = vectorDimension(vector);
|
|
8659
9178
|
const existing = await db.query(
|
|
8660
9179
|
`SELECT id FROM embedding_set
|
|
8661
9180
|
WHERE model_name = $1 AND dimensions = $2
|
|
8662
9181
|
ORDER BY created_at, id
|
|
8663
9182
|
LIMIT 1`,
|
|
8664
|
-
[
|
|
9183
|
+
[modelName, dimension]
|
|
8665
9184
|
);
|
|
8666
9185
|
if (existing.rows[0]?.id) return existing.rows[0].id;
|
|
8667
9186
|
const id = generateId();
|
|
@@ -8670,7 +9189,7 @@ async function resolveEmbeddingSetIdForServerEmbedding(db, model, vector) {
|
|
|
8670
9189
|
id, name, slug, description, purpose, document_count, embedding_count,
|
|
8671
9190
|
is_system, keywords_json, model_name, dimensions, kind, created_at, updated_at
|
|
8672
9191
|
) VALUES ($1, $2, $3, NULL, NULL, 0, 0, false, '[]'::jsonb, $2, $4, 'physical', now(), now())`,
|
|
8673
|
-
[id,
|
|
9192
|
+
[id, modelName, slugifyServerEmbeddingSet(modelName), dimension]
|
|
8674
9193
|
);
|
|
8675
9194
|
return id;
|
|
8676
9195
|
}
|
|
@@ -8889,7 +9408,7 @@ var knowledge_shard_schema_default = {
|
|
|
8889
9408
|
link: {
|
|
8890
9409
|
type: "object",
|
|
8891
9410
|
additionalProperties: false,
|
|
8892
|
-
required: ["id", "from_note_id", "to_note_id", "
|
|
9411
|
+
required: ["id", "from_note_id", "to_note_id", "kind", "score", "created_at"],
|
|
8893
9412
|
properties: {
|
|
8894
9413
|
id: { type: "string", minLength: 1 },
|
|
8895
9414
|
from_note_id: { type: "string", minLength: 1 },
|
|
@@ -8904,7 +9423,7 @@ var knowledge_shard_schema_default = {
|
|
|
8904
9423
|
embeddingSet: {
|
|
8905
9424
|
type: "object",
|
|
8906
9425
|
additionalProperties: false,
|
|
8907
|
-
required: ["id", "
|
|
9426
|
+
required: ["id", "model", "dimension"],
|
|
8908
9427
|
properties: {
|
|
8909
9428
|
id: { type: "string", minLength: 1 },
|
|
8910
9429
|
name: { type: "string" },
|
|
@@ -8932,10 +9451,11 @@ var knowledge_shard_schema_default = {
|
|
|
8932
9451
|
embeddingSetMember: {
|
|
8933
9452
|
type: "object",
|
|
8934
9453
|
additionalProperties: false,
|
|
8935
|
-
required: ["embedding_set_id", "note_id"
|
|
9454
|
+
required: ["embedding_set_id", "note_id"],
|
|
8936
9455
|
properties: {
|
|
8937
9456
|
embedding_set_id: { type: "string", minLength: 1 },
|
|
8938
9457
|
note_id: { type: "string", minLength: 1 },
|
|
9458
|
+
embedding_id: { type: "string", minLength: 1 },
|
|
8939
9459
|
membership_type: { type: "string" },
|
|
8940
9460
|
added_at: { $ref: "#/$defs/isoDateTime" },
|
|
8941
9461
|
added_by: { type: ["string", "null"] }
|
|
@@ -8959,7 +9479,7 @@ var knowledge_shard_schema_default = {
|
|
|
8959
9479
|
embedding: {
|
|
8960
9480
|
type: "object",
|
|
8961
9481
|
additionalProperties: false,
|
|
8962
|
-
required: ["id", "note_id", "
|
|
9482
|
+
required: ["id", "note_id", "vector"],
|
|
8963
9483
|
properties: {
|
|
8964
9484
|
id: { type: "string", minLength: 1 },
|
|
8965
9485
|
note_id: { type: "string", minLength: 1 },
|
|
@@ -9843,8 +10363,6 @@ function clearPrefetchedShard(url) {
|
|
|
9843
10363
|
}
|
|
9844
10364
|
warmStore.delete(url);
|
|
9845
10365
|
}
|
|
9846
|
-
|
|
9847
|
-
// src/aiwg-index.ts
|
|
9848
10366
|
var AIWG_SCAN_REQUIRED_FIELDS = [
|
|
9849
10367
|
"schema_version",
|
|
9850
10368
|
"id",
|
|
@@ -11659,28 +12177,281 @@ function communityIdsFor(item, options) {
|
|
|
11659
12177
|
if (item.concepts.length > 0) return item.concepts.map((concept) => `concept:${concept}`);
|
|
11660
12178
|
return [`type:${item.type}`];
|
|
11661
12179
|
}
|
|
11662
|
-
|
|
11663
|
-
|
|
11664
|
-
var
|
|
11665
|
-
|
|
11666
|
-
|
|
11667
|
-
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
11671
|
-
|
|
11672
|
-
|
|
11673
|
-
|
|
11674
|
-
|
|
11675
|
-
|
|
11676
|
-
|
|
11677
|
-
|
|
11678
|
-
|
|
11679
|
-
|
|
11680
|
-
|
|
11681
|
-
|
|
11682
|
-
|
|
11683
|
-
|
|
12180
|
+
var AIWG_SHARD_UUID_NAMESPACE = "7ab5d1f8-29d2-5e35-9e2f-3a45de171a9e";
|
|
12181
|
+
var shardEncoder = new TextEncoder();
|
|
12182
|
+
var shardDecoder = new TextDecoder();
|
|
12183
|
+
function aiwgShardUuid(kind, id) {
|
|
12184
|
+
return v5(`${kind}:${id}`, AIWG_SHARD_UUID_NAMESPACE);
|
|
12185
|
+
}
|
|
12186
|
+
function aiwgShardTimestamp(value, fallback) {
|
|
12187
|
+
if (!value || Number.isNaN(Date.parse(value))) return fallback;
|
|
12188
|
+
return new Date(value).toISOString();
|
|
12189
|
+
}
|
|
12190
|
+
function aiwgRecordTitle(record) {
|
|
12191
|
+
return record.title ?? record.search?.title ?? record.search?.name ?? record.id;
|
|
12192
|
+
}
|
|
12193
|
+
function aiwgRecordContent(record) {
|
|
12194
|
+
if (record.text) return record.text;
|
|
12195
|
+
if (record.search?.body) return record.search.body;
|
|
12196
|
+
const chunks = record.chunks?.map((chunk) => chunk.text ?? chunk.body ?? chunk.summary ?? "").filter(Boolean);
|
|
12197
|
+
if (chunks?.length) return chunks.join("\n\n");
|
|
12198
|
+
return record.search?.summary ?? "";
|
|
12199
|
+
}
|
|
12200
|
+
function aiwgShardMetadata(index, record) {
|
|
12201
|
+
return {
|
|
12202
|
+
aiwg_fortemi_index: {
|
|
12203
|
+
envelope: {
|
|
12204
|
+
schema_version: index.schema_version,
|
|
12205
|
+
generated_at: index.generated_at,
|
|
12206
|
+
source: index.source,
|
|
12207
|
+
...index.compatibility ? { compatibility: index.compatibility } : {}
|
|
12208
|
+
},
|
|
12209
|
+
record
|
|
12210
|
+
}
|
|
12211
|
+
};
|
|
12212
|
+
}
|
|
12213
|
+
function encodeJsonLines(values) {
|
|
12214
|
+
return shardEncoder.encode(values.map((value) => JSON.stringify(value)).join("\n"));
|
|
12215
|
+
}
|
|
12216
|
+
async function aiwgFortemiIndexToKnowledgeShard(index, options = {}) {
|
|
12217
|
+
const validation = validateAiwgFortemiIndexExport(index);
|
|
12218
|
+
if (!validation.valid) {
|
|
12219
|
+
throw new Error(`Invalid AIWG Fortemi index export:
|
|
12220
|
+
${validation.errors.join("\n")}`);
|
|
12221
|
+
}
|
|
12222
|
+
if (index.schema_version !== "aiwg.fortemi.index.export.v2") {
|
|
12223
|
+
throw new Error("Knowledge Shard conversion requires aiwg.fortemi.index.export.v2");
|
|
12224
|
+
}
|
|
12225
|
+
const createdAt = aiwgShardTimestamp(options.createdAt ?? index.generated_at, (/* @__PURE__ */ new Date()).toISOString());
|
|
12226
|
+
const noteIds = new Map(index.items.map((record) => [record.id, aiwgShardUuid("record", record.id)]));
|
|
12227
|
+
const notes = index.items.map((record) => ({
|
|
12228
|
+
id: noteIds.get(record.id),
|
|
12229
|
+
title: aiwgRecordTitle(record),
|
|
12230
|
+
original_content: aiwgRecordContent(record),
|
|
12231
|
+
revised_content: null,
|
|
12232
|
+
metadata: aiwgShardMetadata(index, record),
|
|
12233
|
+
format: "markdown",
|
|
12234
|
+
source: "aiwg-index",
|
|
12235
|
+
starred: false,
|
|
12236
|
+
archived: false,
|
|
12237
|
+
tags: [...new Set(record.tags)].sort(),
|
|
12238
|
+
created_at: aiwgShardTimestamp(record.source.updated_at ?? record.updated_at, createdAt),
|
|
12239
|
+
updated_at: aiwgShardTimestamp(record.updated_at, createdAt),
|
|
12240
|
+
deleted_at: null
|
|
12241
|
+
}));
|
|
12242
|
+
const links = [];
|
|
12243
|
+
for (const record of index.items) {
|
|
12244
|
+
for (const [position, relationship] of record.relationships.entries()) {
|
|
12245
|
+
const targetNoteId = noteIds.get(relationship.target_id) ?? null;
|
|
12246
|
+
links.push({
|
|
12247
|
+
id: aiwgShardUuid(
|
|
12248
|
+
"relationship",
|
|
12249
|
+
`${record.id}\0${position}\0${relationship.type}\0${relationship.target_id}`
|
|
12250
|
+
),
|
|
12251
|
+
from_note_id: noteIds.get(record.id),
|
|
12252
|
+
to_note_id: targetNoteId,
|
|
12253
|
+
to_url: targetNoteId ? null : `aiwg://record/${encodeURIComponent(relationship.target_id)}`,
|
|
12254
|
+
kind: relationship.type,
|
|
12255
|
+
score: relationship.confidence ?? null,
|
|
12256
|
+
created_at: aiwgShardTimestamp(record.updated_at, createdAt),
|
|
12257
|
+
metadata: {
|
|
12258
|
+
aiwg_fortemi_index: {
|
|
12259
|
+
source_record_id: record.id,
|
|
12260
|
+
target_record_id: relationship.target_id,
|
|
12261
|
+
relationship
|
|
12262
|
+
}
|
|
12263
|
+
}
|
|
12264
|
+
});
|
|
12265
|
+
}
|
|
12266
|
+
}
|
|
12267
|
+
const defaultScheme = index.source.graph ?? index.source.repo;
|
|
12268
|
+
const concepts = /* @__PURE__ */ new Map();
|
|
12269
|
+
for (const record of index.items) {
|
|
12270
|
+
for (const conceptId of record.concepts) {
|
|
12271
|
+
concepts.set(conceptId, {
|
|
12272
|
+
concept: { id: conceptId, prefLabel: conceptId },
|
|
12273
|
+
scheme: defaultScheme
|
|
12274
|
+
});
|
|
12275
|
+
}
|
|
12276
|
+
for (const concept of record.skos_concepts ?? []) {
|
|
12277
|
+
concepts.set(concept.id, { concept, scheme: concept.scheme ?? defaultScheme });
|
|
12278
|
+
}
|
|
12279
|
+
for (const relation of record.skos_relations ?? []) {
|
|
12280
|
+
if (!concepts.has(relation.source_id)) {
|
|
12281
|
+
concepts.set(relation.source_id, {
|
|
12282
|
+
concept: { id: relation.source_id, prefLabel: relation.source_id },
|
|
12283
|
+
scheme: defaultScheme
|
|
12284
|
+
});
|
|
12285
|
+
}
|
|
12286
|
+
if (!concepts.has(relation.target_id)) {
|
|
12287
|
+
concepts.set(relation.target_id, {
|
|
12288
|
+
concept: { id: relation.target_id, prefLabel: relation.target_id },
|
|
12289
|
+
scheme: defaultScheme
|
|
12290
|
+
});
|
|
12291
|
+
}
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
const schemeNames = [...new Set([...concepts.values()].map(({ scheme }) => scheme))].sort();
|
|
12295
|
+
const schemes = schemeNames.map((scheme) => ({
|
|
12296
|
+
id: aiwgShardUuid("skos-scheme", scheme),
|
|
12297
|
+
title: scheme,
|
|
12298
|
+
description: `SKOS scheme projected from ${index.source.repo}`,
|
|
12299
|
+
created_at: createdAt,
|
|
12300
|
+
updated_at: createdAt
|
|
12301
|
+
}));
|
|
12302
|
+
const shardConcepts = [...concepts.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([id, { concept, scheme }]) => ({
|
|
12303
|
+
id: aiwgShardUuid("skos-concept", id),
|
|
12304
|
+
scheme_id: aiwgShardUuid("skos-scheme", scheme),
|
|
12305
|
+
pref_label: concept.prefLabel,
|
|
12306
|
+
alt_labels: concept.altLabels ?? [],
|
|
12307
|
+
definition: concept.definition ?? null,
|
|
12308
|
+
created_at: createdAt,
|
|
12309
|
+
updated_at: createdAt
|
|
12310
|
+
}));
|
|
12311
|
+
const noteSkosTags = [];
|
|
12312
|
+
const skosRelations = [];
|
|
12313
|
+
const provenanceEdges = [];
|
|
12314
|
+
for (const record of index.items) {
|
|
12315
|
+
const recordConceptIds = /* @__PURE__ */ new Set([
|
|
12316
|
+
...record.concepts,
|
|
12317
|
+
...(record.skos_concepts ?? []).map((concept) => concept.id)
|
|
12318
|
+
]);
|
|
12319
|
+
for (const conceptId of [...recordConceptIds].sort()) {
|
|
12320
|
+
noteSkosTags.push({
|
|
12321
|
+
id: aiwgShardUuid("note-skos-tag", `${record.id}\0${conceptId}`),
|
|
12322
|
+
note_id: noteIds.get(record.id),
|
|
12323
|
+
concept_id: aiwgShardUuid("skos-concept", conceptId),
|
|
12324
|
+
created_at: createdAt
|
|
12325
|
+
});
|
|
12326
|
+
}
|
|
12327
|
+
for (const [position, relation] of (record.skos_relations ?? []).entries()) {
|
|
12328
|
+
const relationType = relation.type === "broader" || relation.type === "narrower" ? relation.type : "related";
|
|
12329
|
+
skosRelations.push({
|
|
12330
|
+
id: aiwgShardUuid(
|
|
12331
|
+
"skos-relation",
|
|
12332
|
+
`${record.id}\0${position}\0${relation.source_id}\0${relation.type}\0${relation.target_id}`
|
|
12333
|
+
),
|
|
12334
|
+
source_concept_id: aiwgShardUuid("skos-concept", relation.source_id),
|
|
12335
|
+
target_concept_id: aiwgShardUuid("skos-concept", relation.target_id),
|
|
12336
|
+
relation_type: relationType,
|
|
12337
|
+
created_at: createdAt
|
|
12338
|
+
});
|
|
12339
|
+
}
|
|
12340
|
+
for (const [position, provenance] of record.provenance.entries()) {
|
|
12341
|
+
provenanceEdges.push({
|
|
12342
|
+
id: aiwgShardUuid("provenance", `${record.id}\0field\0${position}`),
|
|
12343
|
+
entity_type: "note",
|
|
12344
|
+
entity_id: noteIds.get(record.id),
|
|
12345
|
+
activity: "source",
|
|
12346
|
+
agent: provenance.source,
|
|
12347
|
+
started_at: aiwgShardTimestamp(record.updated_at, createdAt),
|
|
12348
|
+
ended_at: null,
|
|
12349
|
+
attributes: { aiwg_fortemi_index: { provenance } }
|
|
12350
|
+
});
|
|
12351
|
+
}
|
|
12352
|
+
for (const [position, event] of (record.provenance_events ?? []).entries()) {
|
|
12353
|
+
provenanceEdges.push({
|
|
12354
|
+
id: aiwgShardUuid("provenance", event.id ?? `${record.id}\0event\0${position}`),
|
|
12355
|
+
entity_type: "note",
|
|
12356
|
+
entity_id: noteIds.get(record.id),
|
|
12357
|
+
activity: event.activity,
|
|
12358
|
+
agent: event.agent ?? "aiwg-index",
|
|
12359
|
+
started_at: aiwgShardTimestamp(event.started_at, createdAt),
|
|
12360
|
+
ended_at: event.ended_at ? aiwgShardTimestamp(event.ended_at, createdAt) : null,
|
|
12361
|
+
attributes: { aiwg_fortemi_index: { event } }
|
|
12362
|
+
});
|
|
12363
|
+
}
|
|
12364
|
+
}
|
|
12365
|
+
const files = /* @__PURE__ */ new Map();
|
|
12366
|
+
const components = ["notes", "tags"];
|
|
12367
|
+
const counts = {
|
|
12368
|
+
notes: notes.length,
|
|
12369
|
+
tags: [...new Set(notes.flatMap((note) => note.tags))].length
|
|
12370
|
+
};
|
|
12371
|
+
files.set("notes.jsonl", encodeJsonLines(notes));
|
|
12372
|
+
files.set("tags.json", shardEncoder.encode(JSON.stringify(
|
|
12373
|
+
[...new Set(notes.flatMap((note) => note.tags))].sort().map((name) => ({ name, created_at: createdAt }))
|
|
12374
|
+
)));
|
|
12375
|
+
const addComponent = (component, filename, values, jsonLines = true) => {
|
|
12376
|
+
if (values.length === 0) return;
|
|
12377
|
+
components.push(component);
|
|
12378
|
+
counts[component] = values.length;
|
|
12379
|
+
files.set(filename, jsonLines ? encodeJsonLines(values) : shardEncoder.encode(JSON.stringify(values)));
|
|
12380
|
+
};
|
|
12381
|
+
addComponent("links", "links.jsonl", links);
|
|
12382
|
+
if (options.includeNativeRichComponents) {
|
|
12383
|
+
addComponent("skos_schemes", "skos_schemes.json", schemes, false);
|
|
12384
|
+
addComponent("skos_concepts", "skos_concepts.json", shardConcepts, false);
|
|
12385
|
+
addComponent("skos_relations", "skos_relations.jsonl", skosRelations);
|
|
12386
|
+
addComponent("note_skos_tags", "note_skos_tags.jsonl", noteSkosTags);
|
|
12387
|
+
addComponent("provenance_edges", "provenance_edges.jsonl", provenanceEdges);
|
|
12388
|
+
}
|
|
12389
|
+
const checksums = {};
|
|
12390
|
+
for (const [filename, bytes] of files) checksums[filename] = await sha256Hex(bytes);
|
|
12391
|
+
const manifest = {
|
|
12392
|
+
version: CURRENT_SHARD_VERSION,
|
|
12393
|
+
matric_version: options.matricVersion ?? "fortemi-core-aiwg-index",
|
|
12394
|
+
format: SHARD_FORMAT,
|
|
12395
|
+
created_at: createdAt,
|
|
12396
|
+
components,
|
|
12397
|
+
counts,
|
|
12398
|
+
checksums,
|
|
12399
|
+
min_reader_version: CURRENT_SHARD_VERSION,
|
|
12400
|
+
migrated_from: null,
|
|
12401
|
+
migration_history: []
|
|
12402
|
+
};
|
|
12403
|
+
files.set("manifest.json", shardEncoder.encode(JSON.stringify(manifest, null, 2)));
|
|
12404
|
+
return packTarGz(files);
|
|
12405
|
+
}
|
|
12406
|
+
function aiwgFortemiIndexFromKnowledgeShard(bytes) {
|
|
12407
|
+
const files = unpackTarGz(bytes);
|
|
12408
|
+
const noteBytes = files.get("notes.jsonl");
|
|
12409
|
+
const notes = (noteBytes ? shardDecoder.decode(noteBytes) : "").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
12410
|
+
if (notes.length === 0) throw new Error("Knowledge Shard contains no AIWG index notes");
|
|
12411
|
+
let envelope;
|
|
12412
|
+
const items = [];
|
|
12413
|
+
for (const note of notes) {
|
|
12414
|
+
const metadata = note.metadata?.aiwg_fortemi_index;
|
|
12415
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
|
|
12416
|
+
throw new Error(`Knowledge Shard note ${note.id} has no AIWG index metadata`);
|
|
12417
|
+
}
|
|
12418
|
+
const value = metadata;
|
|
12419
|
+
envelope ??= value.envelope;
|
|
12420
|
+
items.push(value.record);
|
|
12421
|
+
}
|
|
12422
|
+
const restored = {
|
|
12423
|
+
...envelope,
|
|
12424
|
+
items: items.sort((left, right) => left.id.localeCompare(right.id))
|
|
12425
|
+
};
|
|
12426
|
+
const validation = validateAiwgFortemiIndexExport(restored);
|
|
12427
|
+
if (!validation.valid) {
|
|
12428
|
+
throw new Error(`Knowledge Shard contains invalid AIWG index metadata:
|
|
12429
|
+
${validation.errors.join("\n")}`);
|
|
12430
|
+
}
|
|
12431
|
+
return restored;
|
|
12432
|
+
}
|
|
12433
|
+
|
|
12434
|
+
// schemas/aiwg-fortemi-index-export.schema.json
|
|
12435
|
+
var aiwg_fortemi_index_export_schema_default = {
|
|
12436
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
12437
|
+
$id: "https://aiwg.io/schemas/aiwg-fortemi-index-export.json",
|
|
12438
|
+
title: "AIWG Fortemi Index Export",
|
|
12439
|
+
type: "object",
|
|
12440
|
+
additionalProperties: false,
|
|
12441
|
+
required: ["schema_version", "generated_at", "source", "items"],
|
|
12442
|
+
properties: {
|
|
12443
|
+
schema_version: {
|
|
12444
|
+
enum: ["aiwg.fortemi.index.export.v1", "aiwg.fortemi.index.export.v2"]
|
|
12445
|
+
},
|
|
12446
|
+
generated_at: {
|
|
12447
|
+
type: "string",
|
|
12448
|
+
format: "date-time"
|
|
12449
|
+
},
|
|
12450
|
+
source: {
|
|
12451
|
+
type: "object",
|
|
12452
|
+
additionalProperties: false,
|
|
12453
|
+
required: ["repo", "privacy"],
|
|
12454
|
+
properties: {
|
|
11684
12455
|
repo: {
|
|
11685
12456
|
type: "string",
|
|
11686
12457
|
minLength: 1
|
|
@@ -12377,9 +13148,1369 @@ function validateAiwgFortemiProjectedRecordSchema(value) {
|
|
|
12377
13148
|
return { valid, errors: formatErrors2(validate.errors) };
|
|
12378
13149
|
}
|
|
12379
13150
|
|
|
13151
|
+
// src/records/types.ts
|
|
13152
|
+
var RECORD_COLLECTIONS = [
|
|
13153
|
+
"note",
|
|
13154
|
+
"note_original",
|
|
13155
|
+
"note_revised_current",
|
|
13156
|
+
"note_tag",
|
|
13157
|
+
"link",
|
|
13158
|
+
"collection",
|
|
13159
|
+
"collection_note",
|
|
13160
|
+
"attachment",
|
|
13161
|
+
"attachment_blob"
|
|
13162
|
+
];
|
|
13163
|
+
var RECORD_STORE_CAPABILITIES = {
|
|
13164
|
+
crud: true,
|
|
13165
|
+
journal: true,
|
|
13166
|
+
boundedTextScan: true,
|
|
13167
|
+
fullTextSearch: false,
|
|
13168
|
+
vectorSearch: false,
|
|
13169
|
+
sqlJoins: false
|
|
13170
|
+
};
|
|
13171
|
+
|
|
13172
|
+
// src/records/memory-record-store.ts
|
|
13173
|
+
var MemoryRecordStore = class {
|
|
13174
|
+
capabilities = RECORD_STORE_CAPABILITIES;
|
|
13175
|
+
collections = /* @__PURE__ */ new Map();
|
|
13176
|
+
journal = [];
|
|
13177
|
+
seq = 0;
|
|
13178
|
+
table(collection) {
|
|
13179
|
+
let table = this.collections.get(collection);
|
|
13180
|
+
if (!table) {
|
|
13181
|
+
table = /* @__PURE__ */ new Map();
|
|
13182
|
+
this.collections.set(collection, table);
|
|
13183
|
+
}
|
|
13184
|
+
return table;
|
|
13185
|
+
}
|
|
13186
|
+
async get(collection, id) {
|
|
13187
|
+
return this.table(collection).get(id) ?? null;
|
|
13188
|
+
}
|
|
13189
|
+
async put(collection, record) {
|
|
13190
|
+
const entry = {
|
|
13191
|
+
seq: ++this.seq,
|
|
13192
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13193
|
+
op: "put",
|
|
13194
|
+
collection,
|
|
13195
|
+
id: record.id,
|
|
13196
|
+
record: structuredClone(record)
|
|
13197
|
+
};
|
|
13198
|
+
this.table(collection).set(record.id, structuredClone(record));
|
|
13199
|
+
this.journal.push(entry);
|
|
13200
|
+
return entry;
|
|
13201
|
+
}
|
|
13202
|
+
async remove(collection, id) {
|
|
13203
|
+
const entry = {
|
|
13204
|
+
seq: ++this.seq,
|
|
13205
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13206
|
+
op: "delete",
|
|
13207
|
+
collection,
|
|
13208
|
+
id
|
|
13209
|
+
};
|
|
13210
|
+
this.table(collection).delete(id);
|
|
13211
|
+
this.journal.push(entry);
|
|
13212
|
+
return entry;
|
|
13213
|
+
}
|
|
13214
|
+
async list(collection, opts) {
|
|
13215
|
+
const all = [...this.table(collection).values()];
|
|
13216
|
+
return opts?.limit !== void 0 ? all.slice(0, opts.limit) : all;
|
|
13217
|
+
}
|
|
13218
|
+
async journalSince(sinceSeq, limit) {
|
|
13219
|
+
const entries = this.journal.filter((e) => e.seq > sinceSeq);
|
|
13220
|
+
return limit !== void 0 ? entries.slice(0, limit) : entries;
|
|
13221
|
+
}
|
|
13222
|
+
async headSeq() {
|
|
13223
|
+
return this.seq;
|
|
13224
|
+
}
|
|
13225
|
+
async close() {
|
|
13226
|
+
}
|
|
13227
|
+
};
|
|
13228
|
+
|
|
13229
|
+
// src/records/idb-record-store.ts
|
|
13230
|
+
var DB_VERSION = 1;
|
|
13231
|
+
var JOURNAL_STORE = "journal";
|
|
13232
|
+
var META_STORE = "meta";
|
|
13233
|
+
var RECORD_SCHEMA_VERSION = 1;
|
|
13234
|
+
function requestToPromise(request) {
|
|
13235
|
+
return new Promise((resolve, reject) => {
|
|
13236
|
+
request.onsuccess = () => resolve(request.result);
|
|
13237
|
+
request.onerror = () => reject(request.error);
|
|
13238
|
+
});
|
|
13239
|
+
}
|
|
13240
|
+
function transactionComplete(tx) {
|
|
13241
|
+
return new Promise((resolve, reject) => {
|
|
13242
|
+
tx.oncomplete = () => resolve();
|
|
13243
|
+
tx.onabort = () => reject(tx.error);
|
|
13244
|
+
tx.onerror = () => reject(tx.error);
|
|
13245
|
+
});
|
|
13246
|
+
}
|
|
13247
|
+
var IdbRecordStore = class _IdbRecordStore {
|
|
13248
|
+
constructor(db) {
|
|
13249
|
+
this.db = db;
|
|
13250
|
+
}
|
|
13251
|
+
capabilities = RECORD_STORE_CAPABILITIES;
|
|
13252
|
+
static async open(archiveName, options) {
|
|
13253
|
+
const factory = options?.indexedDB ?? globalThis.indexedDB;
|
|
13254
|
+
if (!factory) {
|
|
13255
|
+
throw new Error("IdbRecordStore requires IndexedDB (none available in this environment)");
|
|
13256
|
+
}
|
|
13257
|
+
const db = await new Promise((resolve, reject) => {
|
|
13258
|
+
const request = factory.open(`fortemi-${archiveName}-records`, DB_VERSION);
|
|
13259
|
+
request.onupgradeneeded = () => {
|
|
13260
|
+
const database = request.result;
|
|
13261
|
+
for (const collection of RECORD_COLLECTIONS) {
|
|
13262
|
+
if (!database.objectStoreNames.contains(collection)) {
|
|
13263
|
+
database.createObjectStore(collection, { keyPath: "id" });
|
|
13264
|
+
}
|
|
13265
|
+
}
|
|
13266
|
+
if (!database.objectStoreNames.contains(JOURNAL_STORE)) {
|
|
13267
|
+
database.createObjectStore(JOURNAL_STORE, { keyPath: "seq", autoIncrement: true });
|
|
13268
|
+
}
|
|
13269
|
+
if (!database.objectStoreNames.contains(META_STORE)) {
|
|
13270
|
+
database.createObjectStore(META_STORE);
|
|
13271
|
+
}
|
|
13272
|
+
};
|
|
13273
|
+
request.onsuccess = () => resolve(request.result);
|
|
13274
|
+
request.onerror = () => reject(request.error);
|
|
13275
|
+
});
|
|
13276
|
+
const store = new _IdbRecordStore(db);
|
|
13277
|
+
await store.ensureSchemaVersion();
|
|
13278
|
+
return store;
|
|
13279
|
+
}
|
|
13280
|
+
async ensureSchemaVersion() {
|
|
13281
|
+
const tx = this.db.transaction(META_STORE, "readwrite");
|
|
13282
|
+
const meta = tx.objectStore(META_STORE);
|
|
13283
|
+
const current = await requestToPromise(meta.get("schemaVersion"));
|
|
13284
|
+
if (current === void 0) {
|
|
13285
|
+
meta.put(RECORD_SCHEMA_VERSION, "schemaVersion");
|
|
13286
|
+
} else if (current > RECORD_SCHEMA_VERSION) {
|
|
13287
|
+
throw new Error(
|
|
13288
|
+
`IdbRecordStore: records were written by a newer schema (v${current} > v${RECORD_SCHEMA_VERSION}); refusing to open`
|
|
13289
|
+
);
|
|
13290
|
+
}
|
|
13291
|
+
await transactionComplete(tx);
|
|
13292
|
+
}
|
|
13293
|
+
async get(collection, id) {
|
|
13294
|
+
const tx = this.db.transaction(collection, "readonly");
|
|
13295
|
+
const result = await requestToPromise(
|
|
13296
|
+
tx.objectStore(collection).get(id)
|
|
13297
|
+
);
|
|
13298
|
+
return result ?? null;
|
|
13299
|
+
}
|
|
13300
|
+
async put(collection, record) {
|
|
13301
|
+
const tx = this.db.transaction([collection, JOURNAL_STORE], "readwrite");
|
|
13302
|
+
tx.objectStore(collection).put(record);
|
|
13303
|
+
const pending = {
|
|
13304
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13305
|
+
op: "put",
|
|
13306
|
+
collection,
|
|
13307
|
+
id: record.id,
|
|
13308
|
+
record
|
|
13309
|
+
};
|
|
13310
|
+
const seqReq = tx.objectStore(JOURNAL_STORE).add(pending);
|
|
13311
|
+
await transactionComplete(tx);
|
|
13312
|
+
return { ...pending, seq: seqReq.result };
|
|
13313
|
+
}
|
|
13314
|
+
async remove(collection, id) {
|
|
13315
|
+
const tx = this.db.transaction([collection, JOURNAL_STORE], "readwrite");
|
|
13316
|
+
tx.objectStore(collection).delete(id);
|
|
13317
|
+
const pending = {
|
|
13318
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13319
|
+
op: "delete",
|
|
13320
|
+
collection,
|
|
13321
|
+
id
|
|
13322
|
+
};
|
|
13323
|
+
const seqReq = tx.objectStore(JOURNAL_STORE).add(pending);
|
|
13324
|
+
await transactionComplete(tx);
|
|
13325
|
+
return { ...pending, seq: seqReq.result };
|
|
13326
|
+
}
|
|
13327
|
+
async list(collection, opts) {
|
|
13328
|
+
const tx = this.db.transaction(collection, "readonly");
|
|
13329
|
+
const store = tx.objectStore(collection);
|
|
13330
|
+
const all = await requestToPromise(
|
|
13331
|
+
opts?.limit !== void 0 ? store.getAll(void 0, opts.limit) : store.getAll()
|
|
13332
|
+
);
|
|
13333
|
+
return all;
|
|
13334
|
+
}
|
|
13335
|
+
async journalSince(sinceSeq, limit) {
|
|
13336
|
+
const tx = this.db.transaction(JOURNAL_STORE, "readonly");
|
|
13337
|
+
const range = IDBKeyRange.lowerBound(sinceSeq, true);
|
|
13338
|
+
const entries = await requestToPromise(
|
|
13339
|
+
limit !== void 0 ? tx.objectStore(JOURNAL_STORE).getAll(range, limit) : tx.objectStore(JOURNAL_STORE).getAll(range)
|
|
13340
|
+
);
|
|
13341
|
+
return entries;
|
|
13342
|
+
}
|
|
13343
|
+
async headSeq() {
|
|
13344
|
+
const tx = this.db.transaction(JOURNAL_STORE, "readonly");
|
|
13345
|
+
const cursor = await requestToPromise(
|
|
13346
|
+
tx.objectStore(JOURNAL_STORE).openCursor(null, "prev")
|
|
13347
|
+
);
|
|
13348
|
+
return cursor ? cursor.value.seq : 0;
|
|
13349
|
+
}
|
|
13350
|
+
async close() {
|
|
13351
|
+
this.db.close();
|
|
13352
|
+
}
|
|
13353
|
+
};
|
|
13354
|
+
function createRecordStore(archiveName, options) {
|
|
13355
|
+
return IdbRecordStore.open(archiveName, options);
|
|
13356
|
+
}
|
|
13357
|
+
|
|
13358
|
+
// src/records/canonical-notes-repository.ts
|
|
13359
|
+
function nowIso() {
|
|
13360
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
13361
|
+
}
|
|
13362
|
+
var CanonicalNotesRepository = class {
|
|
13363
|
+
constructor(store) {
|
|
13364
|
+
this.store = store;
|
|
13365
|
+
}
|
|
13366
|
+
// ── Notes ─────────────────────────────────────────────────────────────────
|
|
13367
|
+
async create(input) {
|
|
13368
|
+
const noteId = input.id ?? generateId();
|
|
13369
|
+
const ts = nowIso();
|
|
13370
|
+
await this.store.put("note", {
|
|
13371
|
+
id: noteId,
|
|
13372
|
+
archive_id: null,
|
|
13373
|
+
title: input.title ?? null,
|
|
13374
|
+
format: input.format ?? "markdown",
|
|
13375
|
+
source: input.source ?? "user",
|
|
13376
|
+
visibility: input.visibility ?? "private",
|
|
13377
|
+
revision_mode: "standard",
|
|
13378
|
+
is_starred: false,
|
|
13379
|
+
is_pinned: false,
|
|
13380
|
+
is_archived: false,
|
|
13381
|
+
created_at: ts,
|
|
13382
|
+
updated_at: ts,
|
|
13383
|
+
deleted_at: null
|
|
13384
|
+
});
|
|
13385
|
+
await this.store.put("note_original", {
|
|
13386
|
+
id: generateId(),
|
|
13387
|
+
note_id: noteId,
|
|
13388
|
+
content: input.content,
|
|
13389
|
+
content_hash: computeHash(new TextEncoder().encode(input.content)),
|
|
13390
|
+
created_at: ts
|
|
13391
|
+
});
|
|
13392
|
+
await this.store.put("note_revised_current", {
|
|
13393
|
+
id: noteId,
|
|
13394
|
+
content: input.content,
|
|
13395
|
+
ai_metadata: null,
|
|
13396
|
+
generation_count: 0,
|
|
13397
|
+
model: null,
|
|
13398
|
+
is_user_edited: false,
|
|
13399
|
+
updated_at: ts
|
|
13400
|
+
});
|
|
13401
|
+
return await this.get(noteId);
|
|
13402
|
+
}
|
|
13403
|
+
async get(noteId) {
|
|
13404
|
+
const note = await this.store.get("note", noteId);
|
|
13405
|
+
if (!note) return null;
|
|
13406
|
+
const revised = await this.store.get("note_revised_current", noteId);
|
|
13407
|
+
const originals = (await this.store.list("note_original")).filter(
|
|
13408
|
+
(o) => o.note_id === noteId
|
|
13409
|
+
);
|
|
13410
|
+
const tags = (await this.store.list("note_tag")).filter((t) => t.note_id === noteId).map((t) => t.tag).sort();
|
|
13411
|
+
return {
|
|
13412
|
+
note,
|
|
13413
|
+
original_content: originals[0]?.content ?? "",
|
|
13414
|
+
revised_content: revised?.content ?? originals[0]?.content ?? "",
|
|
13415
|
+
tags
|
|
13416
|
+
};
|
|
13417
|
+
}
|
|
13418
|
+
async update(noteId, input) {
|
|
13419
|
+
const note = await this.store.get("note", noteId);
|
|
13420
|
+
if (!note) throw new Error(`Note not found: ${noteId}`);
|
|
13421
|
+
const ts = nowIso();
|
|
13422
|
+
await this.store.put("note", {
|
|
13423
|
+
...note,
|
|
13424
|
+
title: input.title !== void 0 ? input.title : note.title,
|
|
13425
|
+
format: input.format ?? note.format,
|
|
13426
|
+
visibility: input.visibility ?? note.visibility,
|
|
13427
|
+
is_starred: input.is_starred ?? note.is_starred,
|
|
13428
|
+
is_pinned: input.is_pinned ?? note.is_pinned,
|
|
13429
|
+
is_archived: input.is_archived ?? note.is_archived,
|
|
13430
|
+
updated_at: ts
|
|
13431
|
+
});
|
|
13432
|
+
if (input.content !== void 0) {
|
|
13433
|
+
const revised = await this.store.get("note_revised_current", noteId);
|
|
13434
|
+
await this.store.put("note_revised_current", {
|
|
13435
|
+
id: noteId,
|
|
13436
|
+
content: input.content,
|
|
13437
|
+
ai_metadata: revised?.ai_metadata ?? null,
|
|
13438
|
+
generation_count: revised?.generation_count ?? 0,
|
|
13439
|
+
model: revised?.model ?? null,
|
|
13440
|
+
is_user_edited: true,
|
|
13441
|
+
updated_at: ts
|
|
13442
|
+
});
|
|
13443
|
+
}
|
|
13444
|
+
return await this.get(noteId);
|
|
13445
|
+
}
|
|
13446
|
+
/** Soft-delete: sets `deleted_at`; the record (and history) remains. */
|
|
13447
|
+
async softDelete(noteId) {
|
|
13448
|
+
const note = await this.store.get("note", noteId);
|
|
13449
|
+
if (!note) throw new Error(`Note not found: ${noteId}`);
|
|
13450
|
+
await this.store.put("note", { ...note, deleted_at: nowIso(), updated_at: nowIso() });
|
|
13451
|
+
}
|
|
13452
|
+
async restore(noteId) {
|
|
13453
|
+
const note = await this.store.get("note", noteId);
|
|
13454
|
+
if (!note) throw new Error(`Note not found: ${noteId}`);
|
|
13455
|
+
await this.store.put("note", { ...note, deleted_at: null, updated_at: nowIso() });
|
|
13456
|
+
}
|
|
13457
|
+
/** Non-deleted notes, most recently updated first. */
|
|
13458
|
+
async listRecent(limit = 50) {
|
|
13459
|
+
const notes = (await this.store.list("note")).filter((n) => n.deleted_at === null);
|
|
13460
|
+
notes.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
13461
|
+
return notes.slice(0, limit);
|
|
13462
|
+
}
|
|
13463
|
+
/**
|
|
13464
|
+
* Bounded substring scan over title + revised content (case-insensitive).
|
|
13465
|
+
* This is deliberately not ranked FTS — see `store.capabilities`.
|
|
13466
|
+
*/
|
|
13467
|
+
async searchText(query, limit = 20) {
|
|
13468
|
+
const needle = query.toLowerCase();
|
|
13469
|
+
const revised = new Map(
|
|
13470
|
+
(await this.store.list("note_revised_current")).map((r) => [r.id, r.content])
|
|
13471
|
+
);
|
|
13472
|
+
const hits = [];
|
|
13473
|
+
for (const note of await this.store.list("note")) {
|
|
13474
|
+
if (note.deleted_at !== null) continue;
|
|
13475
|
+
const haystack = `${note.title ?? ""}
|
|
13476
|
+
${revised.get(note.id) ?? ""}`.toLowerCase();
|
|
13477
|
+
if (haystack.includes(needle)) {
|
|
13478
|
+
hits.push(note);
|
|
13479
|
+
if (hits.length >= limit) break;
|
|
13480
|
+
}
|
|
13481
|
+
}
|
|
13482
|
+
return hits;
|
|
13483
|
+
}
|
|
13484
|
+
// ── Tags ──────────────────────────────────────────────────────────────────
|
|
13485
|
+
async addTag(noteId, tag) {
|
|
13486
|
+
const existing = (await this.store.list("note_tag")).find(
|
|
13487
|
+
(t) => t.note_id === noteId && t.tag === tag
|
|
13488
|
+
);
|
|
13489
|
+
if (existing) return;
|
|
13490
|
+
await this.store.put("note_tag", {
|
|
13491
|
+
id: generateId(),
|
|
13492
|
+
note_id: noteId,
|
|
13493
|
+
tag,
|
|
13494
|
+
created_at: nowIso()
|
|
13495
|
+
});
|
|
13496
|
+
}
|
|
13497
|
+
async removeTag(noteId, tag) {
|
|
13498
|
+
const existing = (await this.store.list("note_tag")).find(
|
|
13499
|
+
(t) => t.note_id === noteId && t.tag === tag
|
|
13500
|
+
);
|
|
13501
|
+
if (existing) await this.store.remove("note_tag", existing.id);
|
|
13502
|
+
}
|
|
13503
|
+
async notesByTag(tag) {
|
|
13504
|
+
const noteIds = new Set(
|
|
13505
|
+
(await this.store.list("note_tag")).filter((t) => t.tag === tag).map((t) => t.note_id)
|
|
13506
|
+
);
|
|
13507
|
+
return (await this.store.list("note")).filter(
|
|
13508
|
+
(n) => noteIds.has(n.id) && n.deleted_at === null
|
|
13509
|
+
);
|
|
13510
|
+
}
|
|
13511
|
+
// ── Links ─────────────────────────────────────────────────────────────────
|
|
13512
|
+
async createLink(sourceNoteId, targetNoteId, linkType = "related") {
|
|
13513
|
+
const link = {
|
|
13514
|
+
id: generateId(),
|
|
13515
|
+
source_note_id: sourceNoteId,
|
|
13516
|
+
target_note_id: targetNoteId,
|
|
13517
|
+
link_type: linkType,
|
|
13518
|
+
created_at: nowIso(),
|
|
13519
|
+
deleted_at: null
|
|
13520
|
+
};
|
|
13521
|
+
await this.store.put("link", link);
|
|
13522
|
+
return link;
|
|
13523
|
+
}
|
|
13524
|
+
async softDeleteLink(linkId) {
|
|
13525
|
+
const link = await this.store.get("link", linkId);
|
|
13526
|
+
if (!link) throw new Error(`Link not found: ${linkId}`);
|
|
13527
|
+
await this.store.put("link", { ...link, deleted_at: nowIso() });
|
|
13528
|
+
}
|
|
13529
|
+
/** Active links touching a note (either direction). */
|
|
13530
|
+
async linksOf(noteId) {
|
|
13531
|
+
return (await this.store.list("link")).filter(
|
|
13532
|
+
(l) => l.deleted_at === null && (l.source_note_id === noteId || l.target_note_id === noteId)
|
|
13533
|
+
);
|
|
13534
|
+
}
|
|
13535
|
+
// ── Collections ───────────────────────────────────────────────────────────
|
|
13536
|
+
async createCollection(name, description) {
|
|
13537
|
+
const ts = nowIso();
|
|
13538
|
+
const collection = {
|
|
13539
|
+
id: generateId(),
|
|
13540
|
+
name,
|
|
13541
|
+
description: description ?? null,
|
|
13542
|
+
created_at: ts,
|
|
13543
|
+
updated_at: ts,
|
|
13544
|
+
deleted_at: null
|
|
13545
|
+
};
|
|
13546
|
+
await this.store.put("collection", collection);
|
|
13547
|
+
return collection;
|
|
13548
|
+
}
|
|
13549
|
+
async addNoteToCollection(collectionId, noteId) {
|
|
13550
|
+
const existing = (await this.store.list("collection_note")).find(
|
|
13551
|
+
(cn) => cn.collection_id === collectionId && cn.note_id === noteId
|
|
13552
|
+
);
|
|
13553
|
+
if (existing) return;
|
|
13554
|
+
await this.store.put("collection_note", {
|
|
13555
|
+
id: generateId(),
|
|
13556
|
+
collection_id: collectionId,
|
|
13557
|
+
note_id: noteId,
|
|
13558
|
+
created_at: nowIso()
|
|
13559
|
+
});
|
|
13560
|
+
}
|
|
13561
|
+
async notesInCollection(collectionId) {
|
|
13562
|
+
const noteIds = new Set(
|
|
13563
|
+
(await this.store.list("collection_note")).filter((cn) => cn.collection_id === collectionId).map((cn) => cn.note_id)
|
|
13564
|
+
);
|
|
13565
|
+
return (await this.store.list("note")).filter(
|
|
13566
|
+
(n) => noteIds.has(n.id) && n.deleted_at === null
|
|
13567
|
+
);
|
|
13568
|
+
}
|
|
13569
|
+
};
|
|
13570
|
+
|
|
13571
|
+
// src/records/canonical-attachments-repository.ts
|
|
13572
|
+
function nowIso2() {
|
|
13573
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
13574
|
+
}
|
|
13575
|
+
var CanonicalAttachmentsRepository = class {
|
|
13576
|
+
constructor(store, blobStore) {
|
|
13577
|
+
this.store = store;
|
|
13578
|
+
this.blobStore = blobStore;
|
|
13579
|
+
}
|
|
13580
|
+
/** Bytes-first attach (ADR-013 D5); dedupes on the store-computed hash. */
|
|
13581
|
+
async attach(input) {
|
|
13582
|
+
const contentHash = await this.blobStore.put(input.data);
|
|
13583
|
+
let blob = (await this.store.list("attachment_blob")).find(
|
|
13584
|
+
(b) => b.content_hash === contentHash
|
|
13585
|
+
);
|
|
13586
|
+
if (!blob) {
|
|
13587
|
+
blob = {
|
|
13588
|
+
id: generateId(),
|
|
13589
|
+
content_hash: contentHash,
|
|
13590
|
+
size_bytes: input.data.length,
|
|
13591
|
+
created_at: nowIso2()
|
|
13592
|
+
};
|
|
13593
|
+
await this.store.put("attachment_blob", blob);
|
|
13594
|
+
}
|
|
13595
|
+
const attachment = {
|
|
13596
|
+
id: generateId(),
|
|
13597
|
+
note_id: input.noteId,
|
|
13598
|
+
blob_id: blob.id,
|
|
13599
|
+
document_type_id: null,
|
|
13600
|
+
mime_type: input.mimeType ?? null,
|
|
13601
|
+
extracted_text: input.extractedText ?? null,
|
|
13602
|
+
filename: input.filename,
|
|
13603
|
+
display_name: input.displayName ?? null,
|
|
13604
|
+
position: 0,
|
|
13605
|
+
created_at: nowIso2(),
|
|
13606
|
+
deleted_at: null
|
|
13607
|
+
};
|
|
13608
|
+
await this.store.put("attachment", attachment);
|
|
13609
|
+
return attachment;
|
|
13610
|
+
}
|
|
13611
|
+
async get(id) {
|
|
13612
|
+
const attachment = await this.store.get("attachment", id);
|
|
13613
|
+
if (!attachment) throw new Error(`Attachment not found: ${id}`);
|
|
13614
|
+
return attachment;
|
|
13615
|
+
}
|
|
13616
|
+
/** Null when bytes are absent — the recoverable reference-only state. */
|
|
13617
|
+
async getBlob(attachmentId) {
|
|
13618
|
+
const checksum = await this.checksumOf(attachmentId);
|
|
13619
|
+
return checksum === null ? null : this.blobStore.read(checksum);
|
|
13620
|
+
}
|
|
13621
|
+
async hasBlob(attachmentId) {
|
|
13622
|
+
const checksum = await this.checksumOf(attachmentId);
|
|
13623
|
+
return checksum === null ? false : this.blobStore.has(checksum);
|
|
13624
|
+
}
|
|
13625
|
+
async list(noteId) {
|
|
13626
|
+
const items = (await this.store.list("attachment")).filter(
|
|
13627
|
+
(a) => a.note_id === noteId && a.deleted_at === null
|
|
13628
|
+
);
|
|
13629
|
+
items.sort(
|
|
13630
|
+
(a, b) => a.position - b.position || a.created_at.localeCompare(b.created_at)
|
|
13631
|
+
);
|
|
13632
|
+
return items;
|
|
13633
|
+
}
|
|
13634
|
+
/** Soft-delete the manifest; bytes are only swept via reconcile/gc. */
|
|
13635
|
+
async delete(id) {
|
|
13636
|
+
const attachment = await this.get(id);
|
|
13637
|
+
await this.store.put("attachment", { ...attachment, deleted_at: nowIso2() });
|
|
13638
|
+
}
|
|
13639
|
+
/** Authoritative live set: hashes referenced by non-deleted manifests. */
|
|
13640
|
+
async liveBlobChecksums() {
|
|
13641
|
+
const liveBlobIds = new Set(
|
|
13642
|
+
(await this.store.list("attachment")).filter((a) => a.deleted_at === null).map((a) => a.blob_id)
|
|
13643
|
+
);
|
|
13644
|
+
return (await this.store.list("attachment_blob")).filter((b) => liveBlobIds.has(b.id)).map((b) => b.content_hash);
|
|
13645
|
+
}
|
|
13646
|
+
/** Startup / post-quota reconciliation against canonical manifests (ADR-013 D4). */
|
|
13647
|
+
async reconcileBlobs(opts) {
|
|
13648
|
+
return this.blobStore.reconcile(await this.liveBlobChecksums(), opts);
|
|
13649
|
+
}
|
|
13650
|
+
/** Deferred reachability-based blob GC. */
|
|
13651
|
+
async gcBlobs(opts) {
|
|
13652
|
+
await this.reconcileBlobs();
|
|
13653
|
+
return this.blobStore.gc(opts);
|
|
13654
|
+
}
|
|
13655
|
+
async checksumOf(attachmentId) {
|
|
13656
|
+
const attachment = await this.get(attachmentId);
|
|
13657
|
+
const blob = await this.store.get("attachment_blob", attachment.blob_id);
|
|
13658
|
+
return blob?.content_hash ?? null;
|
|
13659
|
+
}
|
|
13660
|
+
};
|
|
13661
|
+
|
|
13662
|
+
// src/records/attachment-projection.ts
|
|
13663
|
+
async function projectAttachments(db, store) {
|
|
13664
|
+
const blobs = await store.list("attachment_blob");
|
|
13665
|
+
const attachments = await store.list("attachment");
|
|
13666
|
+
for (const blob of blobs) {
|
|
13667
|
+
await db.query(
|
|
13668
|
+
`INSERT INTO attachment_blob (id, content_hash, size_bytes, created_at)
|
|
13669
|
+
VALUES ($1, $2, $3, $4)
|
|
13670
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
13671
|
+
content_hash = EXCLUDED.content_hash,
|
|
13672
|
+
size_bytes = EXCLUDED.size_bytes`,
|
|
13673
|
+
[blob.id, blob.content_hash, blob.size_bytes, blob.created_at]
|
|
13674
|
+
);
|
|
13675
|
+
}
|
|
13676
|
+
for (const att of attachments) {
|
|
13677
|
+
await db.query(
|
|
13678
|
+
`INSERT INTO attachment (
|
|
13679
|
+
id, note_id, blob_id, document_type_id, mime_type, extracted_text,
|
|
13680
|
+
filename, display_name, position, status, created_at, deleted_at
|
|
13681
|
+
)
|
|
13682
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
13683
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
13684
|
+
mime_type = EXCLUDED.mime_type,
|
|
13685
|
+
extracted_text = EXCLUDED.extracted_text,
|
|
13686
|
+
filename = EXCLUDED.filename,
|
|
13687
|
+
display_name = EXCLUDED.display_name,
|
|
13688
|
+
position = EXCLUDED.position,
|
|
13689
|
+
status = EXCLUDED.status,
|
|
13690
|
+
deleted_at = EXCLUDED.deleted_at`,
|
|
13691
|
+
[
|
|
13692
|
+
att.id,
|
|
13693
|
+
att.note_id,
|
|
13694
|
+
att.blob_id,
|
|
13695
|
+
att.document_type_id,
|
|
13696
|
+
att.mime_type,
|
|
13697
|
+
att.extracted_text,
|
|
13698
|
+
att.filename,
|
|
13699
|
+
att.display_name,
|
|
13700
|
+
att.position,
|
|
13701
|
+
// Browser attach-time extraction: text present == processing done.
|
|
13702
|
+
att.extracted_text !== null && att.extracted_text !== "" ? "completed" : "uploaded",
|
|
13703
|
+
att.created_at,
|
|
13704
|
+
att.deleted_at
|
|
13705
|
+
]
|
|
13706
|
+
);
|
|
13707
|
+
}
|
|
13708
|
+
await db.query(
|
|
13709
|
+
`UPDATE attachment_blob ab
|
|
13710
|
+
SET reference_count = (
|
|
13711
|
+
SELECT COUNT(*) FROM attachment a
|
|
13712
|
+
WHERE a.blob_id = ab.id AND a.deleted_at IS NULL
|
|
13713
|
+
)`
|
|
13714
|
+
);
|
|
13715
|
+
return { blobs: blobs.length, attachments: attachments.length };
|
|
13716
|
+
}
|
|
13717
|
+
async function dropAttachmentProjection(db) {
|
|
13718
|
+
await db.query(`DELETE FROM attachment_embedding`);
|
|
13719
|
+
await db.query(`DELETE FROM attachment`);
|
|
13720
|
+
await db.query(`DELETE FROM attachment_blob`);
|
|
13721
|
+
}
|
|
13722
|
+
|
|
13723
|
+
// src/records/record-projection.ts
|
|
13724
|
+
async function projectNotes(db, store) {
|
|
13725
|
+
const [noteRows, originals, revised, tags, links, collections, memberships] = await Promise.all([
|
|
13726
|
+
store.list("note"),
|
|
13727
|
+
store.list("note_original"),
|
|
13728
|
+
store.list("note_revised_current"),
|
|
13729
|
+
store.list("note_tag"),
|
|
13730
|
+
store.list("link"),
|
|
13731
|
+
store.list("collection"),
|
|
13732
|
+
store.list("collection_note")
|
|
13733
|
+
]);
|
|
13734
|
+
for (const c of collections) {
|
|
13735
|
+
await db.query(
|
|
13736
|
+
`INSERT INTO collection (id, name, description, created_at, updated_at, deleted_at)
|
|
13737
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
13738
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
13739
|
+
name = EXCLUDED.name,
|
|
13740
|
+
description = EXCLUDED.description,
|
|
13741
|
+
updated_at = EXCLUDED.updated_at,
|
|
13742
|
+
deleted_at = EXCLUDED.deleted_at`,
|
|
13743
|
+
[c.id, c.name, c.description, c.created_at, c.updated_at, c.deleted_at]
|
|
13744
|
+
);
|
|
13745
|
+
}
|
|
13746
|
+
for (const n of noteRows) {
|
|
13747
|
+
await db.query(
|
|
13748
|
+
`INSERT INTO note (
|
|
13749
|
+
id, archive_id, title, format, source, visibility, revision_mode,
|
|
13750
|
+
is_starred, is_pinned, is_archived, created_at, updated_at, deleted_at
|
|
13751
|
+
)
|
|
13752
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
13753
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
13754
|
+
archive_id = EXCLUDED.archive_id,
|
|
13755
|
+
title = EXCLUDED.title,
|
|
13756
|
+
format = EXCLUDED.format,
|
|
13757
|
+
source = EXCLUDED.source,
|
|
13758
|
+
visibility = EXCLUDED.visibility,
|
|
13759
|
+
revision_mode = EXCLUDED.revision_mode,
|
|
13760
|
+
is_starred = EXCLUDED.is_starred,
|
|
13761
|
+
is_pinned = EXCLUDED.is_pinned,
|
|
13762
|
+
is_archived = EXCLUDED.is_archived,
|
|
13763
|
+
updated_at = EXCLUDED.updated_at,
|
|
13764
|
+
deleted_at = EXCLUDED.deleted_at`,
|
|
13765
|
+
[
|
|
13766
|
+
n.id,
|
|
13767
|
+
n.archive_id,
|
|
13768
|
+
n.title,
|
|
13769
|
+
n.format,
|
|
13770
|
+
n.source,
|
|
13771
|
+
n.visibility,
|
|
13772
|
+
n.revision_mode,
|
|
13773
|
+
n.is_starred,
|
|
13774
|
+
n.is_pinned,
|
|
13775
|
+
n.is_archived,
|
|
13776
|
+
n.created_at,
|
|
13777
|
+
n.updated_at,
|
|
13778
|
+
n.deleted_at
|
|
13779
|
+
]
|
|
13780
|
+
);
|
|
13781
|
+
}
|
|
13782
|
+
for (const o of originals) {
|
|
13783
|
+
await db.query(
|
|
13784
|
+
`INSERT INTO note_original (id, note_id, content, content_hash, created_at)
|
|
13785
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
13786
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
13787
|
+
content = EXCLUDED.content,
|
|
13788
|
+
content_hash = EXCLUDED.content_hash`,
|
|
13789
|
+
[o.id, o.note_id, o.content, o.content_hash, o.created_at]
|
|
13790
|
+
);
|
|
13791
|
+
}
|
|
13792
|
+
for (const r of revised) {
|
|
13793
|
+
await db.query(
|
|
13794
|
+
`INSERT INTO note_revised_current (
|
|
13795
|
+
note_id, content, ai_metadata, generation_count, model, is_user_edited, updated_at
|
|
13796
|
+
)
|
|
13797
|
+
VALUES ($1, $2, $3::jsonb, $4, $5, $6, $7)
|
|
13798
|
+
ON CONFLICT (note_id) DO UPDATE SET
|
|
13799
|
+
content = EXCLUDED.content,
|
|
13800
|
+
ai_metadata = EXCLUDED.ai_metadata,
|
|
13801
|
+
generation_count = EXCLUDED.generation_count,
|
|
13802
|
+
model = EXCLUDED.model,
|
|
13803
|
+
is_user_edited = EXCLUDED.is_user_edited,
|
|
13804
|
+
updated_at = EXCLUDED.updated_at`,
|
|
13805
|
+
[
|
|
13806
|
+
r.id,
|
|
13807
|
+
r.content,
|
|
13808
|
+
r.ai_metadata == null ? null : JSON.stringify(r.ai_metadata),
|
|
13809
|
+
r.generation_count,
|
|
13810
|
+
r.model,
|
|
13811
|
+
r.is_user_edited,
|
|
13812
|
+
r.updated_at
|
|
13813
|
+
]
|
|
13814
|
+
);
|
|
13815
|
+
}
|
|
13816
|
+
for (const t of tags) {
|
|
13817
|
+
await db.query(
|
|
13818
|
+
`INSERT INTO note_tag (id, note_id, tag, created_at)
|
|
13819
|
+
VALUES ($1, $2, $3, $4)
|
|
13820
|
+
ON CONFLICT (note_id, tag) DO NOTHING`,
|
|
13821
|
+
[t.id, t.note_id, t.tag, t.created_at]
|
|
13822
|
+
);
|
|
13823
|
+
}
|
|
13824
|
+
await db.query(
|
|
13825
|
+
`DELETE FROM note_tag WHERE NOT (id = ANY($1::text[]))`,
|
|
13826
|
+
[tags.map((t) => t.id)]
|
|
13827
|
+
);
|
|
13828
|
+
for (const l of links) {
|
|
13829
|
+
await db.query(
|
|
13830
|
+
`INSERT INTO link (id, source_note_id, target_note_id, link_type, created_at, deleted_at)
|
|
13831
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
13832
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
13833
|
+
link_type = EXCLUDED.link_type,
|
|
13834
|
+
deleted_at = EXCLUDED.deleted_at`,
|
|
13835
|
+
[l.id, l.source_note_id, l.target_note_id, l.link_type, l.created_at, l.deleted_at]
|
|
13836
|
+
);
|
|
13837
|
+
}
|
|
13838
|
+
for (const m of memberships) {
|
|
13839
|
+
await db.query(
|
|
13840
|
+
`INSERT INTO collection_note (collection_id, note_id, position, added_at)
|
|
13841
|
+
VALUES ($1, $2, 0, $3)
|
|
13842
|
+
ON CONFLICT (collection_id, note_id) DO NOTHING`,
|
|
13843
|
+
[m.collection_id, m.note_id, m.created_at]
|
|
13844
|
+
);
|
|
13845
|
+
}
|
|
13846
|
+
await db.query(
|
|
13847
|
+
`DELETE FROM collection_note WHERE NOT ((collection_id || ':' || note_id) = ANY($1::text[]))`,
|
|
13848
|
+
[memberships.map((m) => `${m.collection_id}:${m.note_id}`)]
|
|
13849
|
+
);
|
|
13850
|
+
return {
|
|
13851
|
+
notes: noteRows.length,
|
|
13852
|
+
tags: tags.length,
|
|
13853
|
+
links: links.length,
|
|
13854
|
+
collections: collections.length,
|
|
13855
|
+
memberships: memberships.length
|
|
13856
|
+
};
|
|
13857
|
+
}
|
|
13858
|
+
async function projectRecords(db, store) {
|
|
13859
|
+
const notes = await projectNotes(db, store);
|
|
13860
|
+
const attachments = await projectAttachments(db, store);
|
|
13861
|
+
return { ...notes, attachments };
|
|
13862
|
+
}
|
|
13863
|
+
async function dropNoteProjection(db) {
|
|
13864
|
+
await db.query(`DELETE FROM collection_note`);
|
|
13865
|
+
await db.query(`DELETE FROM note_tag`);
|
|
13866
|
+
await db.query(`DELETE FROM link`);
|
|
13867
|
+
await db.query(`DELETE FROM note_revised_current`);
|
|
13868
|
+
await db.query(`DELETE FROM note_revision`);
|
|
13869
|
+
await db.query(`DELETE FROM note_original`);
|
|
13870
|
+
await db.query(`DELETE FROM job_queue`);
|
|
13871
|
+
await db.query(`DELETE FROM note`);
|
|
13872
|
+
await db.query(`DELETE FROM collection`);
|
|
13873
|
+
}
|
|
13874
|
+
|
|
13875
|
+
// src/records/record-backend.ts
|
|
13876
|
+
function noteToBackend(n, tags) {
|
|
13877
|
+
return {
|
|
13878
|
+
id: n.id,
|
|
13879
|
+
title: n.title,
|
|
13880
|
+
tags,
|
|
13881
|
+
createdAt: n.created_at,
|
|
13882
|
+
updatedAt: n.updated_at,
|
|
13883
|
+
source: n.source,
|
|
13884
|
+
starred: n.is_starred,
|
|
13885
|
+
archived: n.is_archived
|
|
13886
|
+
};
|
|
13887
|
+
}
|
|
13888
|
+
function linkToBackend2(link) {
|
|
13889
|
+
return {
|
|
13890
|
+
id: link.id,
|
|
13891
|
+
fromNoteId: link.source_note_id,
|
|
13892
|
+
toNoteId: link.target_note_id,
|
|
13893
|
+
kind: link.link_type,
|
|
13894
|
+
score: null,
|
|
13895
|
+
createdAt: link.created_at
|
|
13896
|
+
};
|
|
13897
|
+
}
|
|
13898
|
+
function createRecordBackend(store, options = {}) {
|
|
13899
|
+
const notes = new CanonicalNotesRepository(store);
|
|
13900
|
+
async function tagsByNote() {
|
|
13901
|
+
const map = /* @__PURE__ */ new Map();
|
|
13902
|
+
for (const row of await store.list("note_tag")) {
|
|
13903
|
+
const tags = map.get(row.note_id) ?? [];
|
|
13904
|
+
tags.push(row.tag);
|
|
13905
|
+
map.set(row.note_id, tags);
|
|
13906
|
+
}
|
|
13907
|
+
for (const tags of map.values()) tags.sort();
|
|
13908
|
+
return map;
|
|
13909
|
+
}
|
|
13910
|
+
return {
|
|
13911
|
+
id: options.id ?? "canonical-records",
|
|
13912
|
+
capabilities: {
|
|
13913
|
+
read: true,
|
|
13914
|
+
write: true,
|
|
13915
|
+
merge: true,
|
|
13916
|
+
// via importShardToRecords (record-shard.ts)
|
|
13917
|
+
multiUser: false,
|
|
13918
|
+
semantic: "none",
|
|
13919
|
+
startupCost: "instant"
|
|
13920
|
+
},
|
|
13921
|
+
async listNotes(o) {
|
|
13922
|
+
const all = (await store.list("note")).filter((n) => n.deleted_at === null);
|
|
13923
|
+
all.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
|
|
13924
|
+
const offset = o?.offset ?? 0;
|
|
13925
|
+
const limit = o?.limit ?? 50;
|
|
13926
|
+
const tags = await tagsByNote();
|
|
13927
|
+
return {
|
|
13928
|
+
items: all.slice(offset, offset + limit).map((n) => noteToBackend(n, tags.get(n.id) ?? [])),
|
|
13929
|
+
total: all.length
|
|
13930
|
+
};
|
|
13931
|
+
},
|
|
13932
|
+
async getNote(id) {
|
|
13933
|
+
const view = await notes.get(id);
|
|
13934
|
+
if (!view || view.note.deleted_at !== null) return null;
|
|
13935
|
+
return noteToBackend(view.note, view.tags);
|
|
13936
|
+
},
|
|
13937
|
+
async search(query, o) {
|
|
13938
|
+
const offset = o?.offset ?? 0;
|
|
13939
|
+
const limit = o?.limit ?? 20;
|
|
13940
|
+
let matched = await notes.searchText(query, offset + limit);
|
|
13941
|
+
if (o?.tags?.length) {
|
|
13942
|
+
const tags2 = await tagsByNote();
|
|
13943
|
+
matched = matched.filter((n) => o.tags.every((t) => (tags2.get(n.id) ?? []).includes(t)));
|
|
13944
|
+
}
|
|
13945
|
+
if (o?.source?.length) {
|
|
13946
|
+
matched = matched.filter((n) => o.source.includes(n.source));
|
|
13947
|
+
}
|
|
13948
|
+
const tags = await tagsByNote();
|
|
13949
|
+
const hits = matched.slice(offset, offset + limit).map((n) => ({ note: noteToBackend(n, tags.get(n.id) ?? []) }));
|
|
13950
|
+
return { hits, total: matched.length };
|
|
13951
|
+
},
|
|
13952
|
+
async getNoteFull(id) {
|
|
13953
|
+
const view = await notes.get(id);
|
|
13954
|
+
if (!view || view.note.deleted_at !== null) return null;
|
|
13955
|
+
return {
|
|
13956
|
+
...noteToBackend(view.note, view.tags),
|
|
13957
|
+
content: view.revised_content,
|
|
13958
|
+
links: (await notes.linksOf(id)).map(linkToBackend2)
|
|
13959
|
+
};
|
|
13960
|
+
},
|
|
13961
|
+
async linksOf(id) {
|
|
13962
|
+
return (await notes.linksOf(id)).map(linkToBackend2);
|
|
13963
|
+
},
|
|
13964
|
+
async manageNote(input) {
|
|
13965
|
+
const parsed = ManageNoteInputSchema.parse(input);
|
|
13966
|
+
switch (parsed.action) {
|
|
13967
|
+
case "update": {
|
|
13968
|
+
const note = await notes.update(parsed.note_id, {
|
|
13969
|
+
title: parsed.title,
|
|
13970
|
+
content: parsed.content,
|
|
13971
|
+
format: parsed.format,
|
|
13972
|
+
visibility: parsed.visibility
|
|
13973
|
+
});
|
|
13974
|
+
return { action: "update", note_id: parsed.note_id, note };
|
|
13975
|
+
}
|
|
13976
|
+
case "delete":
|
|
13977
|
+
await notes.softDelete(parsed.note_id);
|
|
13978
|
+
return { action: "delete", note_id: parsed.note_id };
|
|
13979
|
+
case "restore": {
|
|
13980
|
+
await notes.restore(parsed.note_id);
|
|
13981
|
+
return { action: "restore", note_id: parsed.note_id, note: await notes.get(parsed.note_id) };
|
|
13982
|
+
}
|
|
13983
|
+
case "archive": {
|
|
13984
|
+
const note = await notes.update(parsed.note_id, { is_archived: true });
|
|
13985
|
+
return { action: "archive", note_id: parsed.note_id, note };
|
|
13986
|
+
}
|
|
13987
|
+
case "unarchive": {
|
|
13988
|
+
const note = await notes.update(parsed.note_id, { is_archived: false });
|
|
13989
|
+
return { action: "unarchive", note_id: parsed.note_id, note };
|
|
13990
|
+
}
|
|
13991
|
+
case "star": {
|
|
13992
|
+
const note = await notes.update(parsed.note_id, { is_starred: true });
|
|
13993
|
+
return { action: "star", note_id: parsed.note_id, note };
|
|
13994
|
+
}
|
|
13995
|
+
case "unstar": {
|
|
13996
|
+
const note = await notes.update(parsed.note_id, { is_starred: false });
|
|
13997
|
+
return { action: "unstar", note_id: parsed.note_id, note };
|
|
13998
|
+
}
|
|
13999
|
+
}
|
|
14000
|
+
}
|
|
14001
|
+
};
|
|
14002
|
+
}
|
|
14003
|
+
|
|
14004
|
+
// src/records/record-shard.ts
|
|
14005
|
+
var encoder2 = new TextEncoder();
|
|
14006
|
+
var decoder6 = new TextDecoder();
|
|
14007
|
+
function emptyCounts() {
|
|
14008
|
+
return {
|
|
14009
|
+
notes: 0,
|
|
14010
|
+
collections: 0,
|
|
14011
|
+
templates: 0,
|
|
14012
|
+
tags: 0,
|
|
14013
|
+
links: 0,
|
|
14014
|
+
embedding_sets: 0,
|
|
14015
|
+
embedding_configs: 0,
|
|
14016
|
+
embedding_set_members: 0,
|
|
14017
|
+
embeddings: 0,
|
|
14018
|
+
skos_schemes: 0,
|
|
14019
|
+
skos_concepts: 0,
|
|
14020
|
+
skos_relations: 0,
|
|
14021
|
+
note_skos_tags: 0,
|
|
14022
|
+
provenance_edges: 0,
|
|
14023
|
+
graph_sources: 0,
|
|
14024
|
+
graph_edges: 0,
|
|
14025
|
+
community_sets: 0,
|
|
14026
|
+
communities: 0,
|
|
14027
|
+
community_assignments: 0
|
|
14028
|
+
};
|
|
14029
|
+
}
|
|
14030
|
+
async function exportShardFromRecords(store, options) {
|
|
14031
|
+
const files = /* @__PURE__ */ new Map();
|
|
14032
|
+
const components = [];
|
|
14033
|
+
const counts = {};
|
|
14034
|
+
const [allNotes, originals, revisedRows, tagRows, collections, memberships, attachments, blobs] = await Promise.all([
|
|
14035
|
+
store.list("note"),
|
|
14036
|
+
store.list("note_original"),
|
|
14037
|
+
store.list("note_revised_current"),
|
|
14038
|
+
store.list("note_tag"),
|
|
14039
|
+
store.list("collection"),
|
|
14040
|
+
store.list("collection_note"),
|
|
14041
|
+
store.list("attachment"),
|
|
14042
|
+
store.list("attachment_blob")
|
|
14043
|
+
]);
|
|
14044
|
+
const originalByNote = new Map(originals.map((o) => [o.note_id, o]));
|
|
14045
|
+
const revisedByNote = new Map(revisedRows.map((r) => [r.id, r]));
|
|
14046
|
+
const blobById = new Map(blobs.map((b) => [b.id, b]));
|
|
14047
|
+
const tagsByNote = /* @__PURE__ */ new Map();
|
|
14048
|
+
for (const row of [...tagRows].sort((a, b) => a.tag.localeCompare(b.tag))) {
|
|
14049
|
+
const tags = tagsByNote.get(row.note_id) ?? [];
|
|
14050
|
+
tags.push(row.tag);
|
|
14051
|
+
tagsByNote.set(row.note_id, tags);
|
|
14052
|
+
}
|
|
14053
|
+
const collectionByNote = /* @__PURE__ */ new Map();
|
|
14054
|
+
for (const m of [...memberships].sort((a, b) => a.created_at.localeCompare(b.created_at))) {
|
|
14055
|
+
if (!collectionByNote.has(m.note_id)) collectionByNote.set(m.note_id, m.collection_id);
|
|
14056
|
+
}
|
|
14057
|
+
const membershipNoteIds = /* @__PURE__ */ new Map();
|
|
14058
|
+
for (const m of memberships) {
|
|
14059
|
+
const set = membershipNoteIds.get(m.collection_id) ?? /* @__PURE__ */ new Set();
|
|
14060
|
+
set.add(m.note_id);
|
|
14061
|
+
membershipNoteIds.set(m.collection_id, set);
|
|
14062
|
+
}
|
|
14063
|
+
let notes = allNotes.filter((n) => n.deleted_at === null);
|
|
14064
|
+
if (options?.collectionId) {
|
|
14065
|
+
const inCollection = membershipNoteIds.get(options.collectionId) ?? /* @__PURE__ */ new Set();
|
|
14066
|
+
notes = notes.filter((n) => inCollection.has(n.id));
|
|
14067
|
+
} else if (options?.tag) {
|
|
14068
|
+
notes = notes.filter((n) => tagsByNote.get(n.id)?.includes(options.tag));
|
|
14069
|
+
}
|
|
14070
|
+
notes.sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
14071
|
+
const liveAttachments = attachments.filter((a) => a.deleted_at === null).sort((a, b) => a.position - b.position || a.created_at.localeCompare(b.created_at));
|
|
14072
|
+
const attachmentsByNote = /* @__PURE__ */ new Map();
|
|
14073
|
+
const exportedBlobChecksums = [];
|
|
14074
|
+
for (const att of liveAttachments) {
|
|
14075
|
+
const blob = blobById.get(att.blob_id);
|
|
14076
|
+
if (!blob) continue;
|
|
14077
|
+
const projection = {
|
|
14078
|
+
extracted_text: att.extracted_text,
|
|
14079
|
+
attachment: {
|
|
14080
|
+
// `path` is the display filename per the binary-attachment projection
|
|
14081
|
+
// contract — never a physical storage key.
|
|
14082
|
+
id: att.id,
|
|
14083
|
+
path: att.filename,
|
|
14084
|
+
mime: att.mime_type,
|
|
14085
|
+
checksum: blob.content_hash,
|
|
14086
|
+
bytes: blob.size_bytes
|
|
14087
|
+
}
|
|
14088
|
+
};
|
|
14089
|
+
const list = attachmentsByNote.get(att.note_id) ?? [];
|
|
14090
|
+
list.push(projection);
|
|
14091
|
+
attachmentsByNote.set(att.note_id, list);
|
|
14092
|
+
exportedBlobChecksums.push(blob.content_hash);
|
|
14093
|
+
}
|
|
14094
|
+
const browserNotes = notes.map((n) => {
|
|
14095
|
+
const revised = revisedByNote.get(n.id);
|
|
14096
|
+
return {
|
|
14097
|
+
id: n.id,
|
|
14098
|
+
title: n.title,
|
|
14099
|
+
format: n.format,
|
|
14100
|
+
source: n.source,
|
|
14101
|
+
is_starred: n.is_starred,
|
|
14102
|
+
is_archived: n.is_archived,
|
|
14103
|
+
created_at: n.created_at,
|
|
14104
|
+
updated_at: n.updated_at,
|
|
14105
|
+
deleted_at: n.deleted_at,
|
|
14106
|
+
original_content: originalByNote.get(n.id)?.content ?? "",
|
|
14107
|
+
revised_content: revised?.content ?? null,
|
|
14108
|
+
ai_metadata: revised?.ai_metadata ?? null,
|
|
14109
|
+
collection_id: collectionByNote.get(n.id) ?? null,
|
|
14110
|
+
attachments: attachmentsByNote.get(n.id),
|
|
14111
|
+
tags: tagsByNote.get(n.id) ?? []
|
|
14112
|
+
};
|
|
14113
|
+
});
|
|
14114
|
+
const exportedNoteIds = new Set(browserNotes.map((n) => n.id));
|
|
14115
|
+
const shardNotes = browserNotes.map((n) => noteToShard(n));
|
|
14116
|
+
let layout;
|
|
14117
|
+
const clusterSize = options?.clusterNotesSize;
|
|
14118
|
+
if (clusterSize && Number.isInteger(clusterSize) && clusterSize > 0 && shardNotes.length > 0) {
|
|
14119
|
+
const clusters = [];
|
|
14120
|
+
for (let offset = 0; offset < shardNotes.length; offset += clusterSize) {
|
|
14121
|
+
const slice = shardNotes.slice(offset, offset + clusterSize);
|
|
14122
|
+
const href = `notes/${String(offset).padStart(6, "0")}.jsonl`;
|
|
14123
|
+
clusters.push({ href, offset });
|
|
14124
|
+
files.set(href, encoder2.encode(slice.map((n) => JSON.stringify(n)).join("\n")));
|
|
14125
|
+
}
|
|
14126
|
+
layout = { clusters: { notes: clusters } };
|
|
14127
|
+
} else {
|
|
14128
|
+
files.set("notes.jsonl", encoder2.encode(shardNotes.map((n) => JSON.stringify(n)).join("\n")));
|
|
14129
|
+
}
|
|
14130
|
+
components.push("notes");
|
|
14131
|
+
counts.notes = shardNotes.length;
|
|
14132
|
+
const liveCollections = collections.filter((c) => c.deleted_at === null).sort((a, b) => a.name.localeCompare(b.name));
|
|
14133
|
+
const shardCollections = liveCollections.map(
|
|
14134
|
+
(c) => collectionToShard(
|
|
14135
|
+
// Canonical collections are flat (no parent hierarchy yet).
|
|
14136
|
+
{ id: c.id, name: c.name, description: c.description, parent_id: null, created_at: c.created_at },
|
|
14137
|
+
membershipNoteIds.get(c.id)?.size ?? 0
|
|
14138
|
+
)
|
|
14139
|
+
);
|
|
14140
|
+
files.set("collections.json", encoder2.encode(JSON.stringify(shardCollections)));
|
|
14141
|
+
components.push("collections");
|
|
14142
|
+
counts.collections = shardCollections.length;
|
|
14143
|
+
const distinctTags = [...new Set(
|
|
14144
|
+
tagRows.filter((t) => exportedNoteIds.has(t.note_id) || !options?.collectionId && !options?.tag).map((t) => t.tag)
|
|
14145
|
+
)].sort();
|
|
14146
|
+
const shardTags = tagsToShard(distinctTags.map((name) => ({ name, created_at: /* @__PURE__ */ new Date() })));
|
|
14147
|
+
files.set("tags.json", encoder2.encode(JSON.stringify(shardTags)));
|
|
14148
|
+
components.push("tags");
|
|
14149
|
+
counts.tags = shardTags.length;
|
|
14150
|
+
const links = await store.list("link");
|
|
14151
|
+
const isFiltered = !!(options?.collectionId || options?.tag);
|
|
14152
|
+
const shardLinks = links.filter((l) => l.deleted_at === null).filter((l) => !isFiltered || exportedNoteIds.has(l.source_note_id) && exportedNoteIds.has(l.target_note_id)).sort((a, b) => a.created_at.localeCompare(b.created_at)).map((l) => linkToShard({
|
|
14153
|
+
id: l.id,
|
|
14154
|
+
source_note_id: l.source_note_id,
|
|
14155
|
+
target_note_id: l.target_note_id,
|
|
14156
|
+
link_type: l.link_type,
|
|
14157
|
+
// Canonical links carry no confidence score (PGlite-tier column).
|
|
14158
|
+
confidence: null,
|
|
14159
|
+
created_at: l.created_at
|
|
14160
|
+
}));
|
|
14161
|
+
files.set("links.jsonl", encoder2.encode(shardLinks.map((l) => JSON.stringify(l)).join("\n")));
|
|
14162
|
+
components.push("links");
|
|
14163
|
+
counts.links = shardLinks.length;
|
|
14164
|
+
const checksums = {};
|
|
14165
|
+
for (const [filename, data] of files) {
|
|
14166
|
+
checksums[filename] = await sha256Hex(data);
|
|
14167
|
+
}
|
|
14168
|
+
const manifest = {
|
|
14169
|
+
version: CURRENT_SHARD_VERSION,
|
|
14170
|
+
matric_version: VERSION,
|
|
14171
|
+
format: SHARD_FORMAT,
|
|
14172
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14173
|
+
components,
|
|
14174
|
+
counts,
|
|
14175
|
+
checksums,
|
|
14176
|
+
min_reader_version: "1.0.0",
|
|
14177
|
+
migrated_from: null,
|
|
14178
|
+
migration_history: [],
|
|
14179
|
+
...layout ? { layout } : {}
|
|
14180
|
+
};
|
|
14181
|
+
files.set("manifest.json", encoder2.encode(JSON.stringify(manifest, null, 2)));
|
|
14182
|
+
if (options?.includeBlobs && options.blobStore) {
|
|
14183
|
+
const packed = /* @__PURE__ */ new Set();
|
|
14184
|
+
for (const checksum of exportedBlobChecksums) {
|
|
14185
|
+
if (packed.has(checksum)) continue;
|
|
14186
|
+
packed.add(checksum);
|
|
14187
|
+
const bytes = await options.blobStore.read(checksum);
|
|
14188
|
+
if (bytes) files.set(sidecarEntryName(checksum), bytes);
|
|
14189
|
+
}
|
|
14190
|
+
}
|
|
14191
|
+
return packTarGz(files);
|
|
14192
|
+
}
|
|
14193
|
+
var UNSUPPORTED_COMPONENTS = [
|
|
14194
|
+
"templates",
|
|
14195
|
+
"embedding_sets",
|
|
14196
|
+
"embedding_configs",
|
|
14197
|
+
"embedding_set_members",
|
|
14198
|
+
"embeddings",
|
|
14199
|
+
"skos_schemes",
|
|
14200
|
+
"skos_concepts",
|
|
14201
|
+
"skos_relations",
|
|
14202
|
+
"note_skos_tags",
|
|
14203
|
+
"provenance_edges",
|
|
14204
|
+
"graph_sources",
|
|
14205
|
+
"graph_edges",
|
|
14206
|
+
"communities",
|
|
14207
|
+
"community_assignments"
|
|
14208
|
+
];
|
|
14209
|
+
function failure(counts, skipped, warnings, error, start) {
|
|
14210
|
+
return {
|
|
14211
|
+
success: false,
|
|
14212
|
+
counts,
|
|
14213
|
+
skipped,
|
|
14214
|
+
warnings,
|
|
14215
|
+
errors: [error],
|
|
14216
|
+
duration_ms: performance.now() - start
|
|
14217
|
+
};
|
|
14218
|
+
}
|
|
14219
|
+
async function importShardToRecords(store, data, options) {
|
|
14220
|
+
const start = performance.now();
|
|
14221
|
+
const strategy = options?.conflictStrategy ?? "skip";
|
|
14222
|
+
const counts = emptyCounts();
|
|
14223
|
+
const skipped = {};
|
|
14224
|
+
const warnings = [];
|
|
14225
|
+
const inputData = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
14226
|
+
let files;
|
|
14227
|
+
try {
|
|
14228
|
+
files = unpackTarGz(inputData);
|
|
14229
|
+
} catch (err) {
|
|
14230
|
+
return failure(
|
|
14231
|
+
counts,
|
|
14232
|
+
skipped,
|
|
14233
|
+
warnings,
|
|
14234
|
+
`Failed to decompress archive: ${err instanceof Error ? err.message : String(err)}`,
|
|
14235
|
+
start
|
|
14236
|
+
);
|
|
14237
|
+
}
|
|
14238
|
+
const manifestData = files.get("manifest.json");
|
|
14239
|
+
if (!manifestData) {
|
|
14240
|
+
return failure(counts, skipped, warnings, "Missing manifest.json in shard archive", start);
|
|
14241
|
+
}
|
|
14242
|
+
let manifest;
|
|
14243
|
+
try {
|
|
14244
|
+
manifest = JSON.parse(decoder6.decode(manifestData));
|
|
14245
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
14246
|
+
throw new Error("manifest must be a JSON object");
|
|
14247
|
+
}
|
|
14248
|
+
} catch {
|
|
14249
|
+
return failure(counts, skipped, warnings, "Invalid manifest.json: failed to parse JSON", start);
|
|
14250
|
+
}
|
|
14251
|
+
if (manifest.min_reader_version && compareShardVersions(manifest.min_reader_version, CURRENT_SHARD_VERSION) > 0) {
|
|
14252
|
+
return failure(
|
|
14253
|
+
counts,
|
|
14254
|
+
skipped,
|
|
14255
|
+
warnings,
|
|
14256
|
+
`Shard requires reader version ${manifest.min_reader_version}, but this version supports up to ${CURRENT_SHARD_VERSION}`,
|
|
14257
|
+
start
|
|
14258
|
+
);
|
|
14259
|
+
}
|
|
14260
|
+
const sigError = await enforceSignaturePolicy(files, options, warnings);
|
|
14261
|
+
if (sigError) return failure(counts, skipped, warnings, sigError, start);
|
|
14262
|
+
if (!manifest.checksums || typeof manifest.checksums !== "object") {
|
|
14263
|
+
return failure(counts, skipped, warnings, "Invalid manifest.json: checksums must be an object", start);
|
|
14264
|
+
}
|
|
14265
|
+
const checksumResult = await validateChecksums(manifest.checksums, files);
|
|
14266
|
+
if (!checksumResult.valid) {
|
|
14267
|
+
return failure(
|
|
14268
|
+
counts,
|
|
14269
|
+
skipped,
|
|
14270
|
+
warnings,
|
|
14271
|
+
`Checksum validation failed for: ${checksumResult.failures.join(", ")}`,
|
|
14272
|
+
start
|
|
14273
|
+
);
|
|
14274
|
+
}
|
|
14275
|
+
const sidecarBlobs = options?.blobStore ? collectSidecarBlobs(files) : null;
|
|
14276
|
+
const noteClusters = manifest.layout?.clusters?.notes;
|
|
14277
|
+
let parsedNotes;
|
|
14278
|
+
let parsedCollections;
|
|
14279
|
+
let parsedLinks;
|
|
14280
|
+
try {
|
|
14281
|
+
parsedNotes = noteClusters && noteClusters.length > 0 ? [...noteClusters].sort((a, b) => a.offset - b.offset).flatMap((ref) => parseJsonlBytes(files.get(ref.href))) : parseJsonlBytes(files.get("notes.jsonl"));
|
|
14282
|
+
parsedCollections = parseJsonArrayBytes(files.get("collections.json"));
|
|
14283
|
+
parsedLinks = parseJsonlBytes(files.get("links.jsonl"));
|
|
14284
|
+
} catch (err) {
|
|
14285
|
+
return failure(
|
|
14286
|
+
counts,
|
|
14287
|
+
skipped,
|
|
14288
|
+
warnings,
|
|
14289
|
+
`Failed to parse shard component: ${err instanceof Error ? err.message : String(err)}`,
|
|
14290
|
+
start
|
|
14291
|
+
);
|
|
14292
|
+
}
|
|
14293
|
+
for (const component of manifest.components ?? []) {
|
|
14294
|
+
if (UNSUPPORTED_COMPONENTS.includes(component)) {
|
|
14295
|
+
const count = manifest.counts?.[component];
|
|
14296
|
+
const key = component === "communities" ? "communities" : component;
|
|
14297
|
+
skipped[key] = (skipped[key] ?? 0) + (typeof count === "number" ? count : 0);
|
|
14298
|
+
warnings.push(
|
|
14299
|
+
`Shard component '${component}' is not supported by the canonical record tier and was skipped. Import into a PGlite-backed store to preserve it.`
|
|
14300
|
+
);
|
|
14301
|
+
}
|
|
14302
|
+
}
|
|
14303
|
+
if (strategy === "error") {
|
|
14304
|
+
for (const col of parsedCollections) {
|
|
14305
|
+
if (await store.get("collection", col.id)) {
|
|
14306
|
+
return failure(counts, skipped, warnings, `Collection already exists: ${col.id}`, start);
|
|
14307
|
+
}
|
|
14308
|
+
}
|
|
14309
|
+
for (const shardNote of parsedNotes) {
|
|
14310
|
+
if (await store.get("note", shardNote.id)) {
|
|
14311
|
+
return failure(counts, skipped, warnings, `Note already exists: ${shardNote.id}`, start);
|
|
14312
|
+
}
|
|
14313
|
+
}
|
|
14314
|
+
}
|
|
14315
|
+
const nowIso3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
14316
|
+
for (const shardCol of parsedCollections) {
|
|
14317
|
+
const col = collectionFromShard(shardCol);
|
|
14318
|
+
const existing = await store.get("collection", col.id);
|
|
14319
|
+
if (existing && strategy === "skip") {
|
|
14320
|
+
skipped.collections = (skipped.collections ?? 0) + 1;
|
|
14321
|
+
continue;
|
|
14322
|
+
}
|
|
14323
|
+
await store.put("collection", {
|
|
14324
|
+
id: col.id,
|
|
14325
|
+
name: col.name,
|
|
14326
|
+
description: col.description,
|
|
14327
|
+
created_at: col.created_at,
|
|
14328
|
+
updated_at: existing?.updated_at ?? nowIso3,
|
|
14329
|
+
deleted_at: existing?.deleted_at ?? null
|
|
14330
|
+
});
|
|
14331
|
+
counts.collections++;
|
|
14332
|
+
}
|
|
14333
|
+
const existingBlobs = await store.list("attachment_blob");
|
|
14334
|
+
const blobIdByChecksum = new Map(existingBlobs.map((b) => [b.content_hash, b.id]));
|
|
14335
|
+
const blobsToHydrate = /* @__PURE__ */ new Map();
|
|
14336
|
+
let referenceOnlyCount = 0;
|
|
14337
|
+
let notesWithAttachmentRefs = 0;
|
|
14338
|
+
for (const shardNote of parsedNotes) {
|
|
14339
|
+
const note = noteFromShard(shardNote);
|
|
14340
|
+
const existing = await store.get("note", note.id);
|
|
14341
|
+
if (existing && strategy === "skip") {
|
|
14342
|
+
skipped.notes = (skipped.notes ?? 0) + 1;
|
|
14343
|
+
continue;
|
|
14344
|
+
}
|
|
14345
|
+
await store.put("note", {
|
|
14346
|
+
id: note.id,
|
|
14347
|
+
archive_id: existing?.archive_id ?? null,
|
|
14348
|
+
title: note.title,
|
|
14349
|
+
format: note.format,
|
|
14350
|
+
source: note.source,
|
|
14351
|
+
visibility: existing?.visibility ?? "private",
|
|
14352
|
+
revision_mode: existing?.revision_mode ?? "standard",
|
|
14353
|
+
is_starred: note.is_starred,
|
|
14354
|
+
is_pinned: existing?.is_pinned ?? false,
|
|
14355
|
+
is_archived: note.is_archived,
|
|
14356
|
+
created_at: typeof note.created_at === "string" ? note.created_at : note.created_at.toISOString(),
|
|
14357
|
+
updated_at: typeof note.updated_at === "string" ? note.updated_at : note.updated_at.toISOString(),
|
|
14358
|
+
deleted_at: note.deleted_at ? typeof note.deleted_at === "string" ? note.deleted_at : note.deleted_at.toISOString() : null
|
|
14359
|
+
});
|
|
14360
|
+
const contentHash = computeHash(encoder2.encode(note.original_content));
|
|
14361
|
+
const existingOriginal = existing ? (await store.list("note_original")).find((o) => o.note_id === note.id) : void 0;
|
|
14362
|
+
await store.put("note_original", {
|
|
14363
|
+
id: existingOriginal?.id ?? generateId(),
|
|
14364
|
+
note_id: note.id,
|
|
14365
|
+
content: note.original_content,
|
|
14366
|
+
content_hash: contentHash,
|
|
14367
|
+
created_at: existingOriginal?.created_at ?? nowIso3
|
|
14368
|
+
});
|
|
14369
|
+
const existingRevised = existing ? await store.get("note_revised_current", note.id) : null;
|
|
14370
|
+
await store.put("note_revised_current", {
|
|
14371
|
+
id: note.id,
|
|
14372
|
+
content: note.revised_content ?? note.original_content,
|
|
14373
|
+
ai_metadata: note.ai_metadata ?? null,
|
|
14374
|
+
generation_count: existingRevised?.generation_count ?? 0,
|
|
14375
|
+
model: existingRevised?.model ?? null,
|
|
14376
|
+
is_user_edited: existingRevised?.is_user_edited ?? false,
|
|
14377
|
+
updated_at: typeof note.updated_at === "string" ? note.updated_at : note.updated_at.toISOString()
|
|
14378
|
+
});
|
|
14379
|
+
const existingTags = new Set(
|
|
14380
|
+
(await store.list("note_tag")).filter((t) => t.note_id === note.id).map((t) => t.tag)
|
|
14381
|
+
);
|
|
14382
|
+
for (const tag of note.tags) {
|
|
14383
|
+
if (existingTags.has(tag)) continue;
|
|
14384
|
+
await store.put("note_tag", { id: generateId(), note_id: note.id, tag, created_at: nowIso3 });
|
|
14385
|
+
}
|
|
14386
|
+
if (note.collection_id && await store.get("collection", note.collection_id)) {
|
|
14387
|
+
const member = (await store.list("collection_note")).find(
|
|
14388
|
+
(cn) => cn.collection_id === note.collection_id && cn.note_id === note.id
|
|
14389
|
+
);
|
|
14390
|
+
if (!member) {
|
|
14391
|
+
await store.put("collection_note", {
|
|
14392
|
+
id: generateId(),
|
|
14393
|
+
collection_id: note.collection_id,
|
|
14394
|
+
note_id: note.id,
|
|
14395
|
+
created_at: nowIso3
|
|
14396
|
+
});
|
|
14397
|
+
}
|
|
14398
|
+
}
|
|
14399
|
+
if (note.attachments?.length) {
|
|
14400
|
+
notesWithAttachmentRefs++;
|
|
14401
|
+
for (let position = 0; position < note.attachments.length; position += 1) {
|
|
14402
|
+
const projection = note.attachments[position];
|
|
14403
|
+
const ref = projection.attachment;
|
|
14404
|
+
let blobId = blobIdByChecksum.get(ref.checksum);
|
|
14405
|
+
if (!blobId) {
|
|
14406
|
+
const blob = {
|
|
14407
|
+
id: generateId(),
|
|
14408
|
+
content_hash: ref.checksum,
|
|
14409
|
+
size_bytes: ref.bytes,
|
|
14410
|
+
created_at: nowIso3
|
|
14411
|
+
};
|
|
14412
|
+
await store.put("attachment_blob", blob);
|
|
14413
|
+
blobIdByChecksum.set(ref.checksum, blob.id);
|
|
14414
|
+
blobId = blob.id;
|
|
14415
|
+
}
|
|
14416
|
+
const filename = ref.path.split("/").filter(Boolean).pop() ?? ref.path;
|
|
14417
|
+
const attachment = {
|
|
14418
|
+
id: ref.id,
|
|
14419
|
+
note_id: note.id,
|
|
14420
|
+
blob_id: blobId,
|
|
14421
|
+
document_type_id: null,
|
|
14422
|
+
mime_type: ref.mime,
|
|
14423
|
+
extracted_text: projection.extracted_text,
|
|
14424
|
+
filename,
|
|
14425
|
+
display_name: null,
|
|
14426
|
+
position,
|
|
14427
|
+
created_at: nowIso3,
|
|
14428
|
+
deleted_at: null
|
|
14429
|
+
};
|
|
14430
|
+
const existingAttachment = await store.get("attachment", ref.id);
|
|
14431
|
+
if (!existingAttachment || strategy === "replace") {
|
|
14432
|
+
await store.put("attachment", {
|
|
14433
|
+
...attachment,
|
|
14434
|
+
created_at: existingAttachment?.created_at ?? nowIso3
|
|
14435
|
+
});
|
|
14436
|
+
}
|
|
14437
|
+
let hydrated = false;
|
|
14438
|
+
if (sidecarBlobs) {
|
|
14439
|
+
if (blobsToHydrate.has(ref.checksum)) {
|
|
14440
|
+
hydrated = true;
|
|
14441
|
+
} else {
|
|
14442
|
+
const bytes = sidecarBlobs.get(blobChecksumToHex(ref.checksum));
|
|
14443
|
+
if (bytes) {
|
|
14444
|
+
if (computeBlobHash(bytes) === ref.checksum) {
|
|
14445
|
+
blobsToHydrate.set(ref.checksum, bytes);
|
|
14446
|
+
hydrated = true;
|
|
14447
|
+
} else {
|
|
14448
|
+
warnings.push(
|
|
14449
|
+
`Sidecar blob for attachment ${ref.id} failed BLAKE3 integrity check (expected ${ref.checksum}); imported as reference-only.`
|
|
14450
|
+
);
|
|
14451
|
+
}
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
14454
|
+
}
|
|
14455
|
+
if (!hydrated) referenceOnlyCount++;
|
|
14456
|
+
}
|
|
14457
|
+
}
|
|
14458
|
+
counts.notes++;
|
|
14459
|
+
}
|
|
14460
|
+
if (referenceOnlyCount > 0) {
|
|
14461
|
+
warnings.push(
|
|
14462
|
+
`${referenceOnlyCount} attachment reference(s) across ${notesWithAttachmentRefs} note(s) were imported as metadata only: no matching byte-sidecar entry (or no import blobStore), so getBlob() returns null until bytes are hydrated. Export a self-contained shard (\`includeBlobs\` + a \`blobStore\`) and import with a \`blobStore\` to hydrate bytes.`
|
|
14463
|
+
);
|
|
14464
|
+
}
|
|
14465
|
+
for (const shardLink of parsedLinks) {
|
|
14466
|
+
const link = linkFromShard(shardLink);
|
|
14467
|
+
if (!link.target_note_id) {
|
|
14468
|
+
skipped.links = (skipped.links ?? 0) + 1;
|
|
14469
|
+
warnings.push(
|
|
14470
|
+
link.to_url ? `URL link ${link.id} skipped: the canonical record tier does not persist URL-target links.` : `Shard link skipped: ${link.id} has neither to_note_id nor to_url.`
|
|
14471
|
+
);
|
|
14472
|
+
continue;
|
|
14473
|
+
}
|
|
14474
|
+
const existing = await store.get("link", link.id);
|
|
14475
|
+
if (existing && strategy === "skip") {
|
|
14476
|
+
skipped.links = (skipped.links ?? 0) + 1;
|
|
14477
|
+
continue;
|
|
14478
|
+
}
|
|
14479
|
+
await store.put("link", {
|
|
14480
|
+
id: link.id,
|
|
14481
|
+
source_note_id: link.source_note_id,
|
|
14482
|
+
target_note_id: link.target_note_id,
|
|
14483
|
+
link_type: link.link_type,
|
|
14484
|
+
created_at: link.created_at,
|
|
14485
|
+
deleted_at: existing?.deleted_at ?? null
|
|
14486
|
+
});
|
|
14487
|
+
counts.links++;
|
|
14488
|
+
}
|
|
14489
|
+
const errors = [];
|
|
14490
|
+
if (options?.blobStore && blobsToHydrate.size > 0) {
|
|
14491
|
+
try {
|
|
14492
|
+
for (const [, bytes] of blobsToHydrate) {
|
|
14493
|
+
await options.blobStore.put(bytes);
|
|
14494
|
+
}
|
|
14495
|
+
} catch (err) {
|
|
14496
|
+
warnings.push(
|
|
14497
|
+
`Imported records successfully but failed to hydrate ${blobsToHydrate.size} attachment blob(s) into the BlobStore: ${err instanceof Error ? err.message : String(err)}.`
|
|
14498
|
+
);
|
|
14499
|
+
}
|
|
14500
|
+
}
|
|
14501
|
+
return {
|
|
14502
|
+
success: true,
|
|
14503
|
+
counts,
|
|
14504
|
+
skipped,
|
|
14505
|
+
warnings,
|
|
14506
|
+
errors,
|
|
14507
|
+
duration_ms: performance.now() - start
|
|
14508
|
+
};
|
|
14509
|
+
}
|
|
14510
|
+
|
|
12380
14511
|
// src/index.ts
|
|
12381
|
-
var VERSION = "2026.7.
|
|
14512
|
+
var VERSION = "2026.7.8";
|
|
12382
14513
|
|
|
12383
|
-
export { AIWG_SCAN_REQUIRED_FIELDS, ArchiveManager, AttachmentsRepository, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DB_SNAPSHOT_SCHEMA_VERSION, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FORTEMI_COMPATIBILITY_PATH, FORTEMI_COMPATIBILITY_STATES, FORTEMI_REQUIRED_COMPATIBILITY_CAPABILITIES, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProvenanceRepository, ProviderRegistry, SHARD_FORMAT, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexToCommunityGraph, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, assertShardComponentRecord, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, computeBlobHash, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLegacyProvider, createPGliteBackend, createPGliteInstance, createRemoteBackend, createRoutes, createShardBackend, createWorkerEmbedFunction, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, dumpDbSnapshot, embeddingConfigToShard, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, fetchAndValidateFortemiCompatibility, fetchPluginScript, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, formatFortemiCompatibilitySummary, fortemiCompatibilityUrl, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getAiwgFortemiIndexExportSchema, getEmbedFunction, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getKnowledgeShardSchema, getLlmFunction, getNote, getPrefetchedSha256, handleEmbedRequests, hasFortemiSecureSecrets, importShard, isPluginScriptAllowed, isShardPrefetched, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, prefetchShard, provenanceEdgeToShard, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, restoreDbSnapshot, searchTool, selectBackend, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsToShard, templateToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, urlLinkToShard, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgFortemiIndexExportSchema, validateAiwgFortemiProjectedRecordSchema, validateAiwgStaticEmbeddingSet, validateChecksums, validateFortemiCompatibilityResponse, validateShardArchive, validateShardComponentRecord, validateShardManifest, verifyDbSnapshotMeta, verifySri };
|
|
14514
|
+
export { AIWG_SCAN_REQUIRED_FIELDS, AllowlistTrustStore, ArchiveManager, AttachmentsRepository, CURRENT_MIGRATION_HEAD, CURRENT_SHARD_VERSION, CanonicalAttachmentsRepository, CanonicalNotesRepository, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, DB_SNAPSHOT_SCHEMA_VERSION, DbSnapshotVersionError, EMBED_REQUEST_KIND, EMBED_RESPONSE_KIND, EmbeddingSetsRepository, FORTEMI_COMPATIBILITY_PATH, FORTEMI_COMPATIBILITY_STATES, FORTEMI_REQUIRED_COMPATIBILITY_CAPABILITIES, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, GraphRepository, IdbRecordStore, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MemoryRecordStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, PGliteWorkerStorageBackend, PGliteWorkerStorageBackendFactory, ProvenanceRepository, ProviderRegistry, RECORD_COLLECTIONS, RECORD_SCHEMA_VERSION, RECORD_STORE_CAPABILITIES, SHARD_FORMAT, SIGNATURE_ENTRY, SIGNING_ENVELOPE_VERSION, SUPPORTED_PGLITE_VERSION, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, aiwgFortemiIndexFromKnowledgeShard, aiwgFortemiIndexToCommunityGraph, aiwgFortemiIndexToKnowledgeShard, allMigrations, appendPluginScript, assertAiwgFortemiChunkManifest, assertAiwgFortemiChunkPart, assertAiwgFortemiIndexExport, assertAiwgStaticEmbeddingSet, assertShardComponentRecord, buildAiwgChunkedIndex, buildAiwgStaticEmbeddingSet, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, clearPrefetchedShard, collectionFromShard, collectionToShard, computeBlobHash, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createAiwgFetchChunkLoader, createAiwgFetchDetailLoader, createAiwgIndexController, createAiwgReviewDecisionExport, createBlobStore, createCosineSemanticProvider, createCspReportHandler, createFortemi, createLazyBlobStore, createLegacyProvider, createPGliteBackend, createPGliteInstance, createRecordBackend, createRecordStore, createRemoteBackend, createRoutes, createShardBackend, createWorkerEmbedFunction, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, dropAttachmentProjection, dropNoteProjection, dumpDbSnapshot, embeddingConfigToShard, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, exportShardFromRecords, fetchAndValidateFortemiCompatibility, fetchPluginScript, filterAiwgRecordsByPrivacy, findAiwgStaticDuplicatePairs, formatFortemiCompatibilitySummary, fortemiCompatibilityUrl, fortemiManifest, fromPrefetched, generateId, getAiwgFortemiFacets, getAiwgFortemiIndexExportSchema, getEmbedFunction, getFortemiBridge, getFortemiSecretStore, getJobQueueStatus, getKnowledgeShardSchema, getLlmFunction, getNote, getPrefetchedSha256, handleEmbedRequests, hasFortemiSecureSecrets, importShard, importShardToRecords, isPluginScriptAllowed, isShardPrefetched, isShardSigningSupported, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, migrateLegacyBlobStore, noteFromShard, noteSkosTagToShard, noteToShard, openShard, packTarGz, parseCspReport, prefetchShard, projectAttachments, projectNotes, projectRecords, provenanceEdgeToShard, queryAiwgFortemiIndex, queryAiwgHybridIndex, queryAiwgSemanticIndex, registerLlmCapability, registerSemanticCapability, registerSemanticCapabilityWorker, registerServiceWorker, restoreDbSnapshot, searchTool, selectBackend, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, sidecarBlobDigests, signShard, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsToShard, templateToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, urlLinkToShard, validateAiwgFortemiChunkManifest, validateAiwgFortemiChunkPart, validateAiwgFortemiIndexExport, validateAiwgFortemiIndexExportSchema, validateAiwgFortemiProjectedRecordSchema, validateAiwgStaticEmbeddingSet, validateChecksums, validateFortemiCompatibilityResponse, validateShardArchive, validateShardComponentRecord, validateShardManifest, verifyDbSnapshotMeta, verifyShardSignature, verifySri };
|
|
12384
14515
|
//# sourceMappingURL=index.js.map
|
|
12385
14516
|
//# sourceMappingURL=index.js.map
|