@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/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-store.ts
3613
- function hashPath(hash) {
3614
- return {
3615
- dir1: hash.slice(0, 2),
3616
- dir2: hash.slice(2, 4),
3617
- filename: hash
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
- var OpfsBlobStore = class {
3621
- constructor(archiveName) {
3622
- this.archiveName = archiveName;
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
- async getRoot() {
3625
- const root = await navigator.storage.getDirectory();
3626
- return root.getDirectoryHandle(`fortemi-${this.archiveName}-blobs`, { create: true });
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
- async getFileHandle(hash, create) {
3629
- const { dir1, dir2, filename } = hashPath(hash);
3630
- try {
3631
- const root = await this.getRoot();
3632
- const d1 = await root.getDirectoryHandle(dir1, { create });
3633
- const d2 = await d1.getDirectoryHandle(dir2, { create });
3634
- return d2.getFileHandle(filename, { create });
3635
- } catch (err) {
3636
- if (err instanceof DOMException && err.name === "NotFoundError") {
3637
- return null;
3638
- }
3639
- throw err;
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
- async write(hash, data) {
3643
- const fh = await this.getFileHandle(hash, true);
3644
- if (!fh) throw new Error(`OpfsBlobStore: could not create file for hash ${hash}`);
3645
- const writable = await fh.createWritable();
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
- async read(hash) {
3650
- const fh = await this.getFileHandle(hash, false);
3651
- if (!fh) return null;
3652
- const file = await fh.getFile();
3653
- const buffer = await file.arrayBuffer();
3654
- return new Uint8Array(buffer);
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
- async remove(hash) {
3657
- const { dir1, dir2, filename } = hashPath(hash);
3658
- try {
3659
- const root = await this.getRoot();
3660
- const d1 = await root.getDirectoryHandle(dir1, { create: false });
3661
- const d2 = await d1.getDirectoryHandle(dir2, { create: false });
3662
- await d2.removeEntry(filename);
3663
- } catch (err) {
3664
- if (err instanceof DOMException && err.name === "NotFoundError") {
3665
- return;
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
- async exists(hash) {
3671
- const fh = await this.getFileHandle(hash, false);
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
- var IdbBlobStore = class {
3688
- dbName;
3689
- _db = null;
3690
- constructor(archiveName) {
3691
- this.dbName = `fortemi-${archiveName}-blobs`;
3692
- }
3693
- async db() {
3694
- if (!this._db) {
3695
- this._db = await openDb(this.dbName);
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
- async write(hash, data) {
3700
- const db = await this.db();
3701
- return new Promise((resolve, reject) => {
3702
- const tx = db.transaction(IDB_STORE, "readwrite");
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
- async read(hash) {
3709
- const db = await this.db();
3710
- return new Promise((resolve, reject) => {
3711
- const tx = db.transaction(IDB_STORE, "readonly");
3712
- const req = tx.objectStore(IDB_STORE).get(hash);
3713
- req.onsuccess = () => resolve(req.result ?? null);
3714
- req.onerror = () => reject(req.error);
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 remove(hash) {
3718
- const db = await this.db();
3719
- return new Promise((resolve, reject) => {
3720
- const tx = db.transaction(IDB_STORE, "readwrite");
3721
- tx.objectStore(IDB_STORE).delete(hash);
3722
- tx.oncomplete = () => resolve();
3723
- tx.onerror = () => reject(tx.error);
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 exists(hash) {
3727
- const db = await this.db();
3728
- return new Promise((resolve, reject) => {
3729
- const tx = db.transaction(IDB_STORE, "readonly");
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 MemoryBlobStore = class {
3737
- store = /* @__PURE__ */ new Map();
3738
- async write(hash, data) {
3739
- this.store.set(hash, data);
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 read(hash) {
3742
- return this.store.get(hash) ?? null;
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 remove(hash) {
3745
- this.store.delete(hash);
3946
+ async diagnostics() {
3947
+ return { backend: this.adapter.kind, probe: this.probe };
3746
3948
  }
3747
- async exists(hash) {
3748
- return this.store.has(hash);
3949
+ async close() {
3950
+ await this.facade.close();
3749
3951
  }
3750
3952
  };
3751
- function createBlobStore(archiveName) {
3752
- if (typeof navigator !== "undefined" && "storage" in navigator && "getDirectory" in navigator.storage) {
3753
- return new OpfsBlobStore(archiveName);
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
- return new IdbBlobStore(archiveName);
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
- * If a blob with the same BLAKE3 content hash already exists, the existing
5416
- * blob row is reused (deduplication). Otherwise a new blob row is inserted
5417
- * and the raw bytes are written to the BlobStore. The BLAKE3 `content_hash`
5418
- * (`blake3:<hex>`) matches the server convention and is the key used by the
5419
- * portable Knowledge-Shard byte sidecar.
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 = computeBlobHash(input.data);
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 if the blob cannot be found in the BlobStore.
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 att = await this.get(attachmentId);
5475
- const blob = await this.db.query(
5476
- `SELECT content_hash FROM attachment_blob WHERE id = $1`,
5477
- [att.blob_id]
5478
- );
5479
- if (blob.rows.length === 0) return null;
5480
- return this.blobStore.read(blob.rows[0].content_hash);
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 underlying blob row and BlobStore data are not removed.
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 decoder6 = new TextDecoder();
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 += decoder6.decode(value, { stream: true });
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
- [note.id, note.revised_content ?? note.original_content]
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
- [note.id, note.revised_content ?? note.original_content]
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 [hash, bytes] of blobsToHydrate) {
8640
- await options.blobStore.write(hash, bytes);
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
- [model, dimension]
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, model, slugifyServerEmbeddingSet(model), dimension]
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", "to_url", "kind", "score", "created_at", "metadata"],
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", "name", "slug", "description", "purpose", "document_count", "embedding_count", "is_system", "keywords", "model", "dimension"],
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", "membership_type", "added_at", "added_by"],
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", "chunk_index", "text", "vector", "model"],
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
- // schemas/aiwg-fortemi-index-export.schema.json
11664
- var aiwg_fortemi_index_export_schema_default = {
11665
- $schema: "https://json-schema.org/draft/2020-12/schema",
11666
- $id: "https://aiwg.io/schemas/aiwg-fortemi-index-export.json",
11667
- title: "AIWG Fortemi Index Export",
11668
- type: "object",
11669
- additionalProperties: false,
11670
- required: ["schema_version", "generated_at", "source", "items"],
11671
- properties: {
11672
- schema_version: {
11673
- enum: ["aiwg.fortemi.index.export.v1", "aiwg.fortemi.index.export.v2"]
11674
- },
11675
- generated_at: {
11676
- type: "string",
11677
- format: "date-time"
11678
- },
11679
- source: {
11680
- type: "object",
11681
- additionalProperties: false,
11682
- required: ["repo", "privacy"],
11683
- properties: {
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.5";
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