@fortemi/core 2026.5.2 → 2026.5.4

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
@@ -667,13 +667,134 @@ var migration0005 = {
667
667
  `
668
668
  };
669
669
 
670
+ // src/migrations/0006_embedding_set_metadata.ts
671
+ var migration0006 = {
672
+ version: 6,
673
+ name: "0006_embedding_set_metadata",
674
+ sql: `
675
+ ALTER TABLE embedding_set
676
+ ADD COLUMN IF NOT EXISTS name TEXT NOT NULL DEFAULT 'Full content';
677
+
678
+ ALTER TABLE embedding_set
679
+ ADD COLUMN IF NOT EXISTS purpose TEXT;
680
+ `
681
+ };
682
+
683
+ // src/migrations/0007_virtual_embedding_sets.ts
684
+ var migration0007 = {
685
+ version: 7,
686
+ name: "0007_virtual_embedding_sets",
687
+ sql: `
688
+ ALTER TABLE embedding_set
689
+ ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'physical';
690
+
691
+ ALTER TABLE embedding_set
692
+ ADD COLUMN IF NOT EXISTS mode TEXT;
693
+
694
+ ALTER TABLE embedding_set
695
+ ADD COLUMN IF NOT EXISTS truncate_dimension INTEGER;
696
+
697
+ ALTER TABLE embedding_set
698
+ ADD COLUMN IF NOT EXISTS criteria_json JSONB;
699
+
700
+ ALTER TABLE embedding_set
701
+ ADD COLUMN IF NOT EXISTS source_json JSONB;
702
+
703
+ ALTER TABLE embedding_set
704
+ ADD COLUMN IF NOT EXISTS compatibility_json JSONB;
705
+
706
+ ALTER TABLE embedding_set
707
+ ADD COLUMN IF NOT EXISTS materialization_json JSONB;
708
+
709
+ ALTER TABLE embedding_set
710
+ ADD COLUMN IF NOT EXISTS freshness_json JSONB;
711
+
712
+ ALTER TABLE embedding_set
713
+ ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
714
+ `
715
+ };
716
+
717
+ // src/migrations/0008_graph_community_artifacts.ts
718
+ var migration0008 = {
719
+ version: 8,
720
+ name: "0008_graph_community_artifacts",
721
+ sql: `
722
+ CREATE TABLE IF NOT EXISTS graph_source (
723
+ id TEXT PRIMARY KEY,
724
+ name TEXT NOT NULL,
725
+ kind TEXT NOT NULL,
726
+ source_table TEXT,
727
+ embedding_set_id TEXT,
728
+ virtual_set_id TEXT,
729
+ model TEXT,
730
+ dimension INTEGER,
731
+ truncate_dimension INTEGER,
732
+ metric TEXT,
733
+ algorithm TEXT,
734
+ parameters_json JSONB,
735
+ input_hash TEXT NOT NULL,
736
+ freshness_json JSONB NOT NULL,
737
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
738
+ );
739
+
740
+ CREATE TABLE IF NOT EXISTS graph_edge_artifact (
741
+ graph_source_id TEXT NOT NULL REFERENCES graph_source(id) ON DELETE CASCADE,
742
+ from_note_id TEXT NOT NULL REFERENCES note(id),
743
+ to_note_id TEXT NOT NULL REFERENCES note(id),
744
+ weight DOUBLE PRECISION NOT NULL,
745
+ kind TEXT NOT NULL,
746
+ rank INTEGER,
747
+ metadata_json JSONB,
748
+ PRIMARY KEY (graph_source_id, from_note_id, to_note_id, kind)
749
+ );
750
+
751
+ CREATE TABLE IF NOT EXISTS community_set (
752
+ id TEXT PRIMARY KEY,
753
+ graph_source_id TEXT NOT NULL REFERENCES graph_source(id) ON DELETE CASCADE,
754
+ name TEXT NOT NULL,
755
+ source_type TEXT NOT NULL,
756
+ algorithm TEXT,
757
+ parameters_json JSONB,
758
+ input_hash TEXT NOT NULL,
759
+ freshness_json JSONB NOT NULL,
760
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
761
+ );
762
+
763
+ CREATE TABLE IF NOT EXISTS community (
764
+ id TEXT NOT NULL,
765
+ community_set_id TEXT NOT NULL REFERENCES community_set(id) ON DELETE CASCADE,
766
+ label TEXT,
767
+ rank INTEGER,
768
+ size INTEGER,
769
+ confidence DOUBLE PRECISION,
770
+ representative_note_ids TEXT[],
771
+ metadata_json JSONB,
772
+ PRIMARY KEY (community_set_id, id)
773
+ );
774
+
775
+ CREATE TABLE IF NOT EXISTS community_assignment (
776
+ community_set_id TEXT NOT NULL REFERENCES community_set(id) ON DELETE CASCADE,
777
+ community_id TEXT NOT NULL,
778
+ note_id TEXT NOT NULL REFERENCES note(id),
779
+ confidence DOUBLE PRECISION,
780
+ source_type TEXT NOT NULL,
781
+ metadata_json JSONB,
782
+ PRIMARY KEY (community_set_id, note_id),
783
+ FOREIGN KEY (community_set_id, community_id) REFERENCES community(community_set_id, id) ON DELETE CASCADE
784
+ );
785
+ `
786
+ };
787
+
670
788
  // src/migrations/index.ts
671
789
  var allMigrations = [
672
790
  migration0001,
673
791
  migration0002,
674
792
  migration0003,
675
793
  migration0004,
676
- migration0005
794
+ migration0005,
795
+ migration0006,
796
+ migration0007,
797
+ migration0008
677
798
  ];
678
799
 
679
800
  // src/archive-manager.ts
@@ -1547,26 +1668,410 @@ function buildNoteConditions(options, startIdx, includeDeleted = false) {
1547
1668
  return { conditions, params, nextIdx: idx };
1548
1669
  }
1549
1670
 
1671
+ // src/repositories/embedding-sets-repository.ts
1672
+ var DEFAULT_COMPATIBILITY = {
1673
+ model: "require-same",
1674
+ dimension: "require-same",
1675
+ duplicateVectors: "prefer-set-order",
1676
+ missingVectors: "omit"
1677
+ };
1678
+ function jsonParam(value) {
1679
+ return value == null ? null : JSON.stringify(value);
1680
+ }
1681
+ function asObject(value) {
1682
+ if (value == null) return null;
1683
+ if (typeof value === "string") return JSON.parse(value);
1684
+ return value;
1685
+ }
1686
+ function dateString(value) {
1687
+ if (!value) return void 0;
1688
+ return value instanceof Date ? value.toISOString() : value;
1689
+ }
1690
+ function dateMillis(value) {
1691
+ return value instanceof Date ? value.getTime() : new Date(value).getTime();
1692
+ }
1693
+ var EmbeddingSetsRepository = class {
1694
+ constructor(db) {
1695
+ this.db = db;
1696
+ }
1697
+ async create(input) {
1698
+ const id = input.id ?? generateId();
1699
+ await this.db.query(
1700
+ `INSERT INTO embedding_set (
1701
+ id, name, purpose, model_name, dimensions, kind, mode,
1702
+ truncate_dimension, criteria_json
1703
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`,
1704
+ [
1705
+ id,
1706
+ input.name,
1707
+ input.purpose ?? null,
1708
+ input.model_name ?? "all-MiniLM-L6-v2",
1709
+ input.dimensions ?? 384,
1710
+ input.kind ?? "physical",
1711
+ input.mode ?? null,
1712
+ input.truncate_dimension ?? null,
1713
+ jsonParam(input.criteria ?? null)
1714
+ ]
1715
+ );
1716
+ return this.get(id);
1717
+ }
1718
+ async createVirtualDefinition(input) {
1719
+ const id = input.id ?? generateId();
1720
+ await this.db.query(
1721
+ `INSERT INTO embedding_set (
1722
+ id, name, purpose, model_name, dimensions, kind, mode,
1723
+ source_json, compatibility_json, materialization_json, freshness_json, created_at, updated_at
1724
+ ) VALUES ($1, $2, $3, $4, $5, 'virtual', 'auto', $6::jsonb, $7::jsonb, $8::jsonb, $9::jsonb,
1725
+ COALESCE($10::timestamptz, now()), COALESCE($11::timestamptz, now()))`,
1726
+ [
1727
+ id,
1728
+ input.name,
1729
+ input.purpose ?? null,
1730
+ this.inferDefinitionModel(input) ?? "virtual",
1731
+ this.inferDefinitionDimension(input) ?? 0,
1732
+ jsonParam(input.source),
1733
+ jsonParam(input.compatibility),
1734
+ jsonParam(input.materialization ?? null),
1735
+ jsonParam({ status: input.materialization?.freshness ?? "unknown" }),
1736
+ input.createdAt ?? null,
1737
+ input.updatedAt ?? null
1738
+ ]
1739
+ );
1740
+ return this.get(id);
1741
+ }
1742
+ async ensureDefault() {
1743
+ const existing = await this.db.query(
1744
+ `SELECT * FROM embedding_set WHERE name = $1 AND model_name = $2 AND kind = 'physical' ORDER BY created_at LIMIT 1`,
1745
+ ["Full content", "all-MiniLM-L6-v2"]
1746
+ );
1747
+ if (existing.rows.length > 0) return existing.rows[0];
1748
+ return this.create({
1749
+ name: "Full content",
1750
+ purpose: "Semantic search over full revised note content",
1751
+ model_name: "all-MiniLM-L6-v2",
1752
+ dimensions: 384,
1753
+ kind: "physical"
1754
+ });
1755
+ }
1756
+ async get(id) {
1757
+ const result = await this.db.query(
1758
+ `SELECT * FROM embedding_set WHERE id = $1`,
1759
+ [id]
1760
+ );
1761
+ if (result.rows.length === 0) throw new Error(`Embedding set not found: ${id}`);
1762
+ return result.rows[0];
1763
+ }
1764
+ async list() {
1765
+ const result = await this.db.query(
1766
+ `SELECT * FROM embedding_set ORDER BY created_at, name`
1767
+ );
1768
+ return result.rows;
1769
+ }
1770
+ async listDescriptors() {
1771
+ const rows = await this.list();
1772
+ return rows.map((row) => this.toDescriptor(row));
1773
+ }
1774
+ toDescriptor(row) {
1775
+ return {
1776
+ id: row.id,
1777
+ name: row.name,
1778
+ purpose: row.purpose,
1779
+ kind: row.kind,
1780
+ mode: row.mode ?? void 0,
1781
+ model: row.model_name,
1782
+ dimension: row.dimensions,
1783
+ truncateDimension: row.truncate_dimension,
1784
+ criteria: asObject(row.criteria_json),
1785
+ createdAt: dateString(row.created_at),
1786
+ updatedAt: dateString(row.updated_at),
1787
+ freshness: asObject(row.freshness_json) ?? { status: "fresh" }
1788
+ };
1789
+ }
1790
+ async putEmbedding(input) {
1791
+ const set = await this.get(input.embedding_set_id);
1792
+ if (set.kind === "virtual") {
1793
+ throw new Error(`Cannot store vectors directly in virtual embedding set: ${set.id}`);
1794
+ }
1795
+ if (input.vector.length !== set.dimensions) {
1796
+ throw new Error(
1797
+ `Embedding vector has ${input.vector.length} dimensions; set ${set.id} expects ${set.dimensions}`
1798
+ );
1799
+ }
1800
+ await this.db.query(
1801
+ `DELETE FROM embedding_set_member WHERE note_id = $1 AND embedding_set_id = $2`,
1802
+ [input.note_id, input.embedding_set_id]
1803
+ );
1804
+ await this.db.query(
1805
+ `DELETE FROM embedding WHERE note_id = $1 AND embedding_set_id = $2`,
1806
+ [input.note_id, input.embedding_set_id]
1807
+ );
1808
+ const embeddingId = input.id ?? generateId();
1809
+ const vector2 = `[${input.vector.join(",")}]`;
1810
+ await this.db.query(
1811
+ `INSERT INTO embedding (id, note_id, embedding_set_id, vector)
1812
+ VALUES ($1, $2, $3, $4::vector)`,
1813
+ [embeddingId, input.note_id, input.embedding_set_id, vector2]
1814
+ );
1815
+ await this.db.query(
1816
+ `INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id)
1817
+ VALUES ($1, $2, $3)`,
1818
+ [input.embedding_set_id, input.note_id, embeddingId]
1819
+ );
1820
+ return { id: embeddingId };
1821
+ }
1822
+ async resolveSelector(selector) {
1823
+ if (selector.kind === "default") {
1824
+ const set = await this.ensureDefault();
1825
+ return this.resolvePhysicalSet({ kind: "embedding-set", embeddingSetId: set.id }, set.id);
1826
+ }
1827
+ if (selector.kind === "embedding-set") {
1828
+ if (!selector.embeddingSetId) throw new Error("embedding-set selector requires embeddingSetId");
1829
+ const set = await this.get(selector.embeddingSetId);
1830
+ if (set.kind === "virtual") {
1831
+ const definition = this.definitionFromRow(set);
1832
+ return this.resolveDefinition({ kind: "embedding-set", embeddingSetId: set.id }, definition);
1833
+ }
1834
+ return this.resolvePhysicalSet(selector, set.id);
1835
+ }
1836
+ if (!selector.definition) throw new Error("virtual-definition selector requires definition");
1837
+ return this.resolveDefinition(selector, selector.definition);
1838
+ }
1839
+ async resolveDefinition(selector, definition) {
1840
+ let rows;
1841
+ const errors = [];
1842
+ switch (definition.source.type) {
1843
+ case "criteria":
1844
+ rows = await this.resolveCriteriaSource(definition.source);
1845
+ break;
1846
+ case "set-operation":
1847
+ rows = await this.resolveSetOperationSource(definition.source, definition.compatibility, errors);
1848
+ break;
1849
+ case "fallback":
1850
+ rows = await this.resolveFallbackSource(definition.source.preferredSetIds, definition.compatibility, errors);
1851
+ break;
1852
+ case "latest-compatible":
1853
+ rows = await this.resolveLatestCompatibleSource(definition.source, definition.compatibility, errors);
1854
+ break;
1855
+ case "snapshot":
1856
+ rows = await this.resolvePhysicalRows(definition.source.snapshotId);
1857
+ break;
1858
+ default:
1859
+ rows = [];
1860
+ }
1861
+ return this.finalizeResolution(selector, rows, errors, definition.compatibility, definition.materialization?.freshness ?? "unknown");
1862
+ }
1863
+ async resolvePhysicalSet(selector, setId) {
1864
+ return this.finalizeResolution(selector, await this.resolvePhysicalRows(setId), [], DEFAULT_COMPATIBILITY, "fresh");
1865
+ }
1866
+ async resolvePhysicalRows(setId) {
1867
+ const result = await this.db.query(
1868
+ `SELECT note_id, embedding_set_id, id as embedding_id, vector::text as vector, created_at
1869
+ FROM embedding
1870
+ WHERE embedding_set_id = $1
1871
+ ORDER BY note_id, created_at DESC`,
1872
+ [setId]
1873
+ );
1874
+ return result.rows;
1875
+ }
1876
+ async resolveCriteriaSource(source) {
1877
+ const criteria = source.criteria;
1878
+ if (criteria.conceptIds && criteria.conceptIds.length > 0) {
1879
+ throw new Error("Unsupported virtual embedding-set criteria field: conceptIds");
1880
+ }
1881
+ const conditions = ["e.embedding_set_id = $1"];
1882
+ const params = [source.baseSetId];
1883
+ let idx = 2;
1884
+ if (criteria.noteIds?.length) {
1885
+ conditions.push(`n.id = ANY($${idx++})`);
1886
+ params.push(criteria.noteIds);
1887
+ }
1888
+ if (criteria.tags?.length) {
1889
+ conditions.push(`EXISTS (SELECT 1 FROM note_tag nt WHERE nt.note_id = n.id AND nt.tag = ANY($${idx++}))`);
1890
+ params.push(criteria.tags);
1891
+ }
1892
+ if (criteria.collectionIds?.length) {
1893
+ conditions.push(`EXISTS (SELECT 1 FROM collection_note cn WHERE cn.note_id = n.id AND cn.collection_id = ANY($${idx++}))`);
1894
+ params.push(criteria.collectionIds);
1895
+ }
1896
+ if (criteria.updatedAfter) {
1897
+ conditions.push(`n.updated_at >= $${idx++}`);
1898
+ params.push(criteria.updatedAfter);
1899
+ }
1900
+ if (criteria.updatedBefore) {
1901
+ conditions.push(`n.updated_at <= $${idx++}`);
1902
+ params.push(criteria.updatedBefore);
1903
+ }
1904
+ if (criteria.query?.trim()) {
1905
+ conditions.push(`(n.tsv @@ plainto_tsquery('english', $${idx}) OR to_tsvector('english', coalesce(c.content, '')) @@ plainto_tsquery('english', $${idx}))`);
1906
+ params.push(criteria.query);
1907
+ }
1908
+ const result = await this.db.query(
1909
+ `SELECT e.note_id, e.embedding_set_id, e.id as embedding_id, e.vector::text as vector, e.created_at
1910
+ FROM embedding e
1911
+ JOIN note n ON n.id = e.note_id
1912
+ LEFT JOIN note_revised_current c ON c.note_id = n.id
1913
+ WHERE ${conditions.join(" AND ")}
1914
+ ORDER BY e.note_id, e.created_at DESC`,
1915
+ params
1916
+ );
1917
+ return result.rows;
1918
+ }
1919
+ async resolveSetOperationSource(source, compatibility, errors) {
1920
+ const bySet = /* @__PURE__ */ new Map();
1921
+ for (const setId of source.setIds) bySet.set(setId, await this.resolvePhysicalRows(setId));
1922
+ await this.validateCompatibility(source.setIds, compatibility, errors);
1923
+ const noteSets = source.setIds.map((setId) => new Set((bySet.get(setId) ?? []).map((row) => row.note_id)));
1924
+ const firstRows = bySet.get(source.setIds[0]) ?? [];
1925
+ if (source.operation === "difference") {
1926
+ const excluded = new Set(noteSets.slice(1).flatMap((set) => Array.from(set)));
1927
+ return firstRows.filter((row) => !excluded.has(row.note_id));
1928
+ }
1929
+ if (source.operation === "intersection") {
1930
+ return firstRows.filter((row) => noteSets.every((set) => set.has(row.note_id)));
1931
+ }
1932
+ return this.resolveDuplicateRows(source.setIds.flatMap((setId) => bySet.get(setId) ?? []), compatibility, errors);
1933
+ }
1934
+ async resolveFallbackSource(setIds, compatibility, errors) {
1935
+ await this.validateCompatibility(setIds, compatibility, errors);
1936
+ const selected = /* @__PURE__ */ new Map();
1937
+ for (const setId of setIds) {
1938
+ for (const row of await this.resolvePhysicalRows(setId)) {
1939
+ if (!selected.has(row.note_id)) selected.set(row.note_id, row);
1940
+ }
1941
+ }
1942
+ return Array.from(selected.values()).sort((a, b) => a.note_id.localeCompare(b.note_id));
1943
+ }
1944
+ async resolveLatestCompatibleSource(source, compatibility, errors) {
1945
+ const sets = [];
1946
+ for (const setId of source.candidateSetIds) {
1947
+ const set = await this.get(setId);
1948
+ if (source.model && set.model_name !== source.model) continue;
1949
+ if (source.dimension && set.dimensions !== source.dimension) continue;
1950
+ sets.push(set);
1951
+ }
1952
+ sets.sort((a, b) => dateMillis(b.updated_at) - dateMillis(a.updated_at) || dateMillis(b.created_at) - dateMillis(a.created_at));
1953
+ return this.resolveFallbackSource(sets.map((set) => set.id), compatibility, errors);
1954
+ }
1955
+ async validateCompatibility(setIds, compatibility, errors) {
1956
+ const sets = [];
1957
+ for (const setId of setIds) sets.push(await this.get(setId));
1958
+ if (compatibility.model === "require-same" && new Set(sets.map((set) => set.model_name)).size > 1) {
1959
+ errors.push({ code: "mixed-models", setIds });
1960
+ }
1961
+ if (compatibility.dimension === "require-same" && new Set(sets.map((set) => set.dimensions)).size > 1) {
1962
+ errors.push({ code: "mixed-dimensions", setIds });
1963
+ }
1964
+ }
1965
+ resolveDuplicateRows(rows, compatibility, errors) {
1966
+ const byNote = /* @__PURE__ */ new Map();
1967
+ for (const row of rows) {
1968
+ const existing = byNote.get(row.note_id) ?? [];
1969
+ existing.push(row);
1970
+ byNote.set(row.note_id, existing);
1971
+ }
1972
+ const resolved = [];
1973
+ for (const [noteId, noteRows] of byNote) {
1974
+ if (noteRows.length > 1 && compatibility.duplicateVectors === "error") {
1975
+ errors.push({ code: "duplicate-vector", noteId, setIds: noteRows.map((row) => row.embedding_set_id) });
1976
+ continue;
1977
+ }
1978
+ const ordered = [...noteRows];
1979
+ if (compatibility.duplicateVectors === "prefer-latest") {
1980
+ ordered.sort((a, b) => dateMillis(b.created_at) - dateMillis(a.created_at));
1981
+ }
1982
+ resolved.push(ordered[0]);
1983
+ }
1984
+ return resolved.sort((a, b) => a.note_id.localeCompare(b.note_id));
1985
+ }
1986
+ finalizeResolution(selector, rows, errors, compatibility, freshness) {
1987
+ const deduped = this.resolveDuplicateRows(rows, compatibility, errors);
1988
+ return {
1989
+ selector,
1990
+ rows: deduped,
1991
+ noteIds: deduped.map((row) => row.note_id),
1992
+ embeddingIds: deduped.map((row) => row.embedding_id),
1993
+ errors,
1994
+ freshness: { status: freshness }
1995
+ };
1996
+ }
1997
+ definitionFromRow(row) {
1998
+ const source = asObject(row.source_json);
1999
+ if (!source) throw new Error(`Virtual embedding set has no source definition: ${row.id}`);
2000
+ return {
2001
+ id: row.id,
2002
+ name: row.name,
2003
+ purpose: row.purpose,
2004
+ source,
2005
+ compatibility: asObject(row.compatibility_json) ?? DEFAULT_COMPATIBILITY,
2006
+ materialization: asObject(row.materialization_json) ?? void 0,
2007
+ createdAt: dateString(row.created_at),
2008
+ updatedAt: dateString(row.updated_at)
2009
+ };
2010
+ }
2011
+ inferDefinitionModel(input) {
2012
+ if (input.source.type === "latest-compatible") return input.source.model ?? null;
2013
+ return null;
2014
+ }
2015
+ inferDefinitionDimension(input) {
2016
+ if (input.source.type === "latest-compatible") return input.source.dimension ?? null;
2017
+ return null;
2018
+ }
2019
+ };
2020
+
1550
2021
  // src/repositories/search-repository.ts
1551
2022
  var SearchRepository = class {
1552
2023
  constructor(db, semanticAvailable = false) {
1553
2024
  this.db = db;
1554
2025
  this.semanticAvailable = semanticAvailable;
1555
2026
  }
1556
- /** Select tsquery function based on whether query contains quoted phrases */
1557
2027
  tsqueryFn(query) {
1558
2028
  return query.includes('"') ? "phraseto_tsquery" : "plainto_tsquery";
1559
2029
  }
1560
- /** Returns a Set of note IDs that have an embedding record */
1561
- async fetchEmbeddingSet(noteIds) {
2030
+ async fetchEmbeddingSet(noteIds, embeddingSetId) {
1562
2031
  if (noteIds.length === 0) return /* @__PURE__ */ new Set();
2032
+ const params = [noteIds];
2033
+ const setFilter = embeddingSetId ? " AND embedding_set_id = $2" : "";
2034
+ if (embeddingSetId) params.push(embeddingSetId);
1563
2035
  const result = await this.db.query(
1564
- `SELECT note_id FROM embedding WHERE note_id = ANY($1)`,
1565
- [noteIds]
2036
+ "SELECT note_id FROM embedding WHERE note_id = ANY($1)" + setFilter,
2037
+ params
1566
2038
  );
1567
2039
  return new Set(result.rows.map((r) => r.note_id));
1568
2040
  }
1569
- /** Attach has_embedding to each SearchResult using the provided embedding set */
2041
+ selectorFromOptions(options) {
2042
+ if (options.embeddingSetSelector) return options.embeddingSetSelector;
2043
+ if (options.embeddingSetId) return { kind: "embedding-set", embeddingSetId: options.embeddingSetId };
2044
+ return null;
2045
+ }
2046
+ async resolveEmbeddingSet(options) {
2047
+ const selector = this.selectorFromOptions(options);
2048
+ if (!selector) return null;
2049
+ return new EmbeddingSetsRepository(this.db).resolveSelector(selector);
2050
+ }
2051
+ scopeToResolvedEmbeddingSet(conditions, params, paramIdx, resolved) {
2052
+ if (!resolved) return paramIdx;
2053
+ if (resolved.noteIds.length === 0) {
2054
+ conditions.push("FALSE");
2055
+ return paramIdx;
2056
+ }
2057
+ conditions.push("n.id = ANY($" + paramIdx + ")");
2058
+ params.push(resolved.noteIds);
2059
+ return paramIdx + 1;
2060
+ }
2061
+ scopeToResolvedEmbeddingRows(conditions, params, paramIdx, resolved) {
2062
+ if (!resolved) return paramIdx;
2063
+ if (resolved.embeddingIds.length === 0) {
2064
+ conditions.push("FALSE");
2065
+ return paramIdx;
2066
+ }
2067
+ conditions.push("e.id = ANY($" + paramIdx + ")");
2068
+ params.push(resolved.embeddingIds);
2069
+ return paramIdx + 1;
2070
+ }
2071
+ async fetchEmbeddingStatus(noteIds, resolved, embeddingSetId) {
2072
+ if (resolved) return new Set(noteIds.filter((id) => resolved.noteIds.includes(id)));
2073
+ return this.fetchEmbeddingSet(noteIds, embeddingSetId);
2074
+ }
1570
2075
  attachEmbeddingStatus(results, embeddingSet) {
1571
2076
  return results.map((r) => ({ ...r, has_embedding: embeddingSet.has(r.id) }));
1572
2077
  }
@@ -1574,9 +2079,7 @@ var SearchRepository = class {
1574
2079
  const { limit = 20, offset = 0 } = options;
1575
2080
  const mode = options.mode ?? "auto";
1576
2081
  if (mode === "text") {
1577
- if (!query.trim()) {
1578
- return this.recentNotes(options);
1579
- }
2082
+ if (!query.trim()) return this.recentNotes(options);
1580
2083
  } else if (mode === "semantic") {
1581
2084
  if (!queryEmbedding || queryEmbedding.length === 0) {
1582
2085
  throw new Error("mode=semantic requires a query embedding");
@@ -1589,26 +2092,21 @@ var SearchRepository = class {
1589
2092
  return this.hybridSearch(query, queryEmbedding, options);
1590
2093
  } else {
1591
2094
  if (queryEmbedding && queryEmbedding.length > 0) {
1592
- if (query.trim()) {
1593
- return this.hybridSearch(query, queryEmbedding, options);
1594
- }
2095
+ if (query.trim()) return this.hybridSearch(query, queryEmbedding, options);
1595
2096
  return this.semanticSearch(queryEmbedding, options);
1596
2097
  }
1597
- if (!query.trim()) {
1598
- return this.recentNotes(options);
1599
- }
1600
- }
1601
- if (!query.trim()) {
1602
- return this.recentNotes(options);
2098
+ if (!query.trim()) return this.recentNotes(options);
1603
2099
  }
2100
+ if (!query.trim()) return this.recentNotes(options);
2101
+ const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
1604
2102
  const tsqFn = this.tsqueryFn(query);
1605
2103
  const { conditions, params, nextIdx } = buildNoteConditions(options, 2);
1606
2104
  conditions.unshift(
1607
2105
  `(n.tsv @@ ${tsqFn}('english', $1) OR
1608
2106
  to_tsvector('english', coalesce(c.content, '')) @@ ${tsqFn}('english', $1))`
1609
2107
  );
2108
+ let paramIdx = this.scopeToResolvedEmbeddingSet(conditions, params, nextIdx, resolvedEmbeddingSet);
1610
2109
  const allParams = [query, ...params];
1611
- let paramIdx = nextIdx;
1612
2110
  const where = conditions.join(" AND ");
1613
2111
  const countResult = await this.db.query(
1614
2112
  `SELECT COUNT(*) as count
@@ -1641,7 +2139,7 @@ var SearchRepository = class {
1641
2139
  const resultIds = result.rows.map((r) => r.id);
1642
2140
  const [tagMap, embeddingSet] = await Promise.all([
1643
2141
  this.fetchTagMap(resultIds),
1644
- this.fetchEmbeddingSet(resultIds)
2142
+ this.fetchEmbeddingStatus(resultIds, resolvedEmbeddingSet, options.embeddingSetId)
1645
2143
  ]);
1646
2144
  let facets;
1647
2145
  if (options.include_facets) {
@@ -1673,15 +2171,12 @@ var SearchRepository = class {
1673
2171
  facets
1674
2172
  };
1675
2173
  }
1676
- /**
1677
- * Semantic search using pgvector cosine distance.
1678
- * Returns notes ranked by vector similarity to the query embedding.
1679
- */
1680
2174
  async semanticSearch(queryEmbedding, options = {}) {
1681
2175
  const { limit = 20, offset = 0 } = options;
1682
2176
  const vectorStr = `[${queryEmbedding.join(",")}]`;
2177
+ const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
1683
2178
  const { conditions, params, nextIdx } = buildNoteConditions(options, 1);
1684
- let paramIdx = nextIdx;
2179
+ let paramIdx = this.scopeToResolvedEmbeddingRows(conditions, params, nextIdx, resolvedEmbeddingSet);
1685
2180
  const where = conditions.join(" AND ");
1686
2181
  const countResult = await this.db.query(
1687
2182
  `SELECT COUNT(*) as count
@@ -1707,26 +2202,20 @@ var SearchRepository = class {
1707
2202
  [...params, vectorStr, limit, offset]
1708
2203
  );
1709
2204
  const tagMap = await this.fetchTagMap(result.rows.map((r) => r.id));
1710
- let facets;
1711
- if (options.include_facets) {
1712
- const idsResult = await this.db.query(
1713
- `SELECT n.id FROM embedding e JOIN note n ON n.id = e.note_id WHERE ${where}`,
1714
- params
1715
- );
1716
- facets = await this.fetchFacets(idsResult.rows.map((r) => r.id));
1717
- }
2205
+ const facets = options.include_facets ? await this.fetchFacets((await this.db.query(
2206
+ `SELECT n.id FROM embedding e JOIN note n ON n.id = e.note_id WHERE ${where}`,
2207
+ params
2208
+ )).rows.map((r) => r.id)) : void 0;
1718
2209
  return {
1719
2210
  results: result.rows.map((r) => ({
1720
2211
  id: r.id,
1721
2212
  title: r.title,
1722
2213
  snippet: r.snippet ?? "",
1723
2214
  rank: 1 - r.distance,
1724
- // Convert distance to similarity score
1725
2215
  created_at: r.created_at,
1726
2216
  updated_at: r.updated_at,
1727
2217
  tags: tagMap.get(r.id) ?? [],
1728
2218
  has_embedding: true
1729
- // Semantic results always have embeddings (JOIN on embedding table)
1730
2219
  })),
1731
2220
  total,
1732
2221
  query: "",
@@ -1737,21 +2226,19 @@ var SearchRepository = class {
1737
2226
  facets
1738
2227
  };
1739
2228
  }
1740
- /**
1741
- * Hybrid search combining BM25 (full-text) and vector similarity using
1742
- * Reciprocal Rank Fusion (RRF, k=60).
1743
- */
1744
2229
  async hybridSearch(query, queryEmbedding, options = {}) {
1745
2230
  const { limit = 20, offset = 0 } = options;
1746
2231
  const vectorStr = `[${queryEmbedding.join(",")}]`;
1747
2232
  const k = 60;
1748
2233
  const tsqFn = this.tsqueryFn(query);
2234
+ const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
1749
2235
  const textCond = buildNoteConditions(options, 2);
1750
2236
  const textConditions = [
1751
2237
  ...textCond.conditions,
1752
2238
  `(n.tsv @@ ${tsqFn}('english', $1) OR
1753
2239
  to_tsvector('english', coalesce(c.content, '')) @@ ${tsqFn}('english', $1))`
1754
2240
  ];
2241
+ this.scopeToResolvedEmbeddingSet(textConditions, textCond.params, textCond.nextIdx, resolvedEmbeddingSet);
1755
2242
  const textWhere = textConditions.join(" AND ");
1756
2243
  const textParams = [query, ...textCond.params];
1757
2244
  const textResult = await this.db.query(
@@ -1768,6 +2255,7 @@ var SearchRepository = class {
1768
2255
  textParams
1769
2256
  );
1770
2257
  const vecCond = buildNoteConditions(options, 1);
2258
+ vecCond.nextIdx = this.scopeToResolvedEmbeddingRows(vecCond.conditions, vecCond.params, vecCond.nextIdx, resolvedEmbeddingSet);
1771
2259
  const vecWhere = vecCond.conditions.join(" AND ");
1772
2260
  const vecVecIdx = vecCond.nextIdx;
1773
2261
  const vectorResult = await this.db.query(
@@ -1781,26 +2269,16 @@ var SearchRepository = class {
1781
2269
  );
1782
2270
  const rrfScores = /* @__PURE__ */ new Map();
1783
2271
  textResult.rows.forEach((row, idx) => {
1784
- const score = 1 / (k + idx + 1);
1785
- rrfScores.set(row.id, (rrfScores.get(row.id) ?? 0) + score);
2272
+ rrfScores.set(row.id, (rrfScores.get(row.id) ?? 0) + 1 / (k + idx + 1));
1786
2273
  });
1787
2274
  vectorResult.rows.forEach((row, idx) => {
1788
- const score = 1 / (k + idx + 1);
1789
- rrfScores.set(row.id, (rrfScores.get(row.id) ?? 0) + score);
2275
+ rrfScores.set(row.id, (rrfScores.get(row.id) ?? 0) + 1 / (k + idx + 1));
1790
2276
  });
1791
2277
  const sortedIds = Array.from(rrfScores.entries()).sort((a, b) => b[1] - a[1]).map(([id]) => id);
1792
2278
  const total = sortedIds.length;
1793
2279
  const pageIds = sortedIds.slice(offset, offset + limit);
1794
2280
  if (pageIds.length === 0) {
1795
- return {
1796
- results: [],
1797
- total,
1798
- query,
1799
- mode: "hybrid",
1800
- semantic_available: this.semanticAvailable,
1801
- limit,
1802
- offset
1803
- };
2281
+ return { results: [], total, query, mode: "hybrid", semantic_available: this.semanticAvailable, limit, offset };
1804
2282
  }
1805
2283
  const noteResult = await this.db.query(
1806
2284
  `SELECT n.id, n.title, n.created_at, n.updated_at,
@@ -1813,7 +2291,7 @@ var SearchRepository = class {
1813
2291
  const noteMap = new Map(noteResult.rows.map((r) => [r.id, r]));
1814
2292
  const [tagMap, embeddingSet] = await Promise.all([
1815
2293
  this.fetchTagMap(pageIds),
1816
- this.fetchEmbeddingSet(pageIds)
2294
+ this.fetchEmbeddingStatus(pageIds, resolvedEmbeddingSet, options.embeddingSetId)
1817
2295
  ]);
1818
2296
  const facets = options.include_facets ? await this.fetchFacets(sortedIds) : void 0;
1819
2297
  return {
@@ -1842,8 +2320,9 @@ var SearchRepository = class {
1842
2320
  }
1843
2321
  async recentNotes(options = {}) {
1844
2322
  const { limit = 20, offset = 0 } = options;
2323
+ const resolvedEmbeddingSet = await this.resolveEmbeddingSet(options);
1845
2324
  const { conditions, params, nextIdx } = buildNoteConditions(options, 1);
1846
- let paramIdx = nextIdx;
2325
+ let paramIdx = this.scopeToResolvedEmbeddingSet(conditions, params, nextIdx, resolvedEmbeddingSet);
1847
2326
  const where = conditions.join(" AND ");
1848
2327
  const countResult = await this.db.query(
1849
2328
  `SELECT COUNT(*) as count FROM note n WHERE ${where}`,
@@ -1862,7 +2341,7 @@ var SearchRepository = class {
1862
2341
  listParams
1863
2342
  );
1864
2343
  const resultIds = result.rows.map((r) => r.id);
1865
- const embeddingSet = await this.fetchEmbeddingSet(resultIds);
2344
+ const embeddingSet = await this.fetchEmbeddingStatus(resultIds, resolvedEmbeddingSet, options.embeddingSetId);
1866
2345
  return {
1867
2346
  results: result.rows.map((r) => ({
1868
2347
  id: r.id,
@@ -1882,14 +2361,8 @@ var SearchRepository = class {
1882
2361
  offset
1883
2362
  };
1884
2363
  }
1885
- /**
1886
- * Fetch faceted aggregate counts for tags and collections across all matching note IDs.
1887
- * Uses the full (unpaginated) result set for accurate counts.
1888
- */
1889
2364
  async fetchFacets(noteIds) {
1890
- if (noteIds.length === 0) {
1891
- return { tags: [], collections: [] };
1892
- }
2365
+ if (noteIds.length === 0) return { tags: [], collections: [] };
1893
2366
  const [tagResult, collResult] = await Promise.all([
1894
2367
  this.db.query(
1895
2368
  `SELECT nt.tag, COUNT(*) as count FROM note_tag nt
@@ -1924,6 +2397,430 @@ var SearchRepository = class {
1924
2397
  }
1925
2398
  };
1926
2399
 
2400
+ // src/repositories/graph-repository.ts
2401
+ var SIMILARITY_GRAPH_ALGORITHM = "knn-v1";
2402
+ function hashJson(value) {
2403
+ return computeHash(new TextEncoder().encode(JSON.stringify(value)));
2404
+ }
2405
+ function sourceIdFor(inputHash) {
2406
+ return `similarity-${inputHash.replace(/^sha256:/, "").slice(0, 24)}`;
2407
+ }
2408
+ function jsonObject(value) {
2409
+ if (value == null) return null;
2410
+ if (typeof value === "string") return JSON.parse(value);
2411
+ return value;
2412
+ }
2413
+ function detectCommunities(edges, nodes = [], options = {}) {
2414
+ const nodeIds = new Set(nodes.map((n) => n.id));
2415
+ for (const edge of edges) {
2416
+ nodeIds.add(edge.source);
2417
+ nodeIds.add(edge.target);
2418
+ }
2419
+ const labels = /* @__PURE__ */ new Map();
2420
+ const adjacency = /* @__PURE__ */ new Map();
2421
+ for (const id of nodeIds) {
2422
+ labels.set(id, id);
2423
+ adjacency.set(id, []);
2424
+ }
2425
+ for (const edge of edges) {
2426
+ adjacency.get(edge.source)?.push({ node: edge.target, weight: edge.weight });
2427
+ adjacency.get(edge.target)?.push({ node: edge.source, weight: edge.weight });
2428
+ }
2429
+ const orderedNodes = Array.from(nodeIds).sort();
2430
+ const maxIterations = options.maxIterations ?? 20;
2431
+ for (let i = 0; i < maxIterations; i++) {
2432
+ let changed = false;
2433
+ for (const node of orderedNodes) {
2434
+ const scores = /* @__PURE__ */ new Map();
2435
+ for (const neighbor of adjacency.get(node) ?? []) {
2436
+ const label = labels.get(neighbor.node) ?? neighbor.node;
2437
+ scores.set(label, (scores.get(label) ?? 0) + neighbor.weight);
2438
+ }
2439
+ if (scores.size === 0) continue;
2440
+ const nextLabel = Array.from(scores.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))[0][0];
2441
+ if (nextLabel !== labels.get(node)) {
2442
+ labels.set(node, nextLabel);
2443
+ changed = true;
2444
+ }
2445
+ }
2446
+ if (!changed) break;
2447
+ }
2448
+ const byLabel = /* @__PURE__ */ new Map();
2449
+ for (const node of orderedNodes) {
2450
+ const label = labels.get(node) ?? node;
2451
+ const members = byLabel.get(label) ?? [];
2452
+ members.push(node);
2453
+ byLabel.set(label, members);
2454
+ }
2455
+ return Array.from(byLabel.values()).sort((a, b) => b.length - a.length || a[0].localeCompare(b[0])).map((members, index) => ({ id: `community-${index + 1}`, nodes: members }));
2456
+ }
2457
+ var GraphRepository = class {
2458
+ constructor(db) {
2459
+ this.db = db;
2460
+ }
2461
+ normalizeSimilarityRequest(request) {
2462
+ if (request.minSimilarity !== void 0 && request.threshold !== void 0 && request.minSimilarity !== request.threshold) {
2463
+ throw new Error("conflicting-threshold");
2464
+ }
2465
+ return {
2466
+ selector: request.selector,
2467
+ k: request.k ?? 5,
2468
+ minSimilarity: request.minSimilarity ?? request.threshold ?? -1,
2469
+ metric: request.metric ?? "cosine",
2470
+ source: request.source ?? "cache-preferred"
2471
+ };
2472
+ }
2473
+ async buildSimilarityGraph(embeddingSet, options = {}) {
2474
+ const selector = typeof embeddingSet === "string" ? { kind: "embedding-set", embeddingSetId: embeddingSet } : embeddingSet;
2475
+ return this.buildSimilarityGraphFromResolved(
2476
+ await new EmbeddingSetsRepository(this.db).resolveSelector(selector),
2477
+ options
2478
+ );
2479
+ }
2480
+ async buildSimilarityGraphLive(request) {
2481
+ const normalized = this.normalizeSimilarityRequest({ ...request, source: "live-only" });
2482
+ const resolved = await new EmbeddingSetsRepository(this.db).resolveSelector(normalized.selector);
2483
+ return this.buildSimilarityGraphFromResolved(resolved, normalized);
2484
+ }
2485
+ async getCachedSimilarityGraph(request) {
2486
+ const normalized = this.normalizeSimilarityRequest(request);
2487
+ const resolved = await new EmbeddingSetsRepository(this.db).resolveSelector(normalized.selector);
2488
+ const cacheKey = await this.computeSimilarityGraphCacheKey(normalized, resolved);
2489
+ const inputHash = hashJson(cacheKey);
2490
+ const source = await this.findGraphSource(inputHash);
2491
+ if (!source) return null;
2492
+ const graph = await this.graphFromArtifact(source.id, resolved.noteIds);
2493
+ return {
2494
+ graph,
2495
+ graphSource: { id: source.id, name: source.name, input_hash: source.input_hash, freshness: source.freshness },
2496
+ cache: "hit",
2497
+ freshness: source.freshness
2498
+ };
2499
+ }
2500
+ async buildOrLoadSimilarityGraph(request) {
2501
+ const normalized = this.normalizeSimilarityRequest(request);
2502
+ const resolved = await new EmbeddingSetsRepository(this.db).resolveSelector(normalized.selector);
2503
+ const cacheKey = await this.computeSimilarityGraphCacheKey(normalized, resolved);
2504
+ const inputHash = hashJson(cacheKey);
2505
+ if (normalized.source !== "live-only") {
2506
+ const cached = await this.findGraphSource(inputHash);
2507
+ if (cached?.freshness === "fresh") {
2508
+ const graph3 = await this.graphFromArtifact(cached.id, resolved.noteIds);
2509
+ return {
2510
+ graph: graph3,
2511
+ graphSource: { id: cached.id, name: cached.name, input_hash: cached.input_hash, freshness: cached.freshness },
2512
+ cache: "hit",
2513
+ freshness: "fresh"
2514
+ };
2515
+ }
2516
+ if (normalized.source === "cache-only") {
2517
+ throw new Error(cached ? "similarity graph cache stale" : "similarity graph cache miss");
2518
+ }
2519
+ const graph2 = await this.buildSimilarityGraphFromResolved(resolved, normalized);
2520
+ const graphSource = await this.saveSimilarityGraphArtifact({ graph: graph2, request: normalized, resolved, cacheKey, freshness: "fresh" });
2521
+ return {
2522
+ graph: graph2,
2523
+ graphSource,
2524
+ cache: cached ? "stale-live-built" : "miss-live-built",
2525
+ freshness: cached ? cached.freshness : "fresh"
2526
+ };
2527
+ }
2528
+ const graph = await this.buildSimilarityGraphFromResolved(resolved, normalized);
2529
+ return {
2530
+ graph,
2531
+ graphSource: { id: sourceIdFor(inputHash), name: "Live similarity graph", input_hash: inputHash, freshness: "unknown" },
2532
+ cache: "live-only",
2533
+ freshness: "unknown"
2534
+ };
2535
+ }
2536
+ async saveSimilarityGraphArtifact(input) {
2537
+ const inputHash = hashJson(input.cacheKey);
2538
+ const id = sourceIdFor(inputHash);
2539
+ const parameters = {
2540
+ k: input.request.k,
2541
+ minSimilarity: input.request.minSimilarity,
2542
+ metric: input.request.metric,
2543
+ algorithm: SIMILARITY_GRAPH_ALGORITHM,
2544
+ selectorHash: input.cacheKey.selectorHash,
2545
+ parameterHash: input.cacheKey.parameterHash
2546
+ };
2547
+ await this.db.query(
2548
+ `INSERT INTO graph_source (id, name, kind, source_table, embedding_set_id, virtual_set_id, model, dimension, metric, algorithm, parameters_json, input_hash, freshness_json)
2549
+ VALUES ($1, $2, 'similarity', 'embedding', $3, $4, $5, $6, $7, $8, $9::jsonb, $10, $11::jsonb)
2550
+ ON CONFLICT (id) DO UPDATE SET parameters_json = $9::jsonb, input_hash = $10, freshness_json = $11::jsonb`,
2551
+ [
2552
+ id,
2553
+ "Cached similarity graph",
2554
+ input.request.selector.kind === "embedding-set" ? input.request.selector.embeddingSetId ?? null : null,
2555
+ input.request.selector.kind !== "embedding-set" ? input.request.selector.definition?.id ?? null : null,
2556
+ input.cacheKey.model,
2557
+ input.cacheKey.dimension,
2558
+ input.request.metric,
2559
+ SIMILARITY_GRAPH_ALGORITHM,
2560
+ JSON.stringify(parameters),
2561
+ inputHash,
2562
+ JSON.stringify({ status: input.freshness ?? "fresh" })
2563
+ ]
2564
+ );
2565
+ await this.db.query(`DELETE FROM graph_edge_artifact WHERE graph_source_id = $1`, [id]);
2566
+ let rank = 1;
2567
+ for (const edge of input.graph.edges) {
2568
+ await this.db.query(
2569
+ `INSERT INTO graph_edge_artifact (graph_source_id, from_note_id, to_note_id, weight, kind, rank)
2570
+ VALUES ($1, $2, $3, $4, 'similarity', $5)`,
2571
+ [id, edge.source, edge.target, edge.weight, rank++]
2572
+ );
2573
+ }
2574
+ return { id, name: "Cached similarity graph", input_hash: inputHash, freshness: input.freshness ?? "fresh" };
2575
+ }
2576
+ async markSimilarityGraphStale(graphSourceId, reason) {
2577
+ await this.db.query(
2578
+ `UPDATE graph_source SET freshness_json = $2::jsonb WHERE id = $1`,
2579
+ [graphSourceId, JSON.stringify({ status: "stale", stale_reason: reason, checked_at: (/* @__PURE__ */ new Date()).toISOString() })]
2580
+ );
2581
+ }
2582
+ async loadGraphArtifact(graphSourceId, noteIds = []) {
2583
+ return this.graphFromArtifact(graphSourceId, noteIds);
2584
+ }
2585
+ async buildSimilarityGraphFromResolved(resolved, options) {
2586
+ const k = options.k ?? 5;
2587
+ const minSimilarity = options.minSimilarity ?? options.threshold ?? -1;
2588
+ const embeddings = resolved.rows;
2589
+ const nodes = embeddings.map((row) => ({ id: row.note_id }));
2590
+ const edgeMap = /* @__PURE__ */ new Map();
2591
+ for (const row of embeddings) {
2592
+ const neighbors = await this.db.query(
2593
+ `SELECT note_id, 1 - (vector <=> $2::vector) as similarity
2594
+ FROM embedding
2595
+ WHERE id = ANY($1) AND note_id != $3
2596
+ ORDER BY vector <=> $2::vector ASC
2597
+ LIMIT $4`,
2598
+ [resolved.embeddingIds, row.vector, row.note_id, k]
2599
+ );
2600
+ for (const neighbor of neighbors.rows) {
2601
+ if (neighbor.similarity < minSimilarity) continue;
2602
+ const [source, target] = [row.note_id, neighbor.note_id].sort();
2603
+ const id = `${source}\0${target}`;
2604
+ const existing = edgeMap.get(id);
2605
+ if (!existing || neighbor.similarity > existing.weight) {
2606
+ edgeMap.set(id, { source, target, weight: neighbor.similarity, kind: "similarity" });
2607
+ }
2608
+ }
2609
+ }
2610
+ const edges = Array.from(edgeMap.values()).sort(
2611
+ (a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target)
2612
+ );
2613
+ return { nodes, edges, communities: detectCommunities(edges, nodes) };
2614
+ }
2615
+ async computeSimilarityGraphCacheKey(request, resolved) {
2616
+ const firstSetId = resolved.rows[0]?.embedding_set_id;
2617
+ const set = firstSetId ? await new EmbeddingSetsRepository(this.db).get(firstSetId) : null;
2618
+ return {
2619
+ selectorHash: hashJson(request.selector),
2620
+ resolvedEmbeddingSetId: request.selector.kind === "embedding-set" ? request.selector.embeddingSetId : void 0,
2621
+ virtualSetId: request.selector.kind === "virtual-definition" ? request.selector.definition?.id : void 0,
2622
+ k: request.k,
2623
+ minSimilarity: request.minSimilarity,
2624
+ metric: request.metric,
2625
+ model: set?.model_name ?? "unknown",
2626
+ dimension: set?.dimensions ?? 0,
2627
+ truncateDimension: set?.truncate_dimension ?? null,
2628
+ memberHash: hashJson(resolved.rows.map((row) => [row.note_id, row.embedding_set_id, row.embedding_id])),
2629
+ vectorHash: hashJson(resolved.rows.map((row) => [row.embedding_id, row.vector])),
2630
+ parameterHash: hashJson({ k: request.k, minSimilarity: request.minSimilarity, metric: request.metric, algorithm: SIMILARITY_GRAPH_ALGORITHM })
2631
+ };
2632
+ }
2633
+ async findGraphSource(inputHash) {
2634
+ const result = await this.db.query(
2635
+ `SELECT id, name, input_hash, freshness_json FROM graph_source WHERE kind = 'similarity' AND input_hash = $1 LIMIT 1`,
2636
+ [inputHash]
2637
+ );
2638
+ if (result.rows.length === 0) return null;
2639
+ const row = result.rows[0];
2640
+ const freshness = jsonObject(row.freshness_json);
2641
+ return { id: row.id, name: row.name, input_hash: row.input_hash, freshness: freshness?.status ?? "unknown" };
2642
+ }
2643
+ async graphFromArtifact(graphSourceId, noteIds) {
2644
+ const result = await this.db.query(
2645
+ `SELECT from_note_id as source, to_note_id as target, weight, kind
2646
+ FROM graph_edge_artifact
2647
+ WHERE graph_source_id = $1
2648
+ ORDER BY from_note_id, to_note_id`,
2649
+ [graphSourceId]
2650
+ );
2651
+ const nodes = Array.from(/* @__PURE__ */ new Set([...noteIds, ...result.rows.flatMap((edge) => [edge.source, edge.target])])).sort().map((id) => ({ id }));
2652
+ return { nodes, edges: result.rows, communities: detectCommunities(result.rows, nodes) };
2653
+ }
2654
+ async buildLinkGraph(linkType) {
2655
+ const params = [];
2656
+ const typeFilter = linkType ? " AND link_type = $1" : "";
2657
+ if (linkType) params.push(linkType);
2658
+ const result = await this.db.query(
2659
+ `SELECT source_note_id as source, target_note_id as target,
2660
+ COALESCE(confidence, 1.0) as weight, link_type as kind
2661
+ FROM link
2662
+ WHERE deleted_at IS NULL${typeFilter}
2663
+ ORDER BY source_note_id, target_note_id`,
2664
+ params
2665
+ );
2666
+ const nodes = Array.from(new Set(result.rows.flatMap((edge) => [edge.source, edge.target]))).sort().map((id) => ({ id }));
2667
+ return { nodes, edges: result.rows, communities: detectCommunities(result.rows, nodes) };
2668
+ }
2669
+ };
2670
+
2671
+ // src/repositories/communities-repository.ts
2672
+ function json(value) {
2673
+ return value == null ? null : JSON.stringify(value);
2674
+ }
2675
+ function parseObject(value) {
2676
+ if (value == null) return void 0;
2677
+ if (typeof value === "string") return JSON.parse(value);
2678
+ return value;
2679
+ }
2680
+ function iso(value) {
2681
+ if (!value) return void 0;
2682
+ return value instanceof Date ? value.toISOString() : value;
2683
+ }
2684
+ var CommunitiesRepository = class {
2685
+ constructor(db) {
2686
+ this.db = db;
2687
+ }
2688
+ async previewDynamicCommunity(filters) {
2689
+ const noteIds = await this.resolveFilterNoteIds(filters);
2690
+ return noteIds.map((noteId) => ({
2691
+ communitySourceId: "dynamic-preview",
2692
+ communityId: "dynamic-preview",
2693
+ noteId,
2694
+ label: "Dynamic preview",
2695
+ confidence: null,
2696
+ sourceType: "dynamic"
2697
+ }));
2698
+ }
2699
+ async saveCommunity(input) {
2700
+ const sourceId = generateId();
2701
+ const communitySetId = generateId();
2702
+ const communityId = generateId();
2703
+ const noteIds = input.noteIds ?? (input.filters ? await this.resolveFilterNoteIds(input.filters) : []);
2704
+ const sourceKind = input.sourceType === "dynamic-snapshot" ? "search" : "manual";
2705
+ const freshness = input.sourceType === "dynamic-snapshot" ? "fresh" : "unknown";
2706
+ await this.db.query(
2707
+ `INSERT INTO graph_source (id, name, kind, source_table, parameters_json, input_hash, freshness_json)
2708
+ VALUES ($1, $2, $3, 'manual', $4::jsonb, $5, $6::jsonb)`,
2709
+ [sourceId, input.name, sourceKind, json({ filters: input.filters ?? null }), `community:${sourceId}`, json({ status: freshness })]
2710
+ );
2711
+ await this.db.query(
2712
+ `INSERT INTO community_set (id, graph_source_id, name, source_type, parameters_json, input_hash, freshness_json)
2713
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7::jsonb)`,
2714
+ [communitySetId, sourceId, input.name, input.sourceType, json({ filters: input.filters ?? null }), `community:${communitySetId}`, json({ status: freshness })]
2715
+ );
2716
+ await this.db.query(
2717
+ `INSERT INTO community (community_set_id, id, label, rank, size, representative_note_ids)
2718
+ VALUES ($1, $2, $3, 1, $4, $5)`,
2719
+ [communitySetId, communityId, input.label ?? input.name, noteIds.length, input.representativeNoteIds ?? []]
2720
+ );
2721
+ for (const noteId of noteIds) {
2722
+ await this.db.query(
2723
+ `INSERT INTO community_assignment (community_set_id, community_id, note_id, confidence, source_type)
2724
+ VALUES ($1, $2, $3, NULL, $4)`,
2725
+ [communitySetId, communityId, noteId, input.sourceType]
2726
+ );
2727
+ }
2728
+ return {
2729
+ id: communitySetId,
2730
+ name: input.name,
2731
+ sourceType: input.sourceType,
2732
+ graphSourceId: sourceId,
2733
+ searchQuery: input.filters?.query,
2734
+ filters: input.filters,
2735
+ freshness
2736
+ };
2737
+ }
2738
+ async rerunDynamicCommunity(sourceId) {
2739
+ const result = await this.db.query(
2740
+ `SELECT parameters_json, source_type FROM community_set WHERE id = $1`,
2741
+ [sourceId]
2742
+ );
2743
+ if (result.rows.length === 0) throw new Error(`Community source not found: ${sourceId}`);
2744
+ const parameters = parseObject(result.rows[0].parameters_json);
2745
+ if (!parameters?.filters) return this.getCommunityAssignments(sourceId);
2746
+ const preview = await this.previewDynamicCommunity(parameters.filters);
2747
+ return preview.map((assignment) => ({ ...assignment, communitySourceId: sourceId, sourceType: "dynamic" }));
2748
+ }
2749
+ async listCommunitySources() {
2750
+ const result = await this.db.query(`SELECT * FROM community_set ORDER BY created_at, name`);
2751
+ return result.rows.map((row) => {
2752
+ const parameters = parseObject(row.parameters_json);
2753
+ const freshness = parseObject(row.freshness_json);
2754
+ return {
2755
+ id: row.id,
2756
+ name: row.name,
2757
+ sourceType: row.source_type,
2758
+ graphSourceId: row.graph_source_id,
2759
+ searchQuery: parameters?.filters?.query,
2760
+ filters: parameters?.filters ?? void 0,
2761
+ createdAt: iso(row.created_at),
2762
+ freshness: freshness?.status ?? "unknown"
2763
+ };
2764
+ });
2765
+ }
2766
+ async getCommunityAssignments(sourceId) {
2767
+ const result = await this.db.query(
2768
+ `SELECT ca.community_set_id, ca.community_id, ca.note_id, c.label, ca.confidence, ca.source_type
2769
+ FROM community_assignment ca
2770
+ LEFT JOIN community c ON c.community_set_id = ca.community_set_id AND c.id = ca.community_id
2771
+ WHERE ca.community_set_id = $1
2772
+ ORDER BY ca.community_id, ca.note_id`,
2773
+ [sourceId]
2774
+ );
2775
+ return result.rows.map((row) => ({
2776
+ communitySourceId: row.community_set_id,
2777
+ communityId: row.community_id,
2778
+ noteId: row.note_id,
2779
+ label: row.label,
2780
+ confidence: row.confidence,
2781
+ sourceType: row.source_type
2782
+ }));
2783
+ }
2784
+ async listCommunitySummaries(sourceId) {
2785
+ const result = await this.db.query(
2786
+ `SELECT c.id, c.label, cs.source_type, c.size, c.confidence, c.representative_note_ids, cs.freshness_json
2787
+ FROM community c
2788
+ JOIN community_set cs ON cs.id = c.community_set_id
2789
+ WHERE c.community_set_id = $1
2790
+ ORDER BY c.rank NULLS LAST, c.id`,
2791
+ [sourceId]
2792
+ );
2793
+ return result.rows.map((row) => {
2794
+ const freshness = parseObject(row.freshness_json);
2795
+ return {
2796
+ id: row.id,
2797
+ label: row.label ?? row.id,
2798
+ sourceType: row.source_type,
2799
+ size: row.size ?? 0,
2800
+ confidence: row.confidence,
2801
+ representativeNoteIds: row.representative_note_ids ?? [],
2802
+ freshness: freshness?.status ?? "unknown"
2803
+ };
2804
+ });
2805
+ }
2806
+ async resolveFilterNoteIds(filters) {
2807
+ if (filters.conceptIds?.length) {
2808
+ throw new Error("Community concept filters are not supported locally yet");
2809
+ }
2810
+ if (filters.noteIds?.length && !filters.query && !filters.tags?.length && !filters.collectionIds?.length && !filters.embeddingSetSelector) {
2811
+ return [...filters.noteIds].sort();
2812
+ }
2813
+ const result = await new SearchRepository(this.db, true).search(filters.query ?? "", {
2814
+ limit: 1e3,
2815
+ tags: filters.tags,
2816
+ collection_id: filters.collectionIds?.[0],
2817
+ embeddingSetSelector: filters.embeddingSetSelector
2818
+ });
2819
+ const ids = result.results.map((row) => row.id);
2820
+ return filters.noteIds?.length ? ids.filter((id) => filters.noteIds?.includes(id)) : ids;
2821
+ }
2822
+ };
2823
+
1927
2824
  // src/capabilities/llm-handler.ts
1928
2825
  var llmFn = null;
1929
2826
  function setLlmFunction(fn) {
@@ -2740,7 +3637,7 @@ function resolvePropertySchema(value) {
2740
3637
  }
2741
3638
  var TOOL_DEFINITIONS = [
2742
3639
  {
2743
- id: "mnemos.capture_knowledge",
3640
+ id: "fortemi.capture_knowledge",
2744
3641
  name: "Capture Knowledge",
2745
3642
  description: "WHEN you have text, ideas, or information to save. WHAT creates one or more notes in Fortemi. HOW accepts content, optional title/tags, supports create/bulk_create/from_template. OUT returns the created note(s) with full metadata.",
2746
3643
  category: "capture",
@@ -2749,7 +3646,7 @@ var TOOL_DEFINITIONS = [
2749
3646
  sideEffects: true
2750
3647
  },
2751
3648
  {
2752
- id: "mnemos.manage_note",
3649
+ id: "fortemi.manage_note",
2753
3650
  name: "Manage Note",
2754
3651
  description: "WHEN you need to modify an existing note. WHAT updates, deletes, restores, archives, or stars a note. HOW accepts note_id and action (update/delete/restore/archive/star). OUT returns the updated note.",
2755
3652
  category: "manage",
@@ -2758,7 +3655,7 @@ var TOOL_DEFINITIONS = [
2758
3655
  sideEffects: true
2759
3656
  },
2760
3657
  {
2761
- id: "mnemos.search",
3658
+ id: "fortemi.search",
2762
3659
  name: "Search Notes",
2763
3660
  description: "WHEN you need to find notes by content or metadata. WHAT performs full-text search across all notes. HOW accepts query string with optional tag/collection filters. OUT returns ranked results with highlighted snippets.",
2764
3661
  category: "search",
@@ -2767,7 +3664,7 @@ var TOOL_DEFINITIONS = [
2767
3664
  sideEffects: false
2768
3665
  },
2769
3666
  {
2770
- id: "mnemos.get_note",
3667
+ id: "fortemi.get_note",
2771
3668
  name: "Get Note",
2772
3669
  description: "WHEN you need the full content of a specific note. WHAT retrieves a single note by ID. HOW accepts note_id. OUT returns complete note with content, metadata, tags, and revision info.",
2773
3670
  category: "manage",
@@ -2776,7 +3673,7 @@ var TOOL_DEFINITIONS = [
2776
3673
  sideEffects: false
2777
3674
  },
2778
3675
  {
2779
- id: "mnemos.list_notes",
3676
+ id: "fortemi.list_notes",
2780
3677
  name: "List Notes",
2781
3678
  description: "WHEN you need to browse or filter notes. WHAT lists notes with pagination and filtering. HOW accepts optional filters (starred, archived, tags, collection). OUT returns paginated note summaries.",
2782
3679
  category: "manage",
@@ -2794,7 +3691,7 @@ var TOOL_DEFINITIONS = [
2794
3691
  sideEffects: false
2795
3692
  },
2796
3693
  {
2797
- id: "mnemos.manage_tags",
3694
+ id: "fortemi.manage_tags",
2798
3695
  name: "Manage Tags",
2799
3696
  description: "WHEN you need to organize notes with tags. WHAT adds or removes tags from notes. HOW accepts note_id, action (add/remove), and tag string. OUT confirms the tag operation.",
2800
3697
  category: "organize",
@@ -2807,7 +3704,7 @@ var TOOL_DEFINITIONS = [
2807
3704
  sideEffects: true
2808
3705
  },
2809
3706
  {
2810
- id: "mnemos.manage_collections",
3707
+ id: "fortemi.manage_collections",
2811
3708
  name: "Manage Collections",
2812
3709
  description: "WHEN you need to organize notes into folders. WHAT creates, updates, or manages collections. HOW accepts collection operations (create/list/assign/delete). OUT returns collection data.",
2813
3710
  category: "organize",
@@ -2821,7 +3718,7 @@ var TOOL_DEFINITIONS = [
2821
3718
  sideEffects: true
2822
3719
  },
2823
3720
  {
2824
- id: "mnemos.manage_links",
3721
+ id: "fortemi.manage_links",
2825
3722
  name: "Manage Links",
2826
3723
  description: "WHEN you need to connect related notes. WHAT creates bidirectional links between notes. HOW accepts source/target note IDs and link type. OUT returns the link data.",
2827
3724
  category: "organize",
@@ -2836,7 +3733,7 @@ var TOOL_DEFINITIONS = [
2836
3733
  sideEffects: true
2837
3734
  },
2838
3735
  {
2839
- id: "mnemos.manage_archive",
3736
+ id: "fortemi.manage_archive",
2840
3737
  name: "Manage Archive",
2841
3738
  description: "WHEN you need to switch between or manage knowledge archives. WHAT creates, lists, switches, or deletes archives. HOW accepts archive name and operation. OUT returns archive info.",
2842
3739
  category: "system",
@@ -2848,7 +3745,7 @@ var TOOL_DEFINITIONS = [
2848
3745
  sideEffects: true
2849
3746
  },
2850
3747
  {
2851
- id: "mnemos.manage_capabilities",
3748
+ id: "fortemi.manage_capabilities",
2852
3749
  name: "Manage Capabilities",
2853
3750
  description: "WHEN you need to enable optional features like vector search or LLM. WHAT enables, disables, or queries WASM capability modules. HOW accepts capability name and action. OUT returns capability status.",
2854
3751
  category: "system",
@@ -2894,8 +3791,8 @@ var FortemiToolManifest = class {
2894
3791
  (t) => t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q) || t.tags.some((tag) => tag.includes(q))
2895
3792
  );
2896
3793
  }
2897
- /** Project all tools as PlinyCapability entries for bridge registration. */
2898
- toPlinyCapabilities() {
3794
+ /** Project all tools as BridgeCapability entries for bridge registration. */
3795
+ toBridgeCapabilities() {
2899
3796
  return this.list().map((tool) => ({
2900
3797
  id: tool.id,
2901
3798
  name: tool.name,
@@ -3475,19 +4372,6 @@ function setEmbedFunction(fn) {
3475
4372
  function getEmbedFunction() {
3476
4373
  return embedFn;
3477
4374
  }
3478
- async function ensureEmbeddingSet(db) {
3479
- const result = await db.query(
3480
- `SELECT id FROM embedding_set WHERE model_name = $1`,
3481
- ["all-MiniLM-L6-v2"]
3482
- );
3483
- if (result.rows.length > 0) return result.rows[0].id;
3484
- const id = generateId();
3485
- await db.query(
3486
- `INSERT INTO embedding_set (id, model_name, dimensions) VALUES ($1, $2, $3)`,
3487
- [id, "all-MiniLM-L6-v2", 384]
3488
- );
3489
- return id;
3490
- }
3491
4375
  function averageEmbeddings(embeddings) {
3492
4376
  if (embeddings.length === 1) return embeddings[0];
3493
4377
  const dims = embeddings[0].length;
@@ -3515,27 +4399,14 @@ async function embeddingGenerationHandler(job, db) {
3515
4399
  const chunks = chunkText(content);
3516
4400
  const embeddings = await fn(chunks);
3517
4401
  const vector2 = averageEmbeddings(embeddings);
3518
- const setId = await ensureEmbeddingSet(db);
3519
- await db.query(
3520
- `DELETE FROM embedding_set_member WHERE note_id = $1 AND embedding_set_id = $2`,
3521
- [job.note_id, setId]
3522
- );
3523
- await db.query(
3524
- `DELETE FROM embedding WHERE note_id = $1 AND embedding_set_id = $2`,
3525
- [job.note_id, setId]
3526
- );
3527
- const embId = generateId();
3528
- const vectorStr = `[${vector2.join(",")}]`;
3529
- await db.query(
3530
- `INSERT INTO embedding (id, note_id, embedding_set_id, vector) VALUES ($1, $2, $3, $4::vector)`,
3531
- [embId, job.note_id, setId, vectorStr]
3532
- );
3533
- await db.query(
3534
- `INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id) VALUES ($1, $2, $3)
3535
- ON CONFLICT (embedding_set_id, note_id) DO UPDATE SET embedding_id = $3`,
3536
- [setId, job.note_id, embId]
3537
- );
3538
- return { chunks: chunks.length, embeddings: embeddings.length, setId };
4402
+ const embeddingSets = new EmbeddingSetsRepository(db);
4403
+ const set = await embeddingSets.ensureDefault();
4404
+ await embeddingSets.putEmbedding({
4405
+ note_id: job.note_id,
4406
+ embedding_set_id: set.id,
4407
+ vector: vector2
4408
+ });
4409
+ return { chunks: chunks.length, embeddings: embeddings.length, setId: set.id };
3539
4410
  }
3540
4411
 
3541
4412
  // src/capabilities/auto-tag.ts
@@ -4424,16 +5295,16 @@ function createCspReportHandler(onReport) {
4424
5295
  if (request.method !== "POST") {
4425
5296
  return new Response(null, { status: 405, headers: { Allow: "POST" } });
4426
5297
  }
4427
- let json;
5298
+ let json2;
4428
5299
  try {
4429
- json = await request.json();
5300
+ json2 = await request.json();
4430
5301
  } catch {
4431
5302
  return new Response(JSON.stringify({ error: "Invalid CSP report JSON" }), {
4432
5303
  status: 400,
4433
5304
  headers: { "Content-Type": "application/json" }
4434
5305
  });
4435
5306
  }
4436
- await onReport(parseCspReport(json));
5307
+ await onReport(parseCspReport(json2));
4437
5308
  return new Response(null, { status: 204 });
4438
5309
  };
4439
5310
  }
@@ -4647,17 +5518,39 @@ function tagsFromShard(shardTags) {
4647
5518
  function embeddingSetToShard(set) {
4648
5519
  return {
4649
5520
  id: set.id,
5521
+ name: set.name ?? set.model_name,
5522
+ purpose: set.purpose ?? null,
4650
5523
  model: set.model_name,
4651
5524
  dimension: set.dimensions,
4652
- created_at: toISOString(set.created_at)
5525
+ kind: set.kind ?? "physical",
5526
+ mode: set.mode ?? null,
5527
+ truncate_dimension: set.truncate_dimension ?? null,
5528
+ criteria: jsonObject2(set.criteria_json),
5529
+ source: jsonObject2(set.source_json),
5530
+ compatibility: jsonObject2(set.compatibility_json),
5531
+ materialization: jsonObject2(set.materialization_json),
5532
+ freshness: jsonObject2(set.freshness_json),
5533
+ created_at: toISOString(set.created_at),
5534
+ updated_at: set.updated_at ? toISOString(set.updated_at) : void 0
4653
5535
  };
4654
5536
  }
4655
5537
  function embeddingSetFromShard(shard) {
4656
5538
  return {
4657
5539
  id: shard.id,
5540
+ name: shard.name ?? shard.model,
5541
+ purpose: shard.purpose ?? null,
4658
5542
  model_name: shard.model,
4659
5543
  dimensions: shard.dimension,
4660
- created_at: shard.created_at
5544
+ kind: shard.kind ?? "physical",
5545
+ mode: shard.mode ?? null,
5546
+ truncate_dimension: shard.truncate_dimension ?? null,
5547
+ criteria_json: jsonString(shard.criteria),
5548
+ source_json: jsonString(shard.source),
5549
+ compatibility_json: jsonString(shard.compatibility),
5550
+ materialization_json: jsonString(shard.materialization),
5551
+ freshness_json: jsonString(shard.freshness),
5552
+ created_at: shard.created_at,
5553
+ updated_at: shard.updated_at ?? null
4661
5554
  };
4662
5555
  }
4663
5556
  function embeddingSetMemberToShard(member) {
@@ -4685,6 +5578,55 @@ function embeddingFromShard(shard) {
4685
5578
  created_at: shard.created_at
4686
5579
  };
4687
5580
  }
5581
+ function skosSchemeToShard(scheme) {
5582
+ return {
5583
+ id: scheme.id,
5584
+ title: scheme.title,
5585
+ description: scheme.description,
5586
+ created_at: toISOString(scheme.created_at),
5587
+ updated_at: toISOString(scheme.updated_at)
5588
+ };
5589
+ }
5590
+ function skosConceptToShard(concept) {
5591
+ return {
5592
+ id: concept.id,
5593
+ scheme_id: concept.scheme_id,
5594
+ pref_label: concept.pref_label,
5595
+ alt_labels: parseJsonArrayField(concept.alt_labels),
5596
+ definition: concept.definition,
5597
+ created_at: toISOString(concept.created_at),
5598
+ updated_at: toISOString(concept.updated_at)
5599
+ };
5600
+ }
5601
+ function skosRelationToShard(relation) {
5602
+ return {
5603
+ id: relation.id,
5604
+ source_concept_id: relation.source_concept_id,
5605
+ target_concept_id: relation.target_concept_id,
5606
+ relation_type: relation.relation_type,
5607
+ created_at: toISOString(relation.created_at)
5608
+ };
5609
+ }
5610
+ function noteSkosTagToShard(tag) {
5611
+ return {
5612
+ id: tag.id,
5613
+ note_id: tag.note_id,
5614
+ concept_id: tag.concept_id,
5615
+ created_at: toISOString(tag.created_at)
5616
+ };
5617
+ }
5618
+ function provenanceEdgeToShard(edge) {
5619
+ return {
5620
+ id: edge.id,
5621
+ entity_type: edge.entity_type,
5622
+ entity_id: edge.entity_id,
5623
+ activity: edge.activity,
5624
+ agent: edge.agent,
5625
+ started_at: toISOString(edge.started_at),
5626
+ ended_at: edge.ended_at ? toISOString(edge.ended_at) : null,
5627
+ attributes: parseJsonObjectField(edge.attributes)
5628
+ };
5629
+ }
4688
5630
  function toISOString(date) {
4689
5631
  if (date instanceof Date) return date.toISOString();
4690
5632
  return date;
@@ -4693,9 +5635,37 @@ function parseVector(vectorStr) {
4693
5635
  const inner = vectorStr.replace(/^\[/, "").replace(/\]$/, "");
4694
5636
  return inner.split(",").map(Number);
4695
5637
  }
5638
+ function parseJsonArrayField(value) {
5639
+ if (Array.isArray(value)) return value;
5640
+ if (!value) return [];
5641
+ const parsed = JSON.parse(value);
5642
+ return Array.isArray(parsed) ? parsed.map(String) : [];
5643
+ }
5644
+ function parseJsonObjectField(value) {
5645
+ if (!value) return null;
5646
+ if (typeof value !== "string") return value;
5647
+ const parsed = JSON.parse(value);
5648
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
5649
+ }
5650
+ function jsonObject2(value) {
5651
+ if (value == null) return null;
5652
+ if (typeof value === "string") return JSON.parse(value);
5653
+ return value;
5654
+ }
5655
+ function jsonString(value) {
5656
+ return value == null ? null : JSON.stringify(value);
5657
+ }
4696
5658
 
4697
5659
  // src/shard/shard-export.ts
4698
5660
  var encoder = new TextEncoder();
5661
+ function jsonObject3(value) {
5662
+ if (value == null) return void 0;
5663
+ if (typeof value === "string") return JSON.parse(value);
5664
+ return value;
5665
+ }
5666
+ function iso2(value) {
5667
+ return value instanceof Date ? value.toISOString() : value;
5668
+ }
4699
5669
  async function exportShard(db, options) {
4700
5670
  const files = /* @__PURE__ */ new Map();
4701
5671
  const components = [];
@@ -4797,6 +5767,41 @@ async function exportShard(db, options) {
4797
5767
  files.set("links.jsonl", encoder.encode(linksJsonl));
4798
5768
  components.push("links");
4799
5769
  counts.links = filteredLinks.length;
5770
+ const allNoteSkosRows = await db.query(`SELECT * FROM note_skos_tag ORDER BY created_at`);
5771
+ const filteredNoteSkosRows = isFiltered ? allNoteSkosRows.rows.filter((row) => exportedNoteIds.has(row.note_id)) : allNoteSkosRows.rows;
5772
+ const referencedConceptIds = new Set(filteredNoteSkosRows.map((row) => row.concept_id));
5773
+ const allConceptRows = await db.query(`SELECT * FROM skos_concept WHERE deleted_at IS NULL ORDER BY pref_label`);
5774
+ const filteredConceptRows = isFiltered ? allConceptRows.rows.filter((row) => referencedConceptIds.has(row.id)) : allConceptRows.rows;
5775
+ const exportedConceptIds = new Set(filteredConceptRows.map((row) => row.id));
5776
+ const exportedSchemeIds = new Set(filteredConceptRows.map((row) => row.scheme_id));
5777
+ const allSchemeRows = await db.query(`SELECT * FROM skos_scheme WHERE deleted_at IS NULL ORDER BY title`);
5778
+ const filteredSchemeRows = isFiltered ? allSchemeRows.rows.filter((row) => exportedSchemeIds.has(row.id)) : allSchemeRows.rows;
5779
+ const allRelationRows = await db.query(`SELECT * FROM skos_concept_relation ORDER BY created_at`);
5780
+ const filteredRelationRows = isFiltered ? allRelationRows.rows.filter(
5781
+ (row) => exportedConceptIds.has(row.source_concept_id) && exportedConceptIds.has(row.target_concept_id)
5782
+ ) : allRelationRows.rows;
5783
+ const shardSkosSchemes = filteredSchemeRows.map(skosSchemeToShard);
5784
+ files.set("skos_schemes.json", encoder.encode(JSON.stringify(shardSkosSchemes)));
5785
+ components.push("skos_schemes");
5786
+ counts.skos_schemes = shardSkosSchemes.length;
5787
+ const shardSkosConcepts = filteredConceptRows.map(skosConceptToShard);
5788
+ files.set("skos_concepts.json", encoder.encode(JSON.stringify(shardSkosConcepts)));
5789
+ components.push("skos_concepts");
5790
+ counts.skos_concepts = shardSkosConcepts.length;
5791
+ const skosRelationsJsonl = filteredRelationRows.map((row) => JSON.stringify(skosRelationToShard(row))).join("\n");
5792
+ files.set("skos_relations.jsonl", encoder.encode(skosRelationsJsonl));
5793
+ components.push("skos_relations");
5794
+ counts.skos_relations = filteredRelationRows.length;
5795
+ const noteSkosJsonl = filteredNoteSkosRows.map((row) => JSON.stringify(noteSkosTagToShard(row))).join("\n");
5796
+ files.set("note_skos_tags.jsonl", encoder.encode(noteSkosJsonl));
5797
+ components.push("note_skos_tags");
5798
+ counts.note_skos_tags = filteredNoteSkosRows.length;
5799
+ const provenanceRows = await db.query(`SELECT * FROM provenance_edge ORDER BY started_at`);
5800
+ const filteredProvenanceRows = isFiltered ? provenanceRows.rows.filter((row) => row.entity_type !== "note" || exportedNoteIds.has(row.entity_id)) : provenanceRows.rows;
5801
+ const provenanceJsonl = filteredProvenanceRows.map((row) => JSON.stringify(provenanceEdgeToShard(row))).join("\n");
5802
+ files.set("provenance_edges.jsonl", encoder.encode(provenanceJsonl));
5803
+ components.push("provenance_edges");
5804
+ counts.provenance_edges = filteredProvenanceRows.length;
4800
5805
  if (options?.includeEmbeddings) {
4801
5806
  const embSetRows = await db.query(`SELECT * FROM embedding_set ORDER BY created_at`);
4802
5807
  const shardEmbSets = embSetRows.rows.map(embeddingSetToShard);
@@ -4814,6 +5819,92 @@ async function exportShard(db, options) {
4814
5819
  components.push("embeddings");
4815
5820
  counts.embeddings = embRows.rows.length;
4816
5821
  }
5822
+ const graphSourceRows = await db.query(`SELECT * FROM graph_source ORDER BY created_at, id`);
5823
+ if (graphSourceRows.rows.length > 0) {
5824
+ const shardGraphSources = graphSourceRows.rows.map((row) => ({
5825
+ id: row.id,
5826
+ name: row.name,
5827
+ kind: row.kind,
5828
+ source_table: row.source_table,
5829
+ embedding_set_id: row.embedding_set_id,
5830
+ virtual_set_id: row.virtual_set_id,
5831
+ model: row.model,
5832
+ dimension: row.dimension,
5833
+ truncate_dimension: row.truncate_dimension,
5834
+ metric: row.metric,
5835
+ algorithm: row.algorithm,
5836
+ parameters: jsonObject3(row.parameters_json),
5837
+ input_hash: row.input_hash,
5838
+ freshness: jsonObject3(row.freshness_json) ?? { status: "unknown" },
5839
+ created_at: iso2(row.created_at)
5840
+ }));
5841
+ files.set("graph_sources.json", encoder.encode(JSON.stringify(shardGraphSources)));
5842
+ components.push("graph_sources");
5843
+ counts.graph_sources = shardGraphSources.length;
5844
+ }
5845
+ const graphEdgeRows = await db.query(`SELECT * FROM graph_edge_artifact ORDER BY graph_source_id, from_note_id, to_note_id, kind`);
5846
+ if (graphEdgeRows.rows.length > 0) {
5847
+ const graphEdgesJsonl = graphEdgeRows.rows.map((row) => JSON.stringify({
5848
+ graph_source_id: row.graph_source_id,
5849
+ from_note_id: row.from_note_id,
5850
+ to_note_id: row.to_note_id,
5851
+ weight: row.weight,
5852
+ kind: row.kind,
5853
+ rank: row.rank,
5854
+ metadata: jsonObject3(row.metadata_json)
5855
+ })).join("\n");
5856
+ files.set("graph_edges.jsonl", encoder.encode(graphEdgesJsonl));
5857
+ components.push("graph_edges");
5858
+ counts.graph_edges = graphEdgeRows.rows.length;
5859
+ }
5860
+ const communitySetRows = await db.query(`SELECT * FROM community_set ORDER BY created_at, id`);
5861
+ const communityRows = await db.query(`SELECT * FROM community ORDER BY community_set_id, rank NULLS LAST, id`);
5862
+ if (communitySetRows.rows.length > 0) {
5863
+ const communitiesBySet = /* @__PURE__ */ new Map();
5864
+ for (const row of communityRows.rows) {
5865
+ const rows = communitiesBySet.get(row.community_set_id) ?? [];
5866
+ rows.push(row);
5867
+ communitiesBySet.set(row.community_set_id, rows);
5868
+ }
5869
+ const shardCommunitySets = communitySetRows.rows.map((row) => ({
5870
+ id: row.id,
5871
+ graph_source_id: row.graph_source_id,
5872
+ name: row.name,
5873
+ source_type: row.source_type,
5874
+ algorithm: row.algorithm,
5875
+ parameters: jsonObject3(row.parameters_json),
5876
+ input_hash: row.input_hash,
5877
+ freshness: jsonObject3(row.freshness_json) ?? { status: "unknown" },
5878
+ communities: (communitiesBySet.get(row.id) ?? []).map((community) => ({
5879
+ id: community.id,
5880
+ label: community.label,
5881
+ rank: community.rank,
5882
+ size: community.size,
5883
+ confidence: community.confidence,
5884
+ representative_note_ids: community.representative_note_ids ?? [],
5885
+ metadata: jsonObject3(community.metadata_json)
5886
+ })),
5887
+ created_at: iso2(row.created_at)
5888
+ }));
5889
+ files.set("communities.json", encoder.encode(JSON.stringify(shardCommunitySets)));
5890
+ components.push("communities");
5891
+ counts.community_sets = shardCommunitySets.length;
5892
+ counts.communities = communityRows.rows.length;
5893
+ }
5894
+ const assignmentRows = await db.query(`SELECT * FROM community_assignment ORDER BY community_set_id, community_id, note_id`);
5895
+ if (assignmentRows.rows.length > 0) {
5896
+ const assignmentsJsonl = assignmentRows.rows.map((row) => JSON.stringify({
5897
+ community_set_id: row.community_set_id,
5898
+ community_id: row.community_id,
5899
+ note_id: row.note_id,
5900
+ confidence: row.confidence,
5901
+ source_type: row.source_type,
5902
+ metadata: jsonObject3(row.metadata_json)
5903
+ })).join("\n");
5904
+ files.set("community_assignments.jsonl", encoder.encode(assignmentsJsonl));
5905
+ components.push("community_assignments");
5906
+ counts.community_assignments = assignmentRows.rows.length;
5907
+ }
4817
5908
  const checksums = {};
4818
5909
  for (const [filename, data] of files) {
4819
5910
  checksums[filename] = await sha256Hex(data);
@@ -4846,7 +5937,17 @@ async function importShard(db, data, options) {
4846
5937
  links: 0,
4847
5938
  embedding_sets: 0,
4848
5939
  embedding_set_members: 0,
4849
- embeddings: 0
5940
+ embeddings: 0,
5941
+ skos_schemes: 0,
5942
+ skos_concepts: 0,
5943
+ skos_relations: 0,
5944
+ note_skos_tags: 0,
5945
+ provenance_edges: 0,
5946
+ graph_sources: 0,
5947
+ graph_edges: 0,
5948
+ community_sets: 0,
5949
+ communities: 0,
5950
+ community_assignments: 0
4850
5951
  };
4851
5952
  const skipped = {};
4852
5953
  const inputData = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
@@ -4919,6 +6020,15 @@ async function importShard(db, data, options) {
4919
6020
  files.get("embedding_set_members.jsonl")
4920
6021
  );
4921
6022
  const parsedEmbeddings = parseJsonl(files.get("embeddings.jsonl"));
6023
+ const parsedSkosSchemes = parseJsonArray(files.get("skos_schemes.json"));
6024
+ const parsedSkosConcepts = parseJsonArray(files.get("skos_concepts.json"));
6025
+ const parsedSkosRelations = parseJsonl(files.get("skos_relations.jsonl"));
6026
+ const parsedNoteSkosTags = parseJsonl(files.get("note_skos_tags.jsonl"));
6027
+ const parsedProvenanceEdges = parseJsonl(files.get("provenance_edges.jsonl"));
6028
+ const parsedGraphSources = parseJsonArray(files.get("graph_sources.json"));
6029
+ const parsedGraphEdges = parseJsonl(files.get("graph_edges.jsonl"));
6030
+ const parsedCommunitySets = parseJsonArray(files.get("communities.json"));
6031
+ const parsedCommunityAssignments = parseJsonl(files.get("community_assignments.jsonl"));
4922
6032
  const knownFiles = /* @__PURE__ */ new Set([
4923
6033
  "manifest.json",
4924
6034
  "notes.jsonl",
@@ -4929,7 +6039,16 @@ async function importShard(db, data, options) {
4929
6039
  "embedding_set_members.jsonl",
4930
6040
  "embedding_configs.json",
4931
6041
  "embeddings.jsonl",
4932
- "templates.json"
6042
+ "templates.json",
6043
+ "skos_schemes.json",
6044
+ "skos_concepts.json",
6045
+ "skos_relations.jsonl",
6046
+ "note_skos_tags.jsonl",
6047
+ "provenance_edges.jsonl",
6048
+ "graph_sources.json",
6049
+ "graph_edges.jsonl",
6050
+ "communities.json",
6051
+ "community_assignments.jsonl"
4933
6052
  ]);
4934
6053
  for (const filename of files.keys()) {
4935
6054
  if (!knownFiles.has(filename)) {
@@ -5038,6 +6157,41 @@ async function importShard(db, data, options) {
5038
6157
  }
5039
6158
  counts.notes++;
5040
6159
  }
6160
+ for (const scheme of parsedSkosSchemes) {
6161
+ if (strategy === "replace") {
6162
+ await tx.query(
6163
+ `INSERT INTO skos_scheme (id, title, description, created_at, updated_at)
6164
+ VALUES ($1, $2, $3, $4, $5)
6165
+ ON CONFLICT (id) DO UPDATE SET title = $2, description = $3, updated_at = $5`,
6166
+ [scheme.id, scheme.title, scheme.description, scheme.created_at, scheme.updated_at]
6167
+ );
6168
+ } else {
6169
+ await tx.query(
6170
+ `INSERT INTO skos_scheme (id, title, description, created_at, updated_at)
6171
+ VALUES ($1, $2, $3, $4, $5) ${conflictClause}`,
6172
+ [scheme.id, scheme.title, scheme.description, scheme.created_at, scheme.updated_at]
6173
+ );
6174
+ }
6175
+ counts.skos_schemes++;
6176
+ }
6177
+ for (const concept of parsedSkosConcepts) {
6178
+ const altLabels = JSON.stringify(concept.alt_labels ?? []);
6179
+ if (strategy === "replace") {
6180
+ await tx.query(
6181
+ `INSERT INTO skos_concept (id, scheme_id, pref_label, alt_labels, definition, created_at, updated_at)
6182
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
6183
+ ON CONFLICT (id) DO UPDATE SET scheme_id = $2, pref_label = $3, alt_labels = $4, definition = $5, updated_at = $7`,
6184
+ [concept.id, concept.scheme_id, concept.pref_label, altLabels, concept.definition, concept.created_at, concept.updated_at]
6185
+ );
6186
+ } else {
6187
+ await tx.query(
6188
+ `INSERT INTO skos_concept (id, scheme_id, pref_label, alt_labels, definition, created_at, updated_at)
6189
+ VALUES ($1, $2, $3, $4, $5, $6, $7) ${conflictClause}`,
6190
+ [concept.id, concept.scheme_id, concept.pref_label, altLabels, concept.definition, concept.created_at, concept.updated_at]
6191
+ );
6192
+ }
6193
+ counts.skos_concepts++;
6194
+ }
5041
6195
  for (const shardLink of parsedLinks) {
5042
6196
  const link = linkFromShard(shardLink);
5043
6197
  if (strategy === "replace") {
@@ -5056,20 +6210,70 @@ async function importShard(db, data, options) {
5056
6210
  }
5057
6211
  counts.links++;
5058
6212
  }
6213
+ for (const relation of parsedSkosRelations) {
6214
+ if (strategy === "replace") {
6215
+ await tx.query(
6216
+ `INSERT INTO skos_concept_relation (id, source_concept_id, target_concept_id, relation_type, created_at)
6217
+ VALUES ($1, $2, $3, $4, $5)
6218
+ ON CONFLICT (id) DO UPDATE SET source_concept_id = $2, target_concept_id = $3, relation_type = $4`,
6219
+ [relation.id, relation.source_concept_id, relation.target_concept_id, relation.relation_type, relation.created_at]
6220
+ );
6221
+ } else {
6222
+ await tx.query(
6223
+ `INSERT INTO skos_concept_relation (id, source_concept_id, target_concept_id, relation_type, created_at)
6224
+ VALUES ($1, $2, $3, $4, $5) ${conflictClause}`,
6225
+ [relation.id, relation.source_concept_id, relation.target_concept_id, relation.relation_type, relation.created_at]
6226
+ );
6227
+ }
6228
+ counts.skos_relations++;
6229
+ }
6230
+ for (const tag of parsedNoteSkosTags) {
6231
+ await tx.query(
6232
+ `INSERT INTO note_skos_tag (id, note_id, concept_id, created_at)
6233
+ VALUES ($1, $2, $3, $4)
6234
+ ON CONFLICT (note_id, concept_id) DO NOTHING`,
6235
+ [tag.id, tag.note_id, tag.concept_id, tag.created_at]
6236
+ );
6237
+ counts.note_skos_tags++;
6238
+ }
6239
+ for (const edge of parsedProvenanceEdges) {
6240
+ const attributes = edge.attributes === null ? null : JSON.stringify(edge.attributes);
6241
+ if (strategy === "replace") {
6242
+ await tx.query(
6243
+ `INSERT INTO provenance_edge (id, entity_type, entity_id, activity, agent, started_at, ended_at, attributes)
6244
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
6245
+ ON CONFLICT (id) DO UPDATE SET entity_type = $2, entity_id = $3, activity = $4, agent = $5, started_at = $6, ended_at = $7, attributes = $8`,
6246
+ [edge.id, edge.entity_type, edge.entity_id, edge.activity, edge.agent, edge.started_at, edge.ended_at, attributes]
6247
+ );
6248
+ } else {
6249
+ await tx.query(
6250
+ `INSERT INTO provenance_edge (id, entity_type, entity_id, activity, agent, started_at, ended_at, attributes)
6251
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ${conflictClause}`,
6252
+ [edge.id, edge.entity_type, edge.entity_id, edge.activity, edge.agent, edge.started_at, edge.ended_at, attributes]
6253
+ );
6254
+ }
6255
+ counts.provenance_edges++;
6256
+ }
5059
6257
  for (const shardSet of parsedEmbSets) {
5060
6258
  const set = embeddingSetFromShard(shardSet);
5061
6259
  if (strategy === "replace") {
5062
6260
  await tx.query(
5063
- `INSERT INTO embedding_set (id, model_name, dimensions, created_at)
5064
- VALUES ($1, $2, $3, $4)
5065
- ON CONFLICT (id) DO UPDATE SET model_name = $2, dimensions = $3`,
5066
- [set.id, set.model_name, set.dimensions, set.created_at]
6261
+ `INSERT INTO embedding_set (
6262
+ id, name, purpose, model_name, dimensions, kind, mode, truncate_dimension,
6263
+ criteria_json, source_json, compatibility_json, materialization_json, freshness_json, created_at, updated_at
6264
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13::jsonb, $14, COALESCE($15::timestamptz, $14::timestamptz))
6265
+ ON CONFLICT (id) DO UPDATE SET name = $2, purpose = $3, model_name = $4, dimensions = $5,
6266
+ kind = $6, mode = $7, truncate_dimension = $8, criteria_json = $9::jsonb, source_json = $10::jsonb,
6267
+ compatibility_json = $11::jsonb, materialization_json = $12::jsonb, freshness_json = $13::jsonb, updated_at = COALESCE($15::timestamptz, $14::timestamptz)`,
6268
+ [set.id, set.name, set.purpose, set.model_name, set.dimensions, set.kind, set.mode, set.truncate_dimension, set.criteria_json, set.source_json, set.compatibility_json, set.materialization_json, set.freshness_json, set.created_at, set.updated_at]
5067
6269
  );
5068
6270
  } else {
5069
6271
  await tx.query(
5070
- `INSERT INTO embedding_set (id, model_name, dimensions, created_at)
5071
- VALUES ($1, $2, $3, $4) ${conflictClause}`,
5072
- [set.id, set.model_name, set.dimensions, set.created_at]
6272
+ `INSERT INTO embedding_set (
6273
+ id, name, purpose, model_name, dimensions, kind, mode, truncate_dimension,
6274
+ criteria_json, source_json, compatibility_json, materialization_json, freshness_json, created_at, updated_at
6275
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13::jsonb, $14, COALESCE($15::timestamptz, $14::timestamptz)) ${conflictClause}`,
6276
+ [set.id, set.name, set.purpose, set.model_name, set.dimensions, set.kind, set.mode, set.truncate_dimension, set.criteria_json, set.source_json, set.compatibility_json, set.materialization_json, set.freshness_json, set.created_at, set.updated_at]
5073
6277
  );
5074
6278
  }
5075
6279
  counts.embedding_sets++;
@@ -5092,6 +6296,60 @@ async function importShard(db, data, options) {
5092
6296
  }
5093
6297
  counts.embeddings++;
5094
6298
  }
6299
+ for (const source of parsedGraphSources) {
6300
+ const parameters = source.parameters == null ? null : JSON.stringify(source.parameters);
6301
+ const freshness = JSON.stringify({ ...source.freshness ?? {}, status: "unknown" });
6302
+ await tx.query(
6303
+ `INSERT INTO graph_source (
6304
+ id, name, kind, source_table, embedding_set_id, virtual_set_id, model, dimension,
6305
+ truncate_dimension, metric, algorithm, parameters_json, input_hash, freshness_json, created_at
6306
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12::jsonb, $13, $14::jsonb, $15)
6307
+ ${strategy === "replace" ? "ON CONFLICT (id) DO UPDATE SET name = $2, kind = $3, source_table = $4, embedding_set_id = $5, virtual_set_id = $6, model = $7, dimension = $8, truncate_dimension = $9, metric = $10, algorithm = $11, parameters_json = $12::jsonb, input_hash = $13, freshness_json = $14::jsonb, created_at = $15" : conflictClause}`,
6308
+ [source.id, source.name, source.kind, source.source_table ?? null, source.embedding_set_id ?? null, source.virtual_set_id ?? null, source.model ?? null, source.dimension ?? null, source.truncate_dimension ?? null, source.metric ?? null, source.algorithm ?? null, parameters, source.input_hash, freshness, source.created_at]
6309
+ );
6310
+ counts.graph_sources++;
6311
+ }
6312
+ for (const edge of parsedGraphEdges) {
6313
+ const metadata = edge.metadata == null ? null : JSON.stringify(edge.metadata);
6314
+ await tx.query(
6315
+ `INSERT INTO graph_edge_artifact (graph_source_id, from_note_id, to_note_id, weight, kind, rank, metadata_json)
6316
+ VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
6317
+ ${strategy === "replace" ? "ON CONFLICT (graph_source_id, from_note_id, to_note_id, kind) DO UPDATE SET weight = $4, rank = $6, metadata_json = $7::jsonb" : conflictClause}`,
6318
+ [edge.graph_source_id, edge.from_note_id, edge.to_note_id, edge.weight, edge.kind, edge.rank ?? null, metadata]
6319
+ );
6320
+ counts.graph_edges++;
6321
+ }
6322
+ for (const set of parsedCommunitySets) {
6323
+ const parameters = set.parameters == null ? null : JSON.stringify(set.parameters);
6324
+ const freshness = JSON.stringify({ ...set.freshness ?? {}, status: "unknown" });
6325
+ await tx.query(
6326
+ `INSERT INTO community_set (id, graph_source_id, name, source_type, algorithm, parameters_json, input_hash, freshness_json, created_at)
6327
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7, $8::jsonb, $9)
6328
+ ${strategy === "replace" ? "ON CONFLICT (id) DO UPDATE SET graph_source_id = $2, name = $3, source_type = $4, algorithm = $5, parameters_json = $6::jsonb, input_hash = $7, freshness_json = $8::jsonb, created_at = $9" : conflictClause}`,
6329
+ [set.id, set.graph_source_id, set.name, set.source_type, set.algorithm ?? null, parameters, set.input_hash, freshness, set.created_at]
6330
+ );
6331
+ counts.community_sets++;
6332
+ for (const community of set.communities ?? []) {
6333
+ const metadata = community.metadata == null ? null : JSON.stringify(community.metadata);
6334
+ await tx.query(
6335
+ `INSERT INTO community (community_set_id, id, label, rank, size, confidence, representative_note_ids, metadata_json)
6336
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
6337
+ ${strategy === "replace" ? "ON CONFLICT (community_set_id, id) DO UPDATE SET label = $3, rank = $4, size = $5, confidence = $6, representative_note_ids = $7, metadata_json = $8::jsonb" : conflictClause}`,
6338
+ [set.id, community.id, community.label ?? null, community.rank ?? null, community.size ?? null, community.confidence ?? null, community.representative_note_ids ?? [], metadata]
6339
+ );
6340
+ counts.communities++;
6341
+ }
6342
+ }
6343
+ for (const assignment of parsedCommunityAssignments) {
6344
+ const metadata = assignment.metadata == null ? null : JSON.stringify(assignment.metadata);
6345
+ await tx.query(
6346
+ `INSERT INTO community_assignment (community_set_id, community_id, note_id, confidence, source_type, metadata_json)
6347
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb)
6348
+ ${strategy === "replace" ? "ON CONFLICT (community_set_id, note_id) DO UPDATE SET community_id = $2, confidence = $4, source_type = $5, metadata_json = $6::jsonb" : conflictClause}`,
6349
+ [assignment.community_set_id, assignment.community_id, assignment.note_id, assignment.confidence ?? null, assignment.source_type, metadata]
6350
+ );
6351
+ counts.community_assignments++;
6352
+ }
5095
6353
  for (const member of parsedEmbMembers) {
5096
6354
  await tx.query(
5097
6355
  `INSERT INTO embedding_set_member (embedding_set_id, note_id, embedding_id)
@@ -5131,8 +6389,8 @@ function parseJsonArray(data) {
5131
6389
  }
5132
6390
 
5133
6391
  // src/index.ts
5134
- var VERSION = "2026.5.2";
6392
+ var VERSION = "2026.5.4";
5135
6393
 
5136
- export { ArchiveManager, AttachmentsRepository, CURRENT_SHARD_VERSION, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, FallbackRouter, FortemiToolManifest, GetNoteInputSchema, JOB_CAPABILITIES, JOB_PRIORITIES, JobQueueWorker, LOCAL_ENDPOINTS, LinksRepository, ListNotesInputSchema, ManageArchiveInputSchema, ManageAttachmentsInputSchema, ManageCapabilitiesInputSchema, ManageCollectionsInputSchema, ManageLinksInputSchema, ManageNoteInputSchema, ManageTagsInputSchema, MemoryBlobStore, MigrationRunner, NotesRepository, OpenAICompatibleProvider, PGliteStorageBackend, PGliteStorageBackendFactory, PGliteWorkerClient, ProviderRegistry, SHARD_FORMAT, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, allMigrations, appendPluginScript, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, collectionFromShard, collectionToShard, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createBlobStore, createCspReportHandler, createFortemi, createLegacyProvider, createPGliteInstance, createRoutes, defaultStorageBackendFactory, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, fetchPluginScript, fortemiManifest, generateId, getEmbedFunction, getJobQueueStatus, getLlmFunction, getNote, importShard, isPluginScriptAllowed, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, noteFromShard, noteToShard, packTarGz, parseCspReport, registerLlmCapability, registerSemanticCapability, registerServiceWorker, searchTool, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, suggestTags, tagsFromShard, tagsToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, validateChecksums, verifySri };
6394
+ export { ArchiveManager, AttachmentsRepository, CURRENT_SHARD_VERSION, CapabilityManager, CaptureKnowledgeInputSchema, CollectionsRepository, CommunitiesRepository, EmbeddingSetsRepository, 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, ProviderRegistry, SHARD_FORMAT, SearchInputSchema, SearchRepository, SkosRepository, TagsRepository, TransactionProxy, TypedEventBus, VERSION, aiRevisionHandler, allMigrations, appendPluginScript, buildNoteConditions, buildPluginCsp, captureKnowledge, chunkText, classifyError, classifyModel, collectionFromShard, collectionToShard, computeHash, computeSri, conceptTaggingHandler, cosineSimilarity, createBlobStore, createCspReportHandler, createFortemi, createLegacyProvider, createPGliteInstance, createRoutes, defaultStorageBackendFactory, detectCommunities, detectGpuCapabilities, detectInferenceCapabilities, discoverLocalProviders, embeddingFromShard, embeddingGenerationHandler, embeddingSetFromShard, embeddingSetMemberToShard, embeddingSetToShard, embeddingToShard, enqueueFullWorkflow, enqueueJob, enqueueNoteCreationJobs, estimateModelFit, estimateVramMB, estimateVramTier, exportShard, fetchPluginScript, fortemiManifest, generateId, getEmbedFunction, getJobQueueStatus, getLlmFunction, getNote, importShard, isPluginScriptAllowed, linkFromShard, linkToShard, linkingHandler, listNotes, manageArchive, manageAttachments, manageCapabilities, manageCollections, manageLinks, manageNote, manageTags, matchRoute, noteFromShard, noteSkosTagToShard, noteToShard, packTarGz, parseCspReport, provenanceEdgeToShard, registerLlmCapability, registerSemanticCapability, registerServiceWorker, searchTool, selectLlmModel, setEmbedFunction, setLlmFunction, sha256Hex, skosConceptToShard, skosRelationToShard, skosSchemeToShard, suggestTags, tagsFromShard, tagsToShard, titleGenerationHandler, unpackTarGz, unregisterLlmCapability, unregisterSemanticCapability, validateChecksums, verifySri };
5137
6395
  //# sourceMappingURL=index.js.map
5138
6396
  //# sourceMappingURL=index.js.map