@tekmidian/pai 0.25.2 → 0.26.0
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/auto-route-BWGvvpcP.mjs +86 -0
- package/dist/auto-route-BWGvvpcP.mjs.map +1 -0
- package/dist/cli/index.mjs +2 -2
- package/dist/cli/program.mjs +2 -2
- package/dist/clusters-BdvGIoD-.mjs +201 -0
- package/dist/clusters-BdvGIoD-.mjs.map +1 -0
- package/dist/daemon/index.mjs +6 -5
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/daemon-BaBTOB1P.mjs +1356 -0
- package/dist/daemon-BaBTOB1P.mjs.map +1 -0
- package/dist/detector-DExEQ5cW.mjs +74 -0
- package/dist/detector-DExEQ5cW.mjs.map +1 -0
- package/dist/factory-CAy0N1xR.mjs +142 -0
- package/dist/factory-CAy0N1xR.mjs.map +1 -0
- package/dist/indexer-backend-vOJqSE0U.mjs +299 -0
- package/dist/indexer-backend-vOJqSE0U.mjs.map +1 -0
- package/dist/latent-ideas-DuM_kgkW.mjs +191 -0
- package/dist/latent-ideas-DuM_kgkW.mjs.map +1 -0
- package/dist/neighborhood-DSkvFMAv.mjs +135 -0
- package/dist/neighborhood-DSkvFMAv.mjs.map +1 -0
- package/dist/note-context-DrcY4cWm.mjs +126 -0
- package/dist/note-context-DrcY4cWm.mjs.map +1 -0
- package/dist/pick-FrfZ_iY7.mjs +13299 -0
- package/dist/pick-FrfZ_iY7.mjs.map +1 -0
- package/dist/postgres-DTyxU4B1.mjs +891 -0
- package/dist/postgres-DTyxU4B1.mjs.map +1 -0
- package/dist/query-feedback-BX5nSyRm.mjs +76 -0
- package/dist/query-feedback-BX5nSyRm.mjs.map +1 -0
- package/dist/router-B2xR3gVP.mjs +228 -0
- package/dist/router-B2xR3gVP.mjs.map +1 -0
- package/dist/sqlite--BBAyXLH.mjs +271 -0
- package/dist/sqlite--BBAyXLH.mjs.map +1 -0
- package/dist/themes-DICajLf-.mjs +148 -0
- package/dist/themes-DICajLf-.mjs.map +1 -0
- package/dist/tools-DMAQxlOk.mjs +1939 -0
- package/dist/tools-DMAQxlOk.mjs.map +1 -0
- package/dist/trace-DfyGmMG_.mjs +137 -0
- package/dist/trace-DfyGmMG_.mjs.map +1 -0
- package/dist/vault-indexer-Dt8qXP-w.mjs +536 -0
- package/dist/vault-indexer-Dt8qXP-w.mjs.map +1 -0
- package/dist/work-queue-worker-Bt_UcbMy.mjs +1856 -0
- package/dist/work-queue-worker-Bt_UcbMy.mjs.map +1 -0
- package/dist/zettelkasten-C8BikWss.mjs +1063 -0
- package/dist/zettelkasten-C8BikWss.mjs.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { r as deserializeEmbedding } from "./embeddings-Bn86ssxR.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/graph/neighborhood.ts
|
|
4
|
+
function folderFromPath(vaultPath) {
|
|
5
|
+
const lastSlash = vaultPath.lastIndexOf("/");
|
|
6
|
+
return lastSlash === -1 ? "" : vaultPath.slice(0, lastSlash);
|
|
7
|
+
}
|
|
8
|
+
function cosineSimilarity(a, b) {
|
|
9
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
10
|
+
let dot = 0;
|
|
11
|
+
let normA = 0;
|
|
12
|
+
let normB = 0;
|
|
13
|
+
for (let i = 0; i < a.length; i++) {
|
|
14
|
+
dot += a[i] * b[i];
|
|
15
|
+
normA += a[i] * a[i];
|
|
16
|
+
normB += b[i] * b[i];
|
|
17
|
+
}
|
|
18
|
+
if (normA === 0 || normB === 0) return 0;
|
|
19
|
+
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
20
|
+
}
|
|
21
|
+
function dominantType(counts) {
|
|
22
|
+
let dominant = "unknown";
|
|
23
|
+
let maxCount = 0;
|
|
24
|
+
for (const [type, n] of Object.entries(counts)) if (n > maxCount) {
|
|
25
|
+
maxCount = n;
|
|
26
|
+
dominant = type;
|
|
27
|
+
}
|
|
28
|
+
return dominant;
|
|
29
|
+
}
|
|
30
|
+
async function fetchObservationTypes(pool, filePaths, projectId) {
|
|
31
|
+
if (filePaths.length === 0) return /* @__PURE__ */ new Map();
|
|
32
|
+
try {
|
|
33
|
+
const params = [filePaths, projectId];
|
|
34
|
+
const result = await pool.query(`SELECT unnested_path AS path, type, COUNT(*) AS cnt
|
|
35
|
+
FROM pai_observations,
|
|
36
|
+
LATERAL unnest(files_modified || files_read) AS unnested_path
|
|
37
|
+
WHERE unnested_path = ANY($1::text[])
|
|
38
|
+
AND project_id = $2
|
|
39
|
+
GROUP BY unnested_path, type`, params);
|
|
40
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const row of result.rows) {
|
|
42
|
+
const existing = byPath.get(row.path) ?? {};
|
|
43
|
+
existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);
|
|
44
|
+
byPath.set(row.path, existing);
|
|
45
|
+
}
|
|
46
|
+
return byPath;
|
|
47
|
+
} catch {
|
|
48
|
+
return /* @__PURE__ */ new Map();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function handleGraphNeighborhood(pool, backend, params) {
|
|
52
|
+
const vaultPaths = params.vault_paths ?? [];
|
|
53
|
+
if (vaultPaths.length === 0) return {
|
|
54
|
+
nodes: [],
|
|
55
|
+
edges: []
|
|
56
|
+
};
|
|
57
|
+
const includeSemanticEdges = params.include_semantic_edges ?? false;
|
|
58
|
+
const semanticThreshold = params.semantic_threshold ?? .7;
|
|
59
|
+
const fileRows = await backend.getVaultFilesByPaths(vaultPaths);
|
|
60
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
61
|
+
for (const row of fileRows) fileIndex.set(row.vaultPath, row);
|
|
62
|
+
const observationsByPath = pool !== null ? await fetchObservationTypes(pool, vaultPaths, params.project_id) : /* @__PURE__ */ new Map();
|
|
63
|
+
const nodes = vaultPaths.map((vp) => {
|
|
64
|
+
const fileRow = fileIndex.get(vp);
|
|
65
|
+
const fileName = vp.split("/").pop() ?? vp;
|
|
66
|
+
const rawTitle = fileRow?.title ?? fileName.replace(/\.md$/i, "");
|
|
67
|
+
const obsCounts = observationsByPath.get(vp) ?? {};
|
|
68
|
+
return {
|
|
69
|
+
vault_path: vp,
|
|
70
|
+
title: rawTitle,
|
|
71
|
+
folder: folderFromPath(vp),
|
|
72
|
+
observation_types: obsCounts,
|
|
73
|
+
dominant_type: dominantType(obsCounts),
|
|
74
|
+
updated_at: fileRow?.indexedAt ?? 0,
|
|
75
|
+
word_count: 0
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
const pathSet = new Set(vaultPaths);
|
|
79
|
+
const linkRows = await backend.getVaultLinksFromPaths(vaultPaths);
|
|
80
|
+
const edges = [];
|
|
81
|
+
for (const row of linkRows) {
|
|
82
|
+
if (!row.targetPath || !pathSet.has(row.targetPath)) continue;
|
|
83
|
+
edges.push({
|
|
84
|
+
source: row.sourcePath,
|
|
85
|
+
target: row.targetPath,
|
|
86
|
+
type: "wikilink",
|
|
87
|
+
weight: 1
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (includeSemanticEdges && vaultPaths.length > 1) {
|
|
91
|
+
const embeddings = /* @__PURE__ */ new Map();
|
|
92
|
+
for (const vp of vaultPaths) {
|
|
93
|
+
const embRows = (await backend.getChunksForPath(params.project_id, vp)).filter((r) => r.embedding !== null);
|
|
94
|
+
if (embRows.length === 0) continue;
|
|
95
|
+
let vecLen = 0;
|
|
96
|
+
const vectors = [];
|
|
97
|
+
for (const row of embRows) {
|
|
98
|
+
const arr = deserializeEmbedding(row.embedding);
|
|
99
|
+
if (vecLen === 0) vecLen = arr.length;
|
|
100
|
+
if (arr.length === vecLen) vectors.push(arr);
|
|
101
|
+
}
|
|
102
|
+
if (vectors.length === 0 || vecLen === 0) continue;
|
|
103
|
+
const mean = new Array(vecLen).fill(0);
|
|
104
|
+
for (const vec of vectors) for (let i = 0; i < vecLen; i++) mean[i] += vec[i];
|
|
105
|
+
for (let i = 0; i < vecLen; i++) mean[i] /= vectors.length;
|
|
106
|
+
embeddings.set(vp, mean);
|
|
107
|
+
}
|
|
108
|
+
const existingEdgeKeys = new Set(edges.map((e) => `${e.source}|||${e.target}`));
|
|
109
|
+
const pathsWithEmbeddings = Array.from(embeddings.keys());
|
|
110
|
+
for (let i = 0; i < pathsWithEmbeddings.length; i++) for (let j = i + 1; j < pathsWithEmbeddings.length; j++) {
|
|
111
|
+
const pathA = pathsWithEmbeddings[i];
|
|
112
|
+
const pathB = pathsWithEmbeddings[j];
|
|
113
|
+
const sim = cosineSimilarity(embeddings.get(pathA), embeddings.get(pathB));
|
|
114
|
+
if (sim < semanticThreshold) continue;
|
|
115
|
+
const keyAB = `${pathA}|||${pathB}`;
|
|
116
|
+
const keyBA = `${pathB}|||${pathA}`;
|
|
117
|
+
if (existingEdgeKeys.has(keyAB) || existingEdgeKeys.has(keyBA)) continue;
|
|
118
|
+
edges.push({
|
|
119
|
+
source: pathA,
|
|
120
|
+
target: pathB,
|
|
121
|
+
type: "semantic",
|
|
122
|
+
weight: sim
|
|
123
|
+
});
|
|
124
|
+
existingEdgeKeys.add(keyAB);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
nodes,
|
|
129
|
+
edges
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
export { handleGraphNeighborhood };
|
|
135
|
+
//# sourceMappingURL=neighborhood-DSkvFMAv.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"neighborhood-DSkvFMAv.mjs","names":[],"sources":["../src/graph/neighborhood.ts"],"sourcesContent":["/**\n * neighborhood.ts — graph_neighborhood endpoint handler\n *\n * Given a set of vault note paths (typically the notes inside a cluster),\n * returns the individual note nodes and the wikilink edges between them.\n *\n * Optionally enriches with semantic edges computed from cosine similarity\n * between chunk embeddings stored in the federation database.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { Pool } from \"pg\";\nimport { deserializeEmbedding } from \"../memory/embeddings.js\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphNeighborhoodParams {\n /** Vault-relative paths of notes in the cluster */\n vault_paths: string[];\n /** Numeric PAI project ID */\n project_id: number;\n /** Whether to compute semantic similarity edges (default: false) */\n include_semantic_edges?: boolean;\n /** Cosine similarity threshold for semantic edges (default: 0.7) */\n semantic_threshold?: number;\n}\n\nexport interface NoteNode {\n vault_path: string;\n title: string;\n folder: string;\n observation_types: Record<string, number>;\n dominant_type: string;\n updated_at: number;\n word_count: number;\n}\n\nexport interface NoteEdge {\n source: string;\n target: string;\n type: \"wikilink\" | \"semantic\";\n weight: number;\n}\n\nexport interface GraphNeighborhoodResult {\n nodes: NoteNode[];\n edges: NoteEdge[];\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n if (a.length !== b.length || a.length === 0) return 0;\n let dot = 0;\n let normA = 0;\n let normB = 0;\n for (let i = 0; i < a.length; i++) {\n dot += a[i] * b[i];\n normA += a[i] * a[i];\n normB += b[i] * b[i];\n }\n if (normA === 0 || normB === 0) return 0;\n return dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n\nfunction dominantType(counts: Record<string, number>): string {\n let dominant = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n dominant = type;\n }\n }\n return dominant;\n}\n\n// ---------------------------------------------------------------------------\n// Observation type enrichment (same pattern as clusters.ts)\n// ---------------------------------------------------------------------------\n\nasync function fetchObservationTypes(\n pool: Pool,\n filePaths: string[],\n projectId: number\n): Promise<Map<string, Record<string, number>>> {\n if (filePaths.length === 0) return new Map();\n\n try {\n const params: (string[] | number)[] = [filePaths, projectId];\n\n const result = await pool.query<{ path: string; type: string; cnt: string }>(\n `SELECT unnested_path AS path, type, COUNT(*) AS cnt\n FROM pai_observations,\n LATERAL unnest(files_modified || files_read) AS unnested_path\n WHERE unnested_path = ANY($1::text[])\n AND project_id = $2\n GROUP BY unnested_path, type`,\n params\n );\n\n const byPath = new Map<string, Record<string, number>>();\n for (const row of result.rows) {\n const existing = byPath.get(row.path) ?? {};\n existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);\n byPath.set(row.path, existing);\n }\n return byPath;\n } catch {\n return new Map();\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphNeighborhood(\n pool: Pool | null,\n backend: StorageBackend,\n params: GraphNeighborhoodParams\n): Promise<GraphNeighborhoodResult> {\n const vaultPaths = params.vault_paths ?? [];\n if (vaultPaths.length === 0) {\n return { nodes: [], edges: [] };\n }\n\n const includeSemanticEdges = params.include_semantic_edges ?? false;\n const semanticThreshold = params.semantic_threshold ?? 0.7;\n\n // -------------------------------------------------------------------------\n // 1. Fetch node metadata from vault_files\n // -------------------------------------------------------------------------\n\n const fileRows = await backend.getVaultFilesByPaths(vaultPaths);\n\n const fileIndex = new Map<string, { vaultPath: string; title: string | null; indexedAt: number }>();\n for (const row of fileRows) {\n fileIndex.set(row.vaultPath, row);\n }\n\n // -------------------------------------------------------------------------\n // 2. Fetch observation types (Postgres if available)\n // -------------------------------------------------------------------------\n\n const observationsByPath =\n pool !== null\n ? await fetchObservationTypes(pool, vaultPaths, params.project_id)\n : new Map<string, Record<string, number>>();\n\n // -------------------------------------------------------------------------\n // 3. Build NoteNode array\n // -------------------------------------------------------------------------\n\n const nodes: NoteNode[] = vaultPaths.map((vp) => {\n const fileRow = fileIndex.get(vp);\n const fileName = vp.split(\"/\").pop() ?? vp;\n const rawTitle = fileRow?.title ?? fileName.replace(/\\.md$/i, \"\");\n\n const obsCounts = observationsByPath.get(vp) ?? {};\n\n return {\n vault_path: vp,\n title: rawTitle,\n folder: folderFromPath(vp),\n observation_types: obsCounts,\n dominant_type: dominantType(obsCounts),\n updated_at: fileRow?.indexedAt ?? 0,\n word_count: 0,\n };\n });\n\n // -------------------------------------------------------------------------\n // 4. Fetch wikilink edges between the provided paths\n // -------------------------------------------------------------------------\n\n const pathSet = new Set(vaultPaths);\n const linkRows = await backend.getVaultLinksFromPaths(vaultPaths);\n\n const edges: NoteEdge[] = [];\n\n for (const row of linkRows) {\n if (!row.targetPath || !pathSet.has(row.targetPath)) continue;\n\n edges.push({\n source: row.sourcePath,\n target: row.targetPath,\n type: \"wikilink\",\n weight: 1.0,\n });\n }\n\n // -------------------------------------------------------------------------\n // 5. Optional: semantic edges\n // -------------------------------------------------------------------------\n\n if (includeSemanticEdges && vaultPaths.length > 1) {\n // Fetch mean embeddings for all paths\n const embeddings = new Map<string, number[]>();\n for (const vp of vaultPaths) {\n const chunkRows = await backend.getChunksForPath(params.project_id, vp);\n const embRows = chunkRows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (embRows.length === 0) continue;\n\n let vecLen = 0;\n const vectors: Float32Array[] = [];\n\n for (const row of embRows) {\n const arr = deserializeEmbedding(row.embedding);\n if (vecLen === 0) vecLen = arr.length;\n if (arr.length === vecLen) vectors.push(arr);\n }\n\n if (vectors.length === 0 || vecLen === 0) continue;\n\n const mean = new Array<number>(vecLen).fill(0);\n for (const vec of vectors) {\n for (let i = 0; i < vecLen; i++) {\n mean[i] += vec[i];\n }\n }\n for (let i = 0; i < vecLen; i++) {\n mean[i] /= vectors.length;\n }\n embeddings.set(vp, mean);\n }\n\n const existingEdgeKeys = new Set<string>(\n edges.map((e) => `${e.source}|||${e.target}`)\n );\n\n const pathsWithEmbeddings = Array.from(embeddings.keys());\n for (let i = 0; i < pathsWithEmbeddings.length; i++) {\n for (let j = i + 1; j < pathsWithEmbeddings.length; j++) {\n const pathA = pathsWithEmbeddings[i];\n const pathB = pathsWithEmbeddings[j];\n\n const vecA = embeddings.get(pathA)!;\n const vecB = embeddings.get(pathB)!;\n\n const sim = cosineSimilarity(vecA, vecB);\n if (sim < semanticThreshold) continue;\n\n const keyAB = `${pathA}|||${pathB}`;\n const keyBA = `${pathB}|||${pathA}`;\n if (existingEdgeKeys.has(keyAB) || existingEdgeKeys.has(keyBA)) continue;\n\n edges.push({\n source: pathA,\n target: pathB,\n type: \"semantic\",\n weight: sim,\n });\n existingEdgeKeys.add(keyAB);\n }\n }\n }\n\n return { nodes, edges };\n}\n"],"mappings":";;;AAuDA,SAAS,eAAe,WAA2B;CACjD,MAAM,YAAY,UAAU,YAAY,IAAI;AAC5C,QAAO,cAAc,KAAK,KAAK,UAAU,MAAM,GAAG,UAAU;;AAG9D,SAAS,iBAAiB,GAAa,GAAqB;AAC1D,KAAI,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAG,QAAO;CACpD,IAAI,MAAM;CACV,IAAI,QAAQ;CACZ,IAAI,QAAQ;AACZ,MAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK;AACjC,SAAO,EAAE,KAAK,EAAE;AAChB,WAAS,EAAE,KAAK,EAAE;AAClB,WAAS,EAAE,KAAK,EAAE;;AAEpB,KAAI,UAAU,KAAK,UAAU,EAAG,QAAO;AACvC,QAAO,OAAO,KAAK,KAAK,MAAM,GAAG,KAAK,KAAK,MAAM;;AAGnD,SAAS,aAAa,QAAwC;CAC5D,IAAI,WAAW;CACf,IAAI,WAAW;AACf,MAAK,MAAM,CAAC,MAAM,MAAM,OAAO,QAAQ,OAAO,CAC5C,KAAI,IAAI,UAAU;AAChB,aAAW;AACX,aAAW;;AAGf,QAAO;;AAOT,eAAe,sBACb,MACA,WACA,WAC8C;AAC9C,KAAI,UAAU,WAAW,EAAG,wBAAO,IAAI,KAAK;AAE5C,KAAI;EACF,MAAM,SAAgC,CAAC,WAAW,UAAU;EAE5D,MAAM,SAAS,MAAM,KAAK,MACxB;;;;;sCAMA,OACD;EAED,MAAM,yBAAS,IAAI,KAAqC;AACxD,OAAK,MAAM,OAAO,OAAO,MAAM;GAC7B,MAAM,WAAW,OAAO,IAAI,IAAI,KAAK,IAAI,EAAE;AAC3C,YAAS,IAAI,SAAS,SAAS,IAAI,SAAS,KAAK,SAAS,IAAI,KAAK,GAAG;AACtE,UAAO,IAAI,IAAI,MAAM,SAAS;;AAEhC,SAAO;SACD;AACN,yBAAO,IAAI,KAAK;;;AAQpB,eAAsB,wBACpB,MACA,SACA,QACkC;CAClC,MAAM,aAAa,OAAO,eAAe,EAAE;AAC3C,KAAI,WAAW,WAAW,EACxB,QAAO;EAAE,OAAO,EAAE;EAAE,OAAO,EAAE;EAAE;CAGjC,MAAM,uBAAuB,OAAO,0BAA0B;CAC9D,MAAM,oBAAoB,OAAO,sBAAsB;CAMvD,MAAM,WAAW,MAAM,QAAQ,qBAAqB,WAAW;CAE/D,MAAM,4BAAY,IAAI,KAA6E;AACnG,MAAK,MAAM,OAAO,SAChB,WAAU,IAAI,IAAI,WAAW,IAAI;CAOnC,MAAM,qBACJ,SAAS,OACL,MAAM,sBAAsB,MAAM,YAAY,OAAO,WAAW,mBAChE,IAAI,KAAqC;CAM/C,MAAM,QAAoB,WAAW,KAAK,OAAO;EAC/C,MAAM,UAAU,UAAU,IAAI,GAAG;EACjC,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,KAAK,IAAI;EACxC,MAAM,WAAW,SAAS,SAAS,SAAS,QAAQ,UAAU,GAAG;EAEjE,MAAM,YAAY,mBAAmB,IAAI,GAAG,IAAI,EAAE;AAElD,SAAO;GACL,YAAY;GACZ,OAAO;GACP,QAAQ,eAAe,GAAG;GAC1B,mBAAmB;GACnB,eAAe,aAAa,UAAU;GACtC,YAAY,SAAS,aAAa;GAClC,YAAY;GACb;GACD;CAMF,MAAM,UAAU,IAAI,IAAI,WAAW;CACnC,MAAM,WAAW,MAAM,QAAQ,uBAAuB,WAAW;CAEjE,MAAM,QAAoB,EAAE;AAE5B,MAAK,MAAM,OAAO,UAAU;AAC1B,MAAI,CAAC,IAAI,cAAc,CAAC,QAAQ,IAAI,IAAI,WAAW,CAAE;AAErD,QAAM,KAAK;GACT,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ,MAAM;GACN,QAAQ;GACT,CAAC;;AAOJ,KAAI,wBAAwB,WAAW,SAAS,GAAG;EAEjD,MAAM,6BAAa,IAAI,KAAuB;AAC9C,OAAK,MAAM,MAAM,YAAY;GAE3B,MAAM,WADY,MAAM,QAAQ,iBAAiB,OAAO,YAAY,GAAG,EAC7C,QAAO,MAAK,EAAE,cAAc,KAAK;AAC3D,OAAI,QAAQ,WAAW,EAAG;GAE1B,IAAI,SAAS;GACb,MAAM,UAA0B,EAAE;AAElC,QAAK,MAAM,OAAO,SAAS;IACzB,MAAM,MAAM,qBAAqB,IAAI,UAAU;AAC/C,QAAI,WAAW,EAAG,UAAS,IAAI;AAC/B,QAAI,IAAI,WAAW,OAAQ,SAAQ,KAAK,IAAI;;AAG9C,OAAI,QAAQ,WAAW,KAAK,WAAW,EAAG;GAE1C,MAAM,OAAO,IAAI,MAAc,OAAO,CAAC,KAAK,EAAE;AAC9C,QAAK,MAAM,OAAO,QAChB,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,IAC1B,MAAK,MAAM,IAAI;AAGnB,QAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,IAC1B,MAAK,MAAM,QAAQ;AAErB,cAAW,IAAI,IAAI,KAAK;;EAG1B,MAAM,mBAAmB,IAAI,IAC3B,MAAM,KAAK,MAAM,GAAG,EAAE,OAAO,KAAK,EAAE,SAAS,CAC9C;EAED,MAAM,sBAAsB,MAAM,KAAK,WAAW,MAAM,CAAC;AACzD,OAAK,IAAI,IAAI,GAAG,IAAI,oBAAoB,QAAQ,IAC9C,MAAK,IAAI,IAAI,IAAI,GAAG,IAAI,oBAAoB,QAAQ,KAAK;GACvD,MAAM,QAAQ,oBAAoB;GAClC,MAAM,QAAQ,oBAAoB;GAKlC,MAAM,MAAM,iBAHC,WAAW,IAAI,MAAM,EACrB,WAAW,IAAI,MAAM,CAEM;AACxC,OAAI,MAAM,kBAAmB;GAE7B,MAAM,QAAQ,GAAG,MAAM,KAAK;GAC5B,MAAM,QAAQ,GAAG,MAAM,KAAK;AAC5B,OAAI,iBAAiB,IAAI,MAAM,IAAI,iBAAiB,IAAI,MAAM,CAAE;AAEhE,SAAM,KAAK;IACT,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,QAAQ;IACT,CAAC;AACF,oBAAiB,IAAI,MAAM;;;AAKjC,QAAO;EAAE;EAAO;EAAO"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//#region src/graph/note-context.ts
|
|
2
|
+
function folderFromPath(vaultPath) {
|
|
3
|
+
const lastSlash = vaultPath.lastIndexOf("/");
|
|
4
|
+
return lastSlash === -1 ? "" : vaultPath.slice(0, lastSlash);
|
|
5
|
+
}
|
|
6
|
+
function dominantType(counts) {
|
|
7
|
+
let best = "unknown";
|
|
8
|
+
let maxCount = 0;
|
|
9
|
+
for (const [type, n] of Object.entries(counts)) if (n > maxCount) {
|
|
10
|
+
maxCount = n;
|
|
11
|
+
best = type;
|
|
12
|
+
}
|
|
13
|
+
return best;
|
|
14
|
+
}
|
|
15
|
+
async function fetchObservationTypes(pool, filePaths, projectId) {
|
|
16
|
+
if (filePaths.length === 0) return /* @__PURE__ */ new Map();
|
|
17
|
+
try {
|
|
18
|
+
const result = await pool.query(`SELECT unnested_path AS path, type, COUNT(*) AS cnt
|
|
19
|
+
FROM pai_observations,
|
|
20
|
+
LATERAL unnest(files_modified || files_read) AS unnested_path
|
|
21
|
+
WHERE unnested_path = ANY($1::text[])
|
|
22
|
+
AND project_id = $2
|
|
23
|
+
GROUP BY unnested_path, type`, [filePaths, projectId]);
|
|
24
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const row of result.rows) {
|
|
26
|
+
const existing = byPath.get(row.path) ?? {};
|
|
27
|
+
existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);
|
|
28
|
+
byPath.set(row.path, existing);
|
|
29
|
+
}
|
|
30
|
+
return byPath;
|
|
31
|
+
} catch {
|
|
32
|
+
return /* @__PURE__ */ new Map();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function buildNoteNode(vaultPath, fileIndex, obsByPath) {
|
|
36
|
+
const fileRow = fileIndex.get(vaultPath);
|
|
37
|
+
const fileName = vaultPath.split("/").pop() ?? vaultPath;
|
|
38
|
+
const rawTitle = fileRow?.title ?? fileName.replace(/\.md$/i, "");
|
|
39
|
+
const obsCounts = obsByPath.get(vaultPath) ?? {};
|
|
40
|
+
return {
|
|
41
|
+
vault_path: vaultPath,
|
|
42
|
+
title: rawTitle,
|
|
43
|
+
folder: folderFromPath(vaultPath),
|
|
44
|
+
observation_types: obsCounts,
|
|
45
|
+
dominant_type: dominantType(obsCounts),
|
|
46
|
+
updated_at: fileRow?.indexedAt ?? 0,
|
|
47
|
+
word_count: 0
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function handleGraphNoteContext(pool, backend, params) {
|
|
51
|
+
const focalPath = params.vault_path;
|
|
52
|
+
if (!focalPath) throw new Error("graph_note_context: vault_path is required");
|
|
53
|
+
const maxNeighbors = params.max_neighbors ?? 50;
|
|
54
|
+
const includeBacklinks = params.include_backlinks !== false;
|
|
55
|
+
const includeOutlinks = params.include_outlinks !== false;
|
|
56
|
+
const neighborPaths = /* @__PURE__ */ new Set();
|
|
57
|
+
const rawEdges = [];
|
|
58
|
+
if (includeOutlinks) {
|
|
59
|
+
const outLinks = await backend.getLinksFromSource(focalPath);
|
|
60
|
+
for (const link of outLinks) {
|
|
61
|
+
if (!link.targetPath) continue;
|
|
62
|
+
neighborPaths.add(link.targetPath);
|
|
63
|
+
rawEdges.push({
|
|
64
|
+
source: focalPath,
|
|
65
|
+
target: link.targetPath
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (includeBacklinks) {
|
|
70
|
+
const inLinks = await backend.getLinksToTarget(focalPath);
|
|
71
|
+
for (const link of inLinks) {
|
|
72
|
+
neighborPaths.add(link.sourcePath);
|
|
73
|
+
rawEdges.push({
|
|
74
|
+
source: link.sourcePath,
|
|
75
|
+
target: focalPath
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
let neighborPathList = Array.from(neighborPaths);
|
|
80
|
+
if (neighborPathList.length > maxNeighbors) {
|
|
81
|
+
const linkCount = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const e of rawEdges) {
|
|
83
|
+
const neighbor = e.source === focalPath ? e.target : e.source;
|
|
84
|
+
linkCount.set(neighbor, (linkCount.get(neighbor) ?? 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
neighborPathList = neighborPathList.sort((a, b) => (linkCount.get(b) ?? 0) - (linkCount.get(a) ?? 0)).slice(0, maxNeighbors);
|
|
87
|
+
}
|
|
88
|
+
const retainedSet = new Set(neighborPathList);
|
|
89
|
+
const retainedEdges = rawEdges.filter((e) => {
|
|
90
|
+
const neighbor = e.source === focalPath ? e.target : e.source;
|
|
91
|
+
return retainedSet.has(neighbor);
|
|
92
|
+
});
|
|
93
|
+
const allPaths = [focalPath, ...neighborPathList];
|
|
94
|
+
const fileRows = await backend.getVaultFilesByPaths(allPaths);
|
|
95
|
+
const fileIndex = new Map(fileRows.map((f) => [f.vaultPath, {
|
|
96
|
+
title: f.title,
|
|
97
|
+
indexedAt: f.indexedAt
|
|
98
|
+
}]));
|
|
99
|
+
const obsByPath = pool !== null ? await fetchObservationTypes(pool, allPaths, params.project_id) : /* @__PURE__ */ new Map();
|
|
100
|
+
const focal = buildNoteNode(focalPath, fileIndex, obsByPath);
|
|
101
|
+
const neighbors = neighborPathList.map((vp) => buildNoteNode(vp, fileIndex, obsByPath));
|
|
102
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
103
|
+
const edges = [];
|
|
104
|
+
for (const e of retainedEdges) {
|
|
105
|
+
const key = `${e.source}|||${e.target}`;
|
|
106
|
+
if (!edgeKeys.has(key)) {
|
|
107
|
+
edgeKeys.add(key);
|
|
108
|
+
edges.push({
|
|
109
|
+
source: e.source,
|
|
110
|
+
target: e.target,
|
|
111
|
+
type: "wikilink",
|
|
112
|
+
weight: 1
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
focal,
|
|
118
|
+
neighbors,
|
|
119
|
+
edges,
|
|
120
|
+
cluster_membership: {}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
export { handleGraphNoteContext };
|
|
126
|
+
//# sourceMappingURL=note-context-DrcY4cWm.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"note-context-DrcY4cWm.mjs","names":[],"sources":["../src/graph/note-context.ts"],"sourcesContent":["/**\n * note-context.ts — graph_note_context endpoint handler\n *\n * Given a single vault note path, returns ALL notes linked to or from it\n * across the entire vault (1-hop neighbourhood), plus the edges.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport type { Pool } from \"pg\";\n\n// ---------------------------------------------------------------------------\n// Public param / result types\n// ---------------------------------------------------------------------------\n\nexport interface GraphNoteContextParams {\n vault_path: string;\n project_id: number;\n max_neighbors?: number;\n include_backlinks?: boolean;\n include_outlinks?: boolean;\n}\n\nexport interface NoteNode {\n vault_path: string;\n title: string;\n folder: string;\n observation_types: Record<string, number>;\n dominant_type: string;\n updated_at: number;\n word_count: number;\n}\n\nexport interface NoteEdge {\n source: string;\n target: string;\n type: \"wikilink\" | \"semantic\";\n weight: number;\n}\n\nexport interface GraphNoteContextResult {\n focal: NoteNode;\n neighbors: NoteNode[];\n edges: NoteEdge[];\n cluster_membership: Record<string, number>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction folderFromPath(vaultPath: string): string {\n const lastSlash = vaultPath.lastIndexOf(\"/\");\n return lastSlash === -1 ? \"\" : vaultPath.slice(0, lastSlash);\n}\n\nfunction dominantType(counts: Record<string, number>): string {\n let best = \"unknown\";\n let maxCount = 0;\n for (const [type, n] of Object.entries(counts)) {\n if (n > maxCount) {\n maxCount = n;\n best = type;\n }\n }\n return best;\n}\n\n// ---------------------------------------------------------------------------\n// Observation type enrichment\n// ---------------------------------------------------------------------------\n\nasync function fetchObservationTypes(\n pool: Pool,\n filePaths: string[],\n projectId: number\n): Promise<Map<string, Record<string, number>>> {\n if (filePaths.length === 0) return new Map();\n\n try {\n const result = await pool.query<{ path: string; type: string; cnt: string }>(\n `SELECT unnested_path AS path, type, COUNT(*) AS cnt\n FROM pai_observations,\n LATERAL unnest(files_modified || files_read) AS unnested_path\n WHERE unnested_path = ANY($1::text[])\n AND project_id = $2\n GROUP BY unnested_path, type`,\n [filePaths, projectId]\n );\n\n const byPath = new Map<string, Record<string, number>>();\n for (const row of result.rows) {\n const existing = byPath.get(row.path) ?? {};\n existing[row.type] = (existing[row.type] ?? 0) + parseInt(row.cnt, 10);\n byPath.set(row.path, existing);\n }\n return byPath;\n } catch {\n return new Map();\n }\n}\n\nfunction buildNoteNode(\n vaultPath: string,\n fileIndex: Map<string, { title: string | null; indexedAt: number }>,\n obsByPath: Map<string, Record<string, number>>\n): NoteNode {\n const fileRow = fileIndex.get(vaultPath);\n const fileName = vaultPath.split(\"/\").pop() ?? vaultPath;\n const rawTitle = fileRow?.title ?? fileName.replace(/\\.md$/i, \"\");\n const obsCounts = obsByPath.get(vaultPath) ?? {};\n\n return {\n vault_path: vaultPath,\n title: rawTitle,\n folder: folderFromPath(vaultPath),\n observation_types: obsCounts,\n dominant_type: dominantType(obsCounts),\n updated_at: fileRow?.indexedAt ?? 0,\n word_count: 0,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Main handler\n// ---------------------------------------------------------------------------\n\nexport async function handleGraphNoteContext(\n pool: Pool | null,\n backend: StorageBackend,\n params: GraphNoteContextParams\n): Promise<GraphNoteContextResult> {\n const focalPath = params.vault_path;\n if (!focalPath) {\n throw new Error(\"graph_note_context: vault_path is required\");\n }\n\n const maxNeighbors = params.max_neighbors ?? 50;\n const includeBacklinks = params.include_backlinks !== false;\n const includeOutlinks = params.include_outlinks !== false;\n\n // -------------------------------------------------------------------------\n // 1. Collect 1-hop neighbor paths via vault_links\n // -------------------------------------------------------------------------\n\n const neighborPaths = new Set<string>();\n const rawEdges: Array<{ source: string; target: string }> = [];\n\n if (includeOutlinks) {\n const outLinks = await backend.getLinksFromSource(focalPath);\n for (const link of outLinks) {\n if (!link.targetPath) continue;\n neighborPaths.add(link.targetPath);\n rawEdges.push({ source: focalPath, target: link.targetPath });\n }\n }\n\n if (includeBacklinks) {\n const inLinks = await backend.getLinksToTarget(focalPath);\n for (const link of inLinks) {\n neighborPaths.add(link.sourcePath);\n rawEdges.push({ source: link.sourcePath, target: focalPath });\n }\n }\n\n // Cap neighbors at max_neighbors, keeping the most-linked ones\n let neighborPathList = Array.from(neighborPaths);\n if (neighborPathList.length > maxNeighbors) {\n const linkCount = new Map<string, number>();\n for (const e of rawEdges) {\n const neighbor = e.source === focalPath ? e.target : e.source;\n linkCount.set(neighbor, (linkCount.get(neighbor) ?? 0) + 1);\n }\n neighborPathList = neighborPathList\n .sort((a, b) => (linkCount.get(b) ?? 0) - (linkCount.get(a) ?? 0))\n .slice(0, maxNeighbors);\n }\n\n const retainedSet = new Set(neighborPathList);\n const retainedEdges = rawEdges.filter((e) => {\n const neighbor = e.source === focalPath ? e.target : e.source;\n return retainedSet.has(neighbor);\n });\n\n // -------------------------------------------------------------------------\n // 2. Fetch vault_files metadata for focal + all neighbors\n // -------------------------------------------------------------------------\n\n const allPaths = [focalPath, ...neighborPathList];\n const fileRows = await backend.getVaultFilesByPaths(allPaths);\n const fileIndex = new Map<string, { title: string | null; indexedAt: number }>(\n fileRows.map(f => [f.vaultPath, { title: f.title, indexedAt: f.indexedAt }])\n );\n\n // -------------------------------------------------------------------------\n // 3. Observation type enrichment (Postgres if available)\n // -------------------------------------------------------------------------\n\n const obsByPath =\n pool !== null\n ? await fetchObservationTypes(pool, allPaths, params.project_id)\n : new Map<string, Record<string, number>>();\n\n // -------------------------------------------------------------------------\n // 4. Build focal NoteNode\n // -------------------------------------------------------------------------\n\n const focal = buildNoteNode(focalPath, fileIndex, obsByPath);\n\n // -------------------------------------------------------------------------\n // 5. Build neighbor NoteNode array\n // -------------------------------------------------------------------------\n\n const neighbors: NoteNode[] = neighborPathList.map((vp) =>\n buildNoteNode(vp, fileIndex, obsByPath)\n );\n\n // -------------------------------------------------------------------------\n // 6. Deduplicate edges\n // -------------------------------------------------------------------------\n\n const edgeKeys = new Set<string>();\n const edges: NoteEdge[] = [];\n for (const e of retainedEdges) {\n const key = `${e.source}|||${e.target}`;\n if (!edgeKeys.has(key)) {\n edgeKeys.add(key);\n edges.push({\n source: e.source,\n target: e.target,\n type: \"wikilink\",\n weight: 1.0,\n });\n }\n }\n\n return {\n focal,\n neighbors,\n edges,\n cluster_membership: {},\n };\n}\n"],"mappings":"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"}
|