@tekmidian/pai 0.66.1 → 0.66.2

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.
Files changed (158) hide show
  1. package/dist/{aibroker-client-B8c42Lh8.mjs → aibroker-client-CHEEvJZW.mjs} +2 -2
  2. package/dist/{aibroker-client-C5Fw7DNz.mjs → aibroker-client-Dfv7j1Od.mjs} +3 -3
  3. package/dist/{aibroker-client-C5Fw7DNz.mjs.map → aibroker-client-Dfv7j1Od.mjs.map} +1 -1
  4. package/dist/{auto-route-o0BOsXn7.mjs → auto-route-BnizyALK.mjs} +65 -4
  5. package/dist/auto-route-BnizyALK.mjs.map +1 -0
  6. package/dist/auto-route-D6fW1Q8z.mjs +3 -0
  7. package/dist/{chain-CZfKIEq9.mjs → chain-D5876UQX.mjs} +3 -4
  8. package/dist/{chain-CZfKIEq9.mjs.map → chain-D5876UQX.mjs.map} +1 -1
  9. package/dist/cli/index.mjs +18 -23
  10. package/dist/cli/index.mjs.map +1 -1
  11. package/dist/cli/program.mjs +17 -22
  12. package/dist/{config-BbLFD7Uf.mjs → config-C-LiGlop.mjs} +2 -2
  13. package/dist/{config-BbLFD7Uf.mjs.map → config-C-LiGlop.mjs.map} +1 -1
  14. package/dist/{config-YinjgXEJ.mjs → config-DKIA7wgF.mjs} +1 -1
  15. package/dist/{context-handover-cache-pkHzmL3c.mjs → context-handover-cache-9PGvXRIw.mjs} +984 -10
  16. package/dist/context-handover-cache-9PGvXRIw.mjs.map +1 -0
  17. package/dist/daemon/index.mjs +17 -19
  18. package/dist/daemon/index.mjs.map +1 -1
  19. package/dist/daemon-BUgNvMUh.mjs +5029 -0
  20. package/dist/daemon-BUgNvMUh.mjs.map +1 -0
  21. package/dist/daemon-DL0eglMJ.mjs +18 -0
  22. package/dist/daemon-mcp/index.mjs +12 -9
  23. package/dist/daemon-mcp/index.mjs.map +1 -1
  24. package/dist/{embeddings-CcscYWwk.mjs → embeddings-CEBGrzwu.mjs} +1 -1
  25. package/dist/{embeddings-Bx3q0QOY.mjs → embeddings-DOLZnT1X.mjs} +1 -1
  26. package/dist/{embeddings-Bx3q0QOY.mjs.map → embeddings-DOLZnT1X.mjs.map} +1 -1
  27. package/dist/{env-JNEIrQWg.mjs → env-DiolswKQ.mjs} +1 -1
  28. package/dist/{env-JNEIrQWg.mjs.map → env-DiolswKQ.mjs.map} +1 -1
  29. package/dist/factory-DrBg24UT.mjs +8 -0
  30. package/dist/factory-Ypf8r0fK.mjs +2892 -0
  31. package/dist/factory-Ypf8r0fK.mjs.map +1 -0
  32. package/dist/{fallback-CIiq2Raw.mjs → fallback-D51R4shY.mjs} +23 -11
  33. package/dist/fallback-D51R4shY.mjs.map +1 -0
  34. package/dist/{chunker-BH4i-F2b.mjs → helpers-CZsi_49C.mjs} +221 -2
  35. package/dist/helpers-CZsi_49C.mjs.map +1 -0
  36. package/dist/hooks/worker-status-line.mjs.map +2 -2
  37. package/dist/index.mjs +6 -7
  38. package/dist/{ipc-client-D16Xw6Uo.mjs → ipc-client-BLYX51nG.mjs} +2 -2
  39. package/dist/{ipc-client-D16Xw6Uo.mjs.map → ipc-client-BLYX51nG.mjs.map} +1 -1
  40. package/dist/main-resolver-BYgP86C1.mjs +7 -0
  41. package/dist/{main-resolver-DPPwHNtn.mjs → main-resolver-kVkMJZUD.mjs} +6 -6
  42. package/dist/{main-resolver-DPPwHNtn.mjs.map → main-resolver-kVkMJZUD.mjs.map} +1 -1
  43. package/dist/{migrate-BD7D8EEh.mjs → migrate-Bq0esMOa.mjs} +2 -2
  44. package/dist/{migrate-BD7D8EEh.mjs.map → migrate-Bq0esMOa.mjs.map} +1 -1
  45. package/dist/{pai-marker-D1MMswkz.mjs → pai-marker-DXVpFsYz.mjs} +1 -1
  46. package/dist/{pai-marker-D1MMswkz.mjs.map → pai-marker-DXVpFsYz.mjs.map} +1 -1
  47. package/dist/{planner-Dm5Acyo1.mjs → planner-D5yt0EfA.mjs} +10 -7
  48. package/dist/{planner-Dm5Acyo1.mjs.map → planner-D5yt0EfA.mjs.map} +1 -1
  49. package/dist/postgres-BmLr0MUm.mjs +5 -0
  50. package/dist/{postgres-Ceqsa64C.mjs → postgres-D3xc2RB4.mjs} +364 -9
  51. package/dist/postgres-D3xc2RB4.mjs.map +1 -0
  52. package/dist/{program-JJ0TwSHI.mjs → program-DIXRtAFy.mjs} +77 -70
  53. package/dist/program-DIXRtAFy.mjs.map +1 -0
  54. package/dist/{query-feedback-BIaZTTFO.mjs → query-feedback-B7FYE4JR.mjs} +1 -1
  55. package/dist/{query-feedback-BIaZTTFO.mjs.map → query-feedback-B7FYE4JR.mjs.map} +1 -1
  56. package/dist/{reranker-DKv80KO5.mjs → reranker-3lnggwgq.mjs} +1 -1
  57. package/dist/{reranker-DKv80KO5.mjs.map → reranker-3lnggwgq.mjs.map} +1 -1
  58. package/dist/{reranker-CFiEzHQu.mjs → reranker-CZ2mP4cf.mjs} +1 -1
  59. package/dist/router-BK-hFeQ6.mjs +3 -0
  60. package/dist/{router-a5G7q8_3.mjs → router-BaTbc9VX.mjs} +2 -2
  61. package/dist/{router-a5G7q8_3.mjs.map → router-BaTbc9VX.mjs.map} +1 -1
  62. package/dist/{run-BCpbxqA5.mjs → run-CLYTOm0x.mjs} +599 -22
  63. package/dist/run-CLYTOm0x.mjs.map +1 -0
  64. package/dist/{run-env-DRcp7A8K.mjs → run-env-Bnmf6bRI.mjs} +2 -2
  65. package/dist/{run-env-DRcp7A8K.mjs.map → run-env-Bnmf6bRI.mjs.map} +1 -1
  66. package/dist/{runtime-paths-CHTg3ywb.mjs → runtime-paths-D3FKAm69.mjs} +1 -1
  67. package/dist/{runtime-paths-CHTg3ywb.mjs.map → runtime-paths-D3FKAm69.mjs.map} +1 -1
  68. package/dist/{search-BOSphCJ1.mjs → search-CNAGTiJP.mjs} +2 -2
  69. package/dist/{search-BOSphCJ1.mjs.map → search-CNAGTiJP.mjs.map} +1 -1
  70. package/dist/{sources-CijVso3n.mjs → sources-CM2g-CLT.mjs} +2 -2
  71. package/dist/{sources-CijVso3n.mjs.map → sources-CM2g-CLT.mjs.map} +1 -1
  72. package/dist/{stop-words-BdQuaE9K.mjs → stop-words-DtxaTWU_.mjs} +1 -1
  73. package/dist/{stop-words-BdQuaE9K.mjs.map → stop-words-DtxaTWU_.mjs.map} +1 -1
  74. package/dist/{utils-C9HsYDpO.mjs → utils-DddRwMsG.mjs} +2 -2
  75. package/dist/{utils-C9HsYDpO.mjs.map → utils-DddRwMsG.mjs.map} +1 -1
  76. package/dist/{zettelkasten-CtGHQWPU.mjs → zettelkasten-BhZMvmoK.mjs} +149 -7
  77. package/dist/zettelkasten-BhZMvmoK.mjs.map +1 -0
  78. package/dist/zettelkasten-BwV1pluL.mjs +5 -0
  79. package/docs/commands/worker.md +2 -0
  80. package/package.json +1 -1
  81. package/dist/async-C2Bm_Lal.mjs +0 -300
  82. package/dist/async-C2Bm_Lal.mjs.map +0 -1
  83. package/dist/auto-route-o0BOsXn7.mjs.map +0 -1
  84. package/dist/chunker-BH4i-F2b.mjs.map +0 -1
  85. package/dist/clusters-BCtD3fbe.mjs +0 -169
  86. package/dist/clusters-BCtD3fbe.mjs.map +0 -1
  87. package/dist/context-handover-cache-pkHzmL3c.mjs.map +0 -1
  88. package/dist/daemon-DXlaVCkw.mjs +0 -1741
  89. package/dist/daemon-DXlaVCkw.mjs.map +0 -1
  90. package/dist/daemon-DyORmxZu.mjs +0 -19
  91. package/dist/detector-BEPFsINR.mjs +0 -3
  92. package/dist/detector-BRTtAWrM.mjs +0 -65
  93. package/dist/detector-BRTtAWrM.mjs.map +0 -1
  94. package/dist/factory-CNpPQQ-5.mjs +0 -249
  95. package/dist/factory-CNpPQQ-5.mjs.map +0 -1
  96. package/dist/factory-DY7x73mE.mjs +0 -3
  97. package/dist/fallback-CIiq2Raw.mjs.map +0 -1
  98. package/dist/federation-db-BTyoufBh.mjs +0 -139
  99. package/dist/federation-db-BTyoufBh.mjs.map +0 -1
  100. package/dist/federation-db-HfIFc7FG.mjs +0 -3
  101. package/dist/helpers-BRCJg0G3.mjs +0 -223
  102. package/dist/helpers-BRCJg0G3.mjs.map +0 -1
  103. package/dist/indexer-backend-DFF2FrYx.mjs +0 -5
  104. package/dist/indexer-backend-isSLg6yE.mjs +0 -1
  105. package/dist/kg-entity-DCOcsVFD.mjs +0 -29
  106. package/dist/kg-entity-DCOcsVFD.mjs.map +0 -1
  107. package/dist/latent-ideas-wwAXquGe.mjs +0 -191
  108. package/dist/latent-ideas-wwAXquGe.mjs.map +0 -1
  109. package/dist/link-boost-CnI7UVMJ.mjs +0 -34
  110. package/dist/link-boost-CnI7UVMJ.mjs.map +0 -1
  111. package/dist/main-resolver-BKz_OuEh.mjs +0 -7
  112. package/dist/merge-2gqRPFu2.mjs +0 -3
  113. package/dist/merge-DgU9OgZy.mjs +0 -6
  114. package/dist/merge-DgU9OgZy.mjs.map +0 -1
  115. package/dist/module-paths-DdRzbkUI.mjs +0 -44
  116. package/dist/module-paths-DdRzbkUI.mjs.map +0 -1
  117. package/dist/neighborhood-2FsmzoxG.mjs +0 -114
  118. package/dist/neighborhood-2FsmzoxG.mjs.map +0 -1
  119. package/dist/note-context-De63k8na.mjs +0 -106
  120. package/dist/note-context-De63k8na.mjs.map +0 -1
  121. package/dist/postgres-Ceqsa64C.mjs.map +0 -1
  122. package/dist/program-JJ0TwSHI.mjs.map +0 -1
  123. package/dist/query-feedback-B_iigYj-.mjs +0 -3
  124. package/dist/registry-db-C7voqML9.mjs +0 -213
  125. package/dist/registry-db-C7voqML9.mjs.map +0 -1
  126. package/dist/registry-db-JHPhA8vF.mjs +0 -3
  127. package/dist/registry-postgres-nOfyBSIW.mjs +0 -795
  128. package/dist/registry-postgres-nOfyBSIW.mjs.map +0 -1
  129. package/dist/registry-sqlite-DrCW1aRK.mjs +0 -593
  130. package/dist/registry-sqlite-DrCW1aRK.mjs.map +0 -1
  131. package/dist/router-CXUGsv85.mjs +0 -3
  132. package/dist/run-BCpbxqA5.mjs.map +0 -1
  133. package/dist/search-Bf3Kub3F.mjs +0 -3
  134. package/dist/server-DgmAHyFK.mjs +0 -403
  135. package/dist/server-DgmAHyFK.mjs.map +0 -1
  136. package/dist/session-keepalive-DxjTHGcK.mjs +0 -582
  137. package/dist/session-keepalive-DxjTHGcK.mjs.map +0 -1
  138. package/dist/sqlite-CqsTy6xo.mjs +0 -936
  139. package/dist/sqlite-CqsTy6xo.mjs.map +0 -1
  140. package/dist/state-DW8zdweW.mjs +0 -3
  141. package/dist/state-HyjqTihC.mjs +0 -76
  142. package/dist/state-HyjqTihC.mjs.map +0 -1
  143. package/dist/themes-CTaOj3e1.mjs +0 -148
  144. package/dist/themes-CTaOj3e1.mjs.map +0 -1
  145. package/dist/tools-BcOKPBtg.mjs +0 -1489
  146. package/dist/tools-BcOKPBtg.mjs.map +0 -1
  147. package/dist/tools-C-_l0o8Z.mjs +0 -6
  148. package/dist/trace-CtV0RO6n.mjs +0 -137
  149. package/dist/trace-CtV0RO6n.mjs.map +0 -1
  150. package/dist/utils-BrX9FLeK.mjs +0 -3
  151. package/dist/vault-indexer-BG1sEVyM.mjs +0 -537
  152. package/dist/vault-indexer-BG1sEVyM.mjs.map +0 -1
  153. package/dist/wakeup-D8n33hYV.mjs +0 -335
  154. package/dist/wakeup-D8n33hYV.mjs.map +0 -1
  155. package/dist/work-queue-worker-4tLa5gpQ.mjs +0 -567
  156. package/dist/work-queue-worker-4tLa5gpQ.mjs.map +0 -1
  157. package/dist/work-queue-worker-rFZvTO3p.mjs +0 -11
  158. package/dist/zettelkasten-CtGHQWPU.mjs.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"work-queue-worker-4tLa5gpQ.mjs","names":["contentToText","contentToText"],"sources":["../src/daemon/templates/context-handover-prompt.ts","../src/daemon/context-handover-worker.ts","../src/daemon/topic-detect-worker.ts","../src/daemon/work-queue-worker.ts"],"sourcesContent":["/**\n * context-handover-prompt.ts — prompt template for the threshold-triggered\n * pre-compaction handover.\n *\n * This is NOT the session-note summary (session-summary-prompt.ts already\n * does that well, and Claude Code's own native compaction summary is also\n * genuinely detailed on file paths and issue numbers). This exists for the\n * one thing neither of those reliably carries: the reasoning behind what\n * happened, which compresses to bullets or vanishes outright once a native\n * compaction fires.\n *\n * FRAMING (lifted from a working example of this exact mechanism, read\n * before writing this prompt): the handover is a DIFF against everything\n * already durable, not a summary of the session. The successor can read\n * git, the tracker, and the notes — so this must not repeat what is\n * recoverable there. What cannot be recovered is what only ever existed in\n * the conversation: reasoning, rejected alternatives, negative results and\n * what they cost, traps discovered but invisible in the repo, and specific\n * values/ids/paths that would otherwise have to be dug up again.\n *\n * Length is not a target. A short session earns a short handover; every\n * line in a long one must be something the successor could not have\n * obtained anywhere else.\n */\n\nexport interface HandoverPromptParams {\n /** Interleaved user + assistant turns, oldest first, each prefixed with\n * who said it — this prompt needs the assistant's reasoning, not just\n * what the user asked for. */\n turns: string[];\n /** Git log output for the session period, if any. */\n gitLog: string;\n cwd: string;\n /** The previous cached handover's summary text, if this session already\n * produced one (the warmup handover, when this call is the refresh).\n * Passed through so the new handover can carry it forward rather than\n * silently dropping everything it already captured — see the \"pointer\n * to the previous handover, kept rather than superseded\" structure. */\n previousHandover?: string;\n}\n\nexport function buildContextHandoverPrompt(params: HandoverPromptParams): string {\n const { turns, gitLog, cwd, previousHandover } = params;\n\n const turnsSection = turns.length > 0\n ? turns.join(\"\\n\\n\")\n : \"(No turns extracted)\";\n\n const gitSection = gitLog.trim() || \"(No git commits during this session)\";\n\n const previousSection = previousHandover\n ? `\\nA PREVIOUS HANDOVER already exists for this session (from an earlier, \\\nlower threshold). Carry it forward — start your response with a one-line \\\npointer to it (\"Carries forward from the previous handover, which stays for \\\nits detail.\") and then write only what has happened SINCE it that a compact \\\nwould also lose. Do not re-derive or repeat what it already covered.\\n\\n\\\nPREVIOUS HANDOVER:\\n${previousHandover}\\n`\n : \"\";\n\n return `This session is approaching a context-window compaction. Write the \\\nhandover that would let a successor continue WITHOUT re-reading this \\\nconversation and WITHOUT asking the user to repeat themselves.\n\nProject directory: ${cwd}\n${previousSection}\nSCOPING RULE — the single most important instruction here: this is a DIFF \\\nagainst everything already durable, not a summary of the session. Assume \\\nthe successor can and will read git history, the issue tracker, and any \\\nnotes files themselves. Do NOT restate anything recoverable there — no file \\\ninventories, no \"implemented X\" restatements, no chronology of commands run. \\\nWrite down ONLY what existed exclusively in this conversation and would \\\notherwise be gone: the reasoning, not the result.\n\nWrite ONLY the sections below that this session actually has content for — \\\nomit a section entirely rather than writing \"none\" or padding it:\n\nWHERE THIS SESSION ENDED — the concrete state right now: what is verified \\\nworking (tests green, tree clean, etc.) versus merely attempted, any branch \\\nor working-tree state that isn't obvious from git status alone (e.g. a \\\ndetached HEAD, an intentionally uncommitted change, a stash).\n\nWHAT CLOSED — each item paired with the evidence that closed it, not just \\\nthe claim that it did.\n\nWHAT IS FILED AND STILL OPEN — each with its next concrete step or who/what \\\nit is waiting on.\n\nDECISIONS & REASONING — choices actually made, each with why this way and \\\nnot another way.\n\nREJECTED ALTERNATIVES — approaches considered and set aside, and why. An \\\nalternative without a reason is not worth recording.\n\nNEGATIVE RESULTS — something investigated that turned out NOT to be the \\\nanswer, kept WITH the reasoning that ruled it out (e.g. \"X looked like the \\\ncause — it wasn't; the actual mechanism was Y\"). Without the reasoning \\\nattached, a successor re-investigates the same dead end.\n\nTRAPS — anything discovered that is invisible from the repo alone and would \\\notherwise bite a successor immediately (a required manual step, an ordering \\\nconstraint, a state that looks fine but isn't).\n\nCRITICAL VALUES — any specific id, path, URL, credential name, version \\\nnumber, or configuration value that appeared in conversation and whose loss \\\nwould force someone to go find it again.\n\nTHE FEW THINGS THAT MATTER MOST, IF NOTHING ELSE IS READ — at most 3 bullets. \\\nForces a priority judgement; do not pad this to fill space.\n\nFormat your response as markdown with those section headers (only the ones \\\nyou have content for), no other structure.\n\n---\n\nCONVERSATION:\n${turnsSection}\n\nGIT COMMITS:\n${gitSection}`;\n}\n","/**\n * context-handover-worker.ts — the threshold-triggered pre-compaction\n * handover.\n *\n * `session-summary-worker.ts` already spawns a model to write session notes,\n * but only from the PreCompact hook itself — which fires at the compaction\n * boundary, far too late to be injected INTO that same compaction's\n * post-compact context. This worker runs earlier, off a token-count\n * threshold (see ../hooks/ts/lib/context-fill.ts), so a model-written\n * handover already exists in a cache file by the time compaction happens.\n *\n * Deliberately narrow: this does not write a session note, does not touch\n * the session-summary cooldown file, and does not run KG extraction. It\n * writes one thing — a cache file the PreCompact hook can read — because its\n * only job is to exist before compaction does, not to duplicate what\n * session-summary-worker already does well.\n *\n * The prompt itself (see templates/context-handover-prompt.ts) treats the\n * handover as a DIFF against everything already durable — git, the tracker,\n * the notes — not a session summary, and a refresh run carries the previous\n * cached handover forward rather than overwriting it.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\n\nimport {\n findLatestJsonl,\n getGitContext,\n spawnSummarizer,\n contentToText,\n} from \"./session-summary-worker.js\";\nimport { buildContextHandoverPrompt } from \"./templates/context-handover-prompt.js\";\nimport {\n readContextHandoverCache,\n writeContextHandoverCache,\n type HandoverThreshold,\n} from \"../hooks/ts/lib/context-handover-cache.js\";\n\nexport type { HandoverThreshold } from \"../hooks/ts/lib/context-handover-cache.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface ContextHandoverPayload {\n cwd: string;\n sessionId: string;\n transcriptPath?: string;\n threshold: HandoverThreshold;\n /** True when this fired via the \"no time for a clean crossing\" path\n * (see isImmediate in context-fill.ts) — logged, not otherwise acted on. */\n urgent?: boolean;\n}\n\n/** How much of the tail of the transcript to feed the summarizer. Generous —\n * this runs at most twice per session, not on every tool call. */\nconst MAX_TURN_CHARS = 150_000;\n\n/** Max turns to include (a hard floor even if MAX_TURN_CHARS is not hit). */\nconst MAX_TURNS = 120;\n\n// ---------------------------------------------------------------------------\n// Transcript → turns\n// ---------------------------------------------------------------------------\n\n/**\n * Interleaved \"User:\"/\"Assistant:\" turns, oldest-kept-from-the-end so a long\n * transcript is truncated to its most recent content rather than its\n * earliest. Unlike session-summary-worker's extractFromJsonl (which keeps\n * only user messages, because a session note doesn't need the assistant's\n * own reasoning) this keeps BOTH sides — the whole point of this prompt is\n * the assistant's reasoning, which lives in assistant turns.\n */\nfunction extractTurns(jsonlPath: string): string[] {\n let raw: string;\n try {\n raw = readFileSync(jsonlPath, \"utf-8\");\n } catch {\n return [];\n }\n\n if (raw.length > MAX_TURN_CHARS) {\n const truncPoint = raw.indexOf(\"\\n\", raw.length - MAX_TURN_CHARS);\n raw = truncPoint >= 0 ? raw.slice(truncPoint + 1) : raw.slice(-MAX_TURN_CHARS);\n }\n\n const turns: string[] = [];\n for (const line of raw.trim().split(\"\\n\")) {\n if (!line.trim()) continue;\n let entry: Record<string, unknown>;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n const text = contentToText(msg?.content);\n if (text && text.length >= 3 && !text.startsWith(\"<system-reminder>\")) {\n turns.push(`User: ${text.slice(0, 2000)}`);\n }\n } else if (entry.type === \"assistant\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n const text = contentToText(msg?.content);\n if (text && text.length >= 3) {\n turns.push(`Assistant: ${text.slice(0, 4000)}`);\n }\n }\n }\n\n return turns.slice(-MAX_TURNS);\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `context-handover` work item.\n *\n * THROWS on every failure path (missing input, no transcript, no turns, an\n * empty/failed summarizer run) rather than logging and returning silently.\n * That used to be a deliberate choice — avoid the work queue's own retry\n * piling up redundant LLM calls — but it made a real failure invisible: a\n * daemon restart mid-spawn (session 77084e72-...) left no cache, no error\n * anywhere the work queue's own stats could show, and a trigger-side marker\n * that (at the time) had already been set to \"done\". Throwing here makes\n * work-queue-worker.ts call markFailed(), which logs prominently AND is\n * visible via `work_queue_stats` / `pai daemon status` — not just a stderr\n * line that can scroll away. It is a second, faster (seconds-to-minutes\n * backoff) retry path alongside the primary one in context-handover-\n * trigger.ts (which retries on its own ~5-minute cadence based on whether a\n * cache actually appeared); the two are complementary, not redundant — a\n * failed cache write costs nothing to retry twice.\n */\nexport async function handleContextHandover(payload: ContextHandoverPayload): Promise<void> {\n const { cwd, sessionId, transcriptPath, threshold, urgent } = payload;\n\n if (!cwd || !sessionId) {\n throw new Error(\"[context-handover] payload missing cwd or sessionId\");\n }\n\n process.stderr.write(\n `[context-handover] Starting for ${cwd} (session=${sessionId}, threshold=${threshold}` +\n `${urgent ? \", urgent\" : \"\"}).\\n`\n );\n\n let jsonlPath: string | null = transcriptPath && existsSync(transcriptPath) ? transcriptPath : null;\n if (!jsonlPath) jsonlPath = findLatestJsonl(cwd);\n if (!jsonlPath) {\n throw new Error(`[context-handover] No transcript found for ${cwd} (session=${sessionId})`);\n }\n\n const turns = extractTurns(jsonlPath);\n if (turns.length === 0) {\n throw new Error(`[context-handover] No turns extracted from ${jsonlPath} (session=${sessionId})`);\n }\n\n const gitLog = await getGitContext(cwd);\n\n // Carry the previous handover forward (see buildContextHandoverPrompt's\n // scoping rule) rather than silently overwriting it — a refresh at the\n // second threshold should not lose what the warmup handover already\n // captured.\n const previous = readContextHandoverCache(sessionId);\n const prompt = buildContextHandoverPrompt({\n turns,\n gitLog,\n cwd,\n previousHandover: previous?.summary,\n });\n process.stderr.write(`[context-handover] Sending ${prompt.length} char prompt to sonnet...\\n`);\n\n const summary = await spawnSummarizer(prompt, \"sonnet\");\n if (!summary || !summary.trim()) {\n // No cache write on this path — an existing cache from an earlier\n // successful run is left untouched rather than clobbered with nothing.\n throw new Error(`[context-handover] sonnet produced no output (session=${sessionId})`);\n }\n\n writeContextHandoverCache({\n sessionId,\n cwd,\n threshold,\n generatedAt: new Date().toISOString(),\n model: \"sonnet\",\n summary: summary.trim(),\n });\n\n process.stderr.write(\n `[context-handover] Wrote cache for session ${sessionId} (${summary.length} chars).\\n`\n );\n}\n","/**\n * topic-detect-worker.ts — Topic shift detection for session note splitting\n *\n * Processes `topic-detect` work items by:\n * 1. Extracting recent user messages from the JSONL transcript\n * 2. Running the BM25-based topic shift detector against the PAI memory DB\n * 3. If a shift is detected, recording a topic boundary marker\n *\n * The actual note splitting is handled by session-summary-worker.ts when it\n * processes the next `session-summary` work item — it uses the TOPIC: line\n * from the summarizer to decide whether to create a new note.\n *\n * This worker provides an additional signal: project-level topic shift\n * (e.g., conversation moved from project A to project B). The session\n * summary worker handles intra-project topic shifts (e.g., from \"dark mode\"\n * to \"keyboard IPC\" within the same project).\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport { homedir } from \"node:os\";\n\nimport { detectTopicShift } from \"../topics/detector.js\";\nimport { registryBackend, storageBackend } from \"./daemon/state.js\";\nimport {\n findNotesDir,\n getCurrentNotePath,\n appendCheckpoint,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface TopicDetectPayload {\n /** Working directory of the session. */\n cwd: string;\n /** Recent conversation context (extracted user messages). */\n context?: string;\n /** The project slug the session is currently routed to. */\n currentProject?: string;\n /** Path to the JSONL transcript (optional — used to extract context if not provided). */\n transcriptPath?: string;\n /** Session ID (for logging). */\n sessionId?: string;\n}\n\n// ---------------------------------------------------------------------------\n// JSONL context extraction (lightweight — just last few user messages)\n// ---------------------------------------------------------------------------\n\nconst MAX_CONTEXT_MESSAGES = 5;\nconst MAX_CONTEXT_CHARS = 2000;\n\n/**\n * Extract recent user messages from a JSONL transcript for topic detection.\n * Takes only the last few messages to represent the current topic.\n */\nfunction extractRecentContext(jsonlPath: string): string {\n try {\n const raw = readFileSync(jsonlPath, \"utf-8\");\n // Read from the end — last 50KB should be more than enough\n const tail = raw.length > 50_000 ? raw.slice(-50_000) : raw;\n const lines = tail.trim().split(\"\\n\");\n\n const messages: string[] = [];\n\n for (let i = lines.length - 1; i >= 0 && messages.length < MAX_CONTEXT_MESSAGES; i--) {\n const line = lines[i].trim();\n if (!line) continue;\n\n try {\n const entry = JSON.parse(line) as Record<string, unknown>;\n if (entry.type === \"user\") {\n const msg = entry.message as Record<string, unknown> | undefined;\n if (msg?.content) {\n const text = contentToText(msg.content);\n if (text && text.length > 3) {\n messages.unshift(text.slice(0, 500));\n }\n }\n }\n } catch { /* skip invalid JSON */ }\n }\n\n return messages.join(\"\\n\\n\").slice(0, MAX_CONTEXT_CHARS);\n } catch {\n return \"\";\n }\n}\n\n/** Convert Claude content (string or content block array) to plain text. */\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\n// ---------------------------------------------------------------------------\n// Topic boundary file — signals to session-summary-worker\n// ---------------------------------------------------------------------------\n\nconst TOPIC_BOUNDARY_FILE = \"topic-boundary.json\";\n\ninterface TopicBoundary {\n timestamp: string;\n previousProject: string | null;\n suggestedProject: string | null;\n confidence: number;\n context: string;\n}\n\n/**\n * Write a topic boundary marker into the Notes directory.\n * The session-summary-worker checks for this file and uses it as an\n * additional signal that a new note should be created.\n */\nfunction writeTopicBoundary(\n cwd: string,\n boundary: TopicBoundary\n): void {\n try {\n const notesInfo = findNotesDir(cwd);\n const boundaryPath = join(notesInfo.path, TOPIC_BOUNDARY_FILE);\n writeFileSync(boundaryPath, JSON.stringify(boundary, null, 2), \"utf-8\");\n process.stderr.write(\n `[topic-detect] Wrote topic boundary marker: ${boundaryPath}\\n`\n );\n } catch (e) {\n process.stderr.write(`[topic-detect] Could not write boundary marker: ${e}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Main entry point\n// ---------------------------------------------------------------------------\n\n/**\n * Process a `topic-detect` work item.\n *\n * Called by work-queue-worker.ts. Throws on fatal errors so the work queue\n * retry logic handles them.\n */\nexport async function handleTopicDetect(payload: TopicDetectPayload): Promise<void> {\n const { cwd, currentProject, transcriptPath, sessionId } = payload;\n\n if (!cwd) {\n throw new Error(\"topic-detect payload missing cwd\");\n }\n\n process.stderr.write(\n `[topic-detect] Starting for ${cwd}` +\n `${currentProject ? ` (project=${currentProject})` : \"\"}` +\n `${sessionId ? ` (session=${sessionId})` : \"\"}\\n`\n );\n\n // Check that daemon state is available\n if (!registryBackend || !storageBackend) {\n process.stderr.write(\n \"[topic-detect] Registry backend or storage backend not available — skipping.\\n\"\n );\n return;\n }\n\n // Extract context from payload or transcript\n let context = payload.context || \"\";\n\n if (!context && transcriptPath && existsSync(transcriptPath)) {\n context = extractRecentContext(transcriptPath);\n }\n\n if (!context || context.trim().length < 10) {\n process.stderr.write(\n \"[topic-detect] Insufficient context for topic detection — skipping.\\n\"\n );\n return;\n }\n\n process.stderr.write(\n `[topic-detect] Context: ${context.length} chars, checking against memory...\\n`\n );\n\n // Run the BM25-based topic shift detector\n const result = await detectTopicShift(registryBackend, storageBackend, {\n context,\n currentProject,\n threshold: 0.6,\n candidates: 20,\n });\n\n process.stderr.write(\n `[topic-detect] Result: shifted=${result.shifted}, ` +\n `suggested=${result.suggestedProject}, confidence=${result.confidence.toFixed(2)}, ` +\n `chunks=${result.chunkCount}\\n`\n );\n\n if (result.topProjects.length > 0) {\n process.stderr.write(\n `[topic-detect] Top projects: ${result.topProjects.map(\n (p) => `${p.slug}(${(p.score * 100).toFixed(0)}%)`\n ).join(\", \")}\\n`\n );\n }\n\n if (result.shifted) {\n // Record the topic boundary\n writeTopicBoundary(cwd, {\n timestamp: new Date().toISOString(),\n previousProject: result.currentProject,\n suggestedProject: result.suggestedProject,\n confidence: result.confidence,\n context: context.slice(0, 200),\n });\n\n // Also append a checkpoint to the current session note\n try {\n const notesInfo = findNotesDir(cwd);\n const notePath = getCurrentNotePath(notesInfo.path);\n if (notePath) {\n appendCheckpoint(\n notePath,\n `Topic shift detected: conversation moved from **${result.currentProject}** ` +\n `to **${result.suggestedProject}** (confidence: ${(result.confidence * 100).toFixed(0)}%). ` +\n `A new session note will be created for the new topic.`\n );\n }\n } catch (e) {\n process.stderr.write(`[topic-detect] Could not append checkpoint: ${e}\\n`);\n }\n }\n\n process.stderr.write(\"[topic-detect] Done.\\n\");\n}\n","/**\n * work-queue-worker.ts — Daemon worker loop for the persistent work queue\n *\n * Runs every 5 seconds to drain the queue.\n * Handles 'session-end' work items by reading the transcript, extracting\n * work summaries, updating the session note, and updating TODO.md.\n * Handles 'session-summary' items by spawning Haiku for AI-powered note generation.\n * Handles 'context-handover' items by spawning Sonnet to write the\n * threshold-triggered pre-compaction reasoning handover (see\n * context-handover-worker.ts and hooks/ts/lib/context-fill.ts).\n * Handles 'registry-scan' items by running performScan() against the registry DB.\n *\n * Handles 'topic-detect' items by running BM25-based topic shift detection.\n * Other item types (note-update, todo-update) are stubs — they log and\n * complete immediately, ready for future expansion.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { basename, dirname } from \"node:path\";\n\nimport {\n dequeue,\n enqueue,\n markCompleted,\n markFailed,\n cleanup,\n getStats,\n hasPendingOrProcessingOfType,\n type WorkItem,\n} from \"./work-queue.js\";\n\nimport {\n handleSessionSummary,\n type SessionSummaryPayload,\n} from \"./session-summary-worker.js\";\n\nimport {\n handleContextHandover,\n type ContextHandoverPayload,\n} from \"./context-handover-worker.js\";\n\nimport {\n handleTopicDetect,\n type TopicDetectPayload,\n} from \"./topic-detect-worker.js\";\n\n// Registry scan — called from handleRegistryScan()\nimport { performScan } from \"../cli/commands/registry/scan.js\";\nimport { registryBackend } from \"./daemon/state.js\";\n\n// Hooks lib imports — resolving through the compiled JS path.\n// These are the same utilities used by stop-hook.ts.\nimport {\n findNotesDir,\n getCurrentNotePath,\n addWorkToSessionNote,\n finalizeSessionNote,\n updateTodoContinue, sessionIdFromTranscript,\n archiveSessionFilesToSessionsDir,\n type WorkItem as NoteWorkItem,\n} from \"../hooks/ts/lib/project-utils/index.js\";\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\nconst WORKER_INTERVAL_MS = 5_000;\nconst HOUSEKEEPING_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes\n\n// ---------------------------------------------------------------------------\n// Timers (stored so shutdown can clear them)\n// ---------------------------------------------------------------------------\n\nlet workerTimer: ReturnType<typeof setInterval> | null = null;\nlet housekeepingTimer: ReturnType<typeof setInterval> | null = null;\nlet _immediateSignal = false;\n\n// ---------------------------------------------------------------------------\n// Worker loop\n// ---------------------------------------------------------------------------\n\n/** Start the background worker and housekeeping timers. */\nexport function startWorker(): void {\n process.stderr.write(\"[work-queue-worker] Starting worker loop.\\n\");\n\n workerTimer = setInterval(async () => {\n try {\n await processNextItem();\n } catch (e) {\n process.stderr.write(`[work-queue-worker] Uncaught error in worker loop: ${e}\\n`);\n }\n }, WORKER_INTERVAL_MS);\n\n housekeepingTimer = setInterval(() => {\n try {\n cleanup();\n } catch (e) {\n process.stderr.write(`[work-queue-worker] Housekeeping error: ${e}\\n`);\n }\n }, HOUSEKEEPING_INTERVAL_MS);\n\n process.stderr.write(\"[work-queue-worker] Worker started (interval=5s, housekeeping=10min).\\n\");\n}\n\n/** Stop the worker timers gracefully. */\nexport function stopWorker(): void {\n if (workerTimer !== null) {\n clearInterval(workerTimer);\n workerTimer = null;\n }\n if (housekeepingTimer !== null) {\n clearInterval(housekeepingTimer);\n housekeepingTimer = null;\n }\n process.stderr.write(\"[work-queue-worker] Worker stopped.\\n\");\n}\n\n/**\n * Signal that new work has been enqueued.\n * The worker will run on its next tick — we don't need to reset the timer\n * since 5 s is fast enough. The flag allows future optimisations.\n */\nexport function notifyNewWork(): void {\n _immediateSignal = true;\n}\n\n// ---------------------------------------------------------------------------\n// Item processor (sequential — one item per tick)\n// ---------------------------------------------------------------------------\n\nasync function processNextItem(): Promise<void> {\n const item = dequeue();\n if (!item) return;\n\n process.stderr.write(\n `[work-queue-worker] Processing ${item.type} (id=${item.id}, attempt=${item.attempts}).\\n`\n );\n\n try {\n switch (item.type) {\n case \"session-end\":\n await handleSessionEnd(item);\n break;\n\n case \"session-summary\":\n await handleSessionSummary(item.payload as unknown as SessionSummaryPayload);\n break;\n\n case \"context-handover\":\n await handleContextHandover(item.payload as unknown as ContextHandoverPayload);\n break;\n\n case \"topic-detect\":\n await handleTopicDetect(item.payload as unknown as TopicDetectPayload);\n break;\n\n case \"registry-scan\":\n await handleRegistryScan();\n break;\n\n case \"note-update\":\n case \"todo-update\":\n // Stubs — log and complete\n process.stderr.write(\n `[work-queue-worker] Item type '${item.type}' is not yet implemented — completing as no-op.\\n`\n );\n break;\n\n default:\n throw new Error(`Unknown work item type: ${(item as WorkItem).type}`);\n }\n\n markCompleted(item.id);\n process.stderr.write(`[work-queue-worker] Completed ${item.type} (id=${item.id}).\\n`);\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n markFailed(item.id, msg);\n }\n}\n\n// ---------------------------------------------------------------------------\n// session-end handler\n// ---------------------------------------------------------------------------\n\n/**\n * Process a 'session-end' work item.\n *\n * Expected payload:\n * transcriptPath: string — absolute path to the .jsonl transcript\n * cwd: string — working directory of the session\n * message?: string — COMPLETED: line extracted by the hook (optional)\n */\nasync function handleSessionEnd(item: WorkItem): Promise<void> {\n const { transcriptPath, cwd, message: hookMessage } = item.payload as {\n transcriptPath: string;\n cwd: string;\n message?: string;\n };\n\n if (!transcriptPath) throw new Error(\"session-end payload missing transcriptPath\");\n if (!cwd) throw new Error(\"session-end payload missing cwd\");\n\n // Read transcript\n let transcript: string;\n try {\n transcript = readFileSync(transcriptPath, \"utf-8\");\n } catch (e) {\n throw new Error(`Could not read transcript at ${transcriptPath}: ${e}`);\n }\n\n const lines = transcript.trim().split(\"\\n\");\n\n // Extract work items from transcript\n const workItems = extractWorkFromTranscript(lines);\n\n // Determine completion message\n let message = hookMessage ?? \"\";\n if (!message) {\n const lastEntry = tryParseJson(lines[lines.length - 1]) as\n | { type?: string; message?: { content?: unknown } }\n | null;\n if (lastEntry?.type === \"assistant\" && lastEntry.message?.content) {\n const content = contentToText(lastEntry.message.content);\n const m = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (m) {\n message = m[1].trim().replace(/\\*+/g, \"\").replace(/\\[.*?\\]/g, \"\").trim();\n }\n }\n }\n\n // Find notes directory and current note\n const notesInfo = findNotesDir(cwd);\n const currentNotePath = getCurrentNotePath(notesInfo.path);\n\n if (currentNotePath) {\n // Add work items to session note\n if (workItems.length > 0) {\n addWorkToSessionNote(currentNotePath, workItems);\n process.stderr.write(\n `[work-queue-worker] Added ${workItems.length} work item(s) to note.\\n`\n );\n } else if (message) {\n addWorkToSessionNote(currentNotePath, [{ title: message, completed: true }]);\n process.stderr.write(\"[work-queue-worker] Added completion message to note.\\n\");\n }\n\n // Finalize the note\n const summary = message || \"Session completed.\";\n finalizeSessionNote(currentNotePath, summary);\n process.stderr.write(\n `[work-queue-worker] Finalized session note: ${basename(currentNotePath)}.\\n`\n );\n\n // Update TODO.md ## Continue section.\n //\n // A session that produced neither work items nor a completion message has\n // no handover to write. Claiming the slot anyway hands the next session a\n // block naming a session that did nothing — see the 2026-08-04 incident\n // recorded in AIBroker's Notes. applyContinue now refuses such a write on\n // its own; this skips building it in the first place, so the \"last session\"\n // line keeps pointing at the last session that actually did something.\n if (workItems.length === 0 && !message) {\n process.stderr.write(\n \"[work-queue-worker] Nothing to hand over — leaving TODO.md ## Continue intact.\\n\"\n );\n } else {\n try {\n const stateLines: string[] = [];\n stateLines.push(`Working directory: ${cwd}`);\n if (workItems.length > 0) {\n stateLines.push(\"\", \"Work completed:\");\n for (const wi of workItems.slice(0, 5)) {\n stateLines.push(`- ${wi.title}`);\n }\n }\n if (message) {\n stateLines.push(\"\", `Last completed: ${message}`);\n }\n updateTodoContinue(\n cwd,\n basename(currentNotePath),\n stateLines.join(\"\\n\"),\n \"session-end\",\n // Same reasoning as the stop hook: the transcript filename is the\n // session's real identity, and the note title is not.\n sessionIdFromTranscript(transcriptPath)\n );\n } catch (todoError) {\n // Non-fatal — log and continue\n process.stderr.write(\n `[work-queue-worker] Could not update TODO.md: ${todoError}\\n`\n );\n }\n }\n } else {\n process.stderr.write(\n \"[work-queue-worker] No current session note found — skipping note update.\\n\"\n );\n }\n\n // Move session .jsonl files to sessions/ subdirectory\n try {\n const transcriptDir = dirname(transcriptPath);\n const archivedCount = archiveSessionFilesToSessionsDir(transcriptDir);\n if (archivedCount > 0) {\n process.stderr.write(\n `[work-queue-worker] Archived ${archivedCount} session file(s) to sessions/.\\n`\n );\n }\n } catch (moveError) {\n // Non-fatal\n process.stderr.write(`[work-queue-worker] Could not move session files: ${moveError}\\n`);\n }\n}\n\n// ---------------------------------------------------------------------------\n// registry-scan handler\n// ---------------------------------------------------------------------------\n\n/**\n * Run performScan() against the registry DB.\n *\n * Debounce: if another registry-scan job is already pending in the queue,\n * we still run this one (dequeue already picked it), but enqueueRegistryScan()\n * checks before enqueuing so duplicates rarely reach here.\n */\nasync function handleRegistryScan(): Promise<void> {\n if (!registryBackend) {\n throw new Error(\"registry-scan: registryBackend not initialized yet\");\n }\n\n const t0 = Date.now();\n process.stderr.write(\"[work-queue-worker] Running registry scan...\\n\");\n\n const result = await performScan();\n const elapsed = Date.now() - t0;\n\n process.stderr.write(\n `[work-queue-worker] Registry scan complete: ` +\n `${result.projectsScanned} projects (${result.projectsNew} new, ${result.projectsUpdated} updated), ` +\n `${result.sessionsScanned} sessions (${result.sessionsNew} new) in ${elapsed}ms.\\n`\n );\n\n if (result.skipped.length > 0) {\n process.stderr.write(\n `[work-queue-worker] Registry scan: ${result.skipped.length} project(s) skipped (path not found).\\n`\n );\n }\n}\n\n/**\n * Enqueue a registry-scan work item — debounced.\n * If a registry-scan item is already pending or processing, skip enqueue.\n */\nexport function enqueueRegistryScan(): void {\n // Debounce: skip if already queued or processing\n if (hasPendingOrProcessingOfType(\"registry-scan\")) {\n process.stderr.write(\"[work-queue-worker] Registry scan already pending/processing — skipping duplicate enqueue.\\n\");\n return;\n }\n\n enqueue({\n type: \"registry-scan\",\n priority: 5, // lowest priority — runs after session-end, session-summary\n payload: {},\n });\n}\n\n// ---------------------------------------------------------------------------\n// Transcript parsing helpers (mirrors stop-hook.ts logic)\n// ---------------------------------------------------------------------------\n\nfunction tryParseJson(line: string): Record<string, unknown> | null {\n try {\n return JSON.parse(line) as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\nfunction contentToText(content: unknown): string {\n if (typeof content === \"string\") return content;\n if (Array.isArray(content)) {\n return content\n .map((c) => {\n if (typeof c === \"string\") return c;\n const block = c as Record<string, unknown>;\n if (block?.text) return String(block.text);\n if (block?.content) return String(block.content);\n return \"\";\n })\n .join(\" \")\n .trim();\n }\n return \"\";\n}\n\nfunction extractWorkFromTranscript(lines: string[]): NoteWorkItem[] {\n const workItems: NoteWorkItem[] = [];\n const seenSummaries = new Set<string>();\n\n for (const line of lines) {\n const entry = tryParseJson(line);\n if (!entry || entry.type !== \"assistant\") continue;\n\n const msg = entry.message as Record<string, unknown> | undefined;\n if (!msg?.content) continue;\n\n const content = contentToText(msg.content);\n\n // SUMMARY: line (preferred)\n const summaryMatch = content.match(/SUMMARY:\\s*(.+?)(?:\\n|$)/i);\n if (summaryMatch) {\n const summary = summaryMatch[1].trim();\n if (summary && !seenSummaries.has(summary) && summary.length > 5) {\n seenSummaries.add(summary);\n\n const details: string[] = [];\n const actionsMatch = content.match(/ACTIONS:\\s*(.+?)(?=\\n[A-Z]+:|$)/is);\n if (actionsMatch) {\n const actionLines = actionsMatch[1]\n .split(\"\\n\")\n .map((l) =>\n l.replace(/^[-*•]\\s*/, \"\").replace(/^\\d+\\.\\s*/, \"\").trim()\n )\n .filter((l) => l.length > 3 && l.length < 100);\n details.push(...actionLines.slice(0, 3));\n }\n\n workItems.push({\n title: summary,\n details: details.length > 0 ? details : undefined,\n completed: true,\n });\n }\n }\n\n // COMPLETED: line (fallback)\n const completedMatch = content.match(/COMPLETED:\\s*(.+?)(?:\\n|$)/i);\n if (completedMatch && workItems.length === 0) {\n const completed = completedMatch[1]\n .trim()\n .replace(/\\*+/g, \"\")\n .replace(/\\[.*?\\]/g, \"\")\n .trim();\n if (completed && !seenSummaries.has(completed) && completed.length > 5) {\n seenSummaries.add(completed);\n workItems.push({ title: completed, completed: true });\n }\n }\n }\n\n return workItems;\n}\n"],"mappings":"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"}
@@ -1,11 +0,0 @@
1
- import "./utils-C9HsYDpO.mjs";
2
- import "./migrate-BD7D8EEh.mjs";
3
- import "./factory-CNpPQQ-5.mjs";
4
- import "./context-handover-cache-pkHzmL3c.mjs";
5
- import "./config-BbLFD7Uf.mjs";
6
- import "./run-env-DRcp7A8K.mjs";
7
- import "./server-DgmAHyFK.mjs";
8
- import "./state-HyjqTihC.mjs";
9
- import { i as stopWorker, n as notifyNewWork, r as startWorker, t as enqueueRegistryScan } from "./work-queue-worker-4tLa5gpQ.mjs";
10
-
11
- export { enqueueRegistryScan };
@@ -1 +0,0 @@
1
- {"version":3,"file":"zettelkasten-CtGHQWPU.mjs","names":["MAX_CHUNKS"],"sources":["../src/zettelkasten/explore.ts","../src/zettelkasten/surprise.ts","../src/zettelkasten/converse.ts","../src/zettelkasten/health.ts","../src/zettelkasten/suggest.ts","../src/zettelkasten/god-notes.ts","../src/zettelkasten/communities.ts"],"sourcesContent":["import type { StorageBackend } from \"../storage/interface.js\";\nimport { dirname } from \"node:path\";\n\nexport interface ExploreOptions {\n startNote: string;\n depth?: number;\n direction?: \"forward\" | \"backward\" | \"both\";\n mode?: \"sequential\" | \"associative\" | \"all\";\n}\n\nexport interface ExploreNode {\n path: string;\n title: string | null;\n depth: number;\n linkType: \"sequential\" | \"associative\";\n inbound: number;\n outbound: number;\n}\n\nexport interface ExploreResult {\n root: string;\n nodes: ExploreNode[];\n edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }>;\n branchingPoints: string[];\n maxDepthReached: boolean;\n}\n\nfunction classifyEdge(source: string, target: string): \"sequential\" | \"associative\" {\n return dirname(source) === dirname(target) ? \"sequential\" : \"associative\";\n}\n\nasync function resolveStart(backend: StorageBackend, startNote: string): Promise<string | null> {\n // Try direct lookup first\n const files = await backend.getVaultFilesByPaths([startNote]);\n if (files.length > 0) return files[0].vaultPath;\n\n // Try alias lookup\n const alias = await backend.getVaultAlias(startNote);\n if (!alias) return null;\n\n const canonical = await backend.getVaultFilesByPaths([alias.canonicalPath]);\n return canonical.length > 0 ? canonical[0].vaultPath : null;\n}\n\nasync function getForwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksFromSource(path);\n return links.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n}\n\nasync function getBackwardNeighbors(backend: StorageBackend, path: string): Promise<string[]> {\n const links = await backend.getLinksToTarget(path);\n return links.map(l => l.sourcePath);\n}\n\nasync function getFileInfo(\n backend: StorageBackend,\n path: string,\n): Promise<{ title: string | null; inbound: number; outbound: number }> {\n const [files, health] = await Promise.all([\n backend.getVaultFilesByPaths([path]),\n backend.getVaultHealth(path),\n ]);\n\n return {\n title: files[0]?.title ?? null,\n inbound: health?.inboundCount ?? 0,\n outbound: health?.outboundCount ?? 0,\n };\n}\n\n/**\n * Traverse the Zettelkasten link graph using BFS, following chains of thought\n * from a starting note up to a configurable depth.\n */\nexport async function zettelExplore(backend: StorageBackend, opts: ExploreOptions): Promise<ExploreResult> {\n const depth = Math.min(Math.max(opts.depth ?? 3, 1), 10);\n const direction = opts.direction ?? \"both\";\n const mode = opts.mode ?? \"all\";\n\n const root = await resolveStart(backend, opts.startNote);\n if (!root) {\n return {\n root: opts.startNote,\n nodes: [],\n edges: [],\n branchingPoints: [],\n maxDepthReached: false,\n };\n }\n\n const visited = new Set<string>([root]);\n const nodes: ExploreNode[] = [];\n const edges: Array<{ from: string; to: string; type: \"sequential\" | \"associative\" }> = [];\n let maxDepthReached = false;\n\n const queue: Array<{ path: string; depth: number }> = [{ path: root, depth: 0 }];\n\n while (queue.length > 0) {\n const current = queue.shift()!;\n\n if (current.depth >= depth) {\n maxDepthReached = true;\n continue;\n }\n\n const neighbors: Array<{ neighbor: string; from: string; to: string }> = [];\n\n if (direction === \"forward\" || direction === \"both\") {\n for (const n of await getForwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: current.path, to: n });\n }\n }\n\n if (direction === \"backward\" || direction === \"both\") {\n for (const n of await getBackwardNeighbors(backend, current.path)) {\n neighbors.push({ neighbor: n, from: n, to: current.path });\n }\n }\n\n for (const { neighbor, from, to } of neighbors) {\n const edgeType = classifyEdge(from, to);\n\n if (mode !== \"all\" && edgeType !== mode) {\n continue;\n }\n\n const alreadyHasEdge = edges.some((e) => e.from === from && e.to === to);\n if (!alreadyHasEdge) {\n edges.push({ from, to, type: edgeType });\n }\n\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n\n const info = await getFileInfo(backend, neighbor);\n nodes.push({\n path: neighbor,\n title: info.title,\n depth: current.depth + 1,\n linkType: edgeType,\n inbound: info.inbound,\n outbound: info.outbound,\n });\n\n queue.push({ path: neighbor, depth: current.depth + 1 });\n }\n }\n }\n\n const branchingPoints = nodes\n .filter((n) => n.outbound > 2)\n .map((n) => n.path);\n\n const rootInfo = await getFileInfo(backend, root);\n if (rootInfo.outbound > 2) {\n branchingPoints.unshift(root);\n }\n\n return { root, nodes, edges, branchingPoints, maxDepthReached };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport {\n deserializeEmbedding,\n generateEmbedding,\n cosineSimilarity,\n} from \"../memory/embeddings.js\";\n\nexport interface SurpriseOptions {\n referencePath: string;\n vaultProjectId: number;\n limit?: number;\n minSimilarity?: number;\n minGraphDistance?: number;\n}\n\nexport interface SurpriseResult {\n path: string;\n title: string | null;\n cosineSimilarity: number;\n graphDistance: number;\n surpriseScore: number;\n sharedSnippet: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst BFS_HOP_CAP = 20;\n\nasync function getFileEmbeddings(\n backend: StorageBackend,\n projectId: number,\n): Promise<Map<string, { embedding: Float32Array; text: string }>> {\n const rows = await backend.getChunksWithEmbeddings(projectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number; text: string }>();\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1, text: row.text });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const result = new Map<string, { embedding: Float32Array; text: string }>();\n for (const [path, { sum, count, text }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n result.set(path, { embedding: avg, text });\n }\n return result;\n}\n\nasync function getReferenceEmbedding(\n backend: StorageBackend,\n projectId: number,\n path: string,\n): Promise<{ embedding: Float32Array; found: boolean }> {\n const rows = await backend.getChunksForPath(projectId, path);\n\n if (rows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const embRows = rows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (embRows.length === 0) {\n return { embedding: new Float32Array(0), found: false };\n }\n\n const dim = deserializeEmbedding(embRows[0].embedding).length;\n const sum = new Float32Array(dim);\n for (const row of embRows) {\n const vec = deserializeEmbedding(row.embedding);\n for (let i = 0; i < dim; i++) {\n sum[i] += vec[i];\n }\n }\n const avg = new Float32Array(dim);\n for (let i = 0; i < dim; i++) {\n avg[i] = sum[i] / embRows.length;\n }\n return { embedding: avg, found: true };\n}\n\nasync function bfsGraphDistance(backend: StorageBackend, source: string, target: string): Promise<number> {\n if (source === target) return 0;\n\n const visited = new Set<string>([source]);\n const queue: Array<{ path: string; hops: number }> = [{ path: source, hops: 0 }];\n\n while (queue.length > 0) {\n const { path, hops } = queue.shift()!;\n if (hops >= BFS_HOP_CAP) continue;\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getLinksFromSource(path),\n backend.getLinksToTarget(path),\n ]);\n\n const neighbors: string[] = [\n ...forwardLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string),\n ...backwardLinks.map(l => l.sourcePath),\n ];\n\n for (const neighbor of neighbors) {\n if (neighbor === target) return hops + 1;\n if (!visited.has(neighbor)) {\n visited.add(neighbor);\n queue.push({ path: neighbor, hops: hops + 1 });\n }\n }\n }\n\n return Infinity;\n}\n\nfunction getBestChunkText(\n chunkRows: Array<{ text: string; embedding: Buffer | null }>,\n refEmbedding: Float32Array,\n): string {\n const rows = chunkRows.filter(r => r.embedding !== null) as Array<{ text: string; embedding: Buffer }>;\n if (rows.length === 0) return \"\";\n\n let bestText = rows[0].text;\n let bestSim = -Infinity;\n\n for (const row of rows) {\n const vec = deserializeEmbedding(row.embedding);\n const sim = cosineSimilarity(refEmbedding, vec);\n if (sim > bestSim) {\n bestSim = sim;\n bestText = row.text;\n }\n }\n\n return bestText.trim().slice(0, 200);\n}\n\n/**\n * Find notes that are semantically similar to a reference note but graph-distant —\n * revealing surprising conceptual connections across unrelated areas of the Zettelkasten.\n */\nexport async function zettelSurprise(\n backend: StorageBackend,\n opts: SurpriseOptions,\n): Promise<SurpriseResult[]> {\n const limit = opts.limit ?? 10;\n const minSimilarity = opts.minSimilarity ?? 0.3;\n const minGraphDistance = opts.minGraphDistance ?? 3;\n\n let { embedding: refEmbedding, found } = await getReferenceEmbedding(\n backend,\n opts.vaultProjectId,\n opts.referencePath,\n );\n\n // Fall back to generating an embedding from the file title if no chunks exist\n if (!found) {\n const files = await backend.getVaultFilesByPaths([opts.referencePath]);\n const text = files[0]?.title ?? opts.referencePath;\n refEmbedding = await generateEmbedding(text, true);\n }\n\n const allFileEmbeddings = await getFileEmbeddings(backend, opts.vaultProjectId);\n\n // Remove the reference note itself from candidates\n allFileEmbeddings.delete(opts.referencePath);\n\n // First pass: filter by semantic similarity to avoid BFS on all nodes\n const semanticCandidates: Array<{ path: string; sim: number }> = [];\n for (const [path, { embedding }] of allFileEmbeddings) {\n const sim = cosineSimilarity(refEmbedding, embedding);\n if (sim >= minSimilarity) {\n semanticCandidates.push({ path, sim });\n }\n }\n\n // Compute graph distances for semantic candidates\n const results: SurpriseResult[] = [];\n\n for (const { path, sim } of semanticCandidates) {\n const graphDistance = await bfsGraphDistance(backend, opts.referencePath, path);\n\n const effectiveDistance = isFinite(graphDistance) ? graphDistance : BFS_HOP_CAP;\n if (effectiveDistance < minGraphDistance) continue;\n\n const files = await backend.getVaultFilesByPaths([path]);\n const chunkRows = await backend.getChunksForPath(opts.vaultProjectId, path, 20);\n\n const surpriseScore = sim * Math.log2(effectiveDistance + 1);\n const sharedSnippet = getBestChunkText(chunkRows, refEmbedding);\n\n results.push({\n path,\n title: files[0]?.title ?? null,\n cosineSimilarity: sim,\n graphDistance: isFinite(graphDistance) ? graphDistance : Infinity,\n surpriseScore,\n sharedSnippet,\n });\n }\n\n results.sort((a, b) => b.surpriseScore - a.surpriseScore);\n return results.slice(0, limit);\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport type { SearchResult } from \"../memory/search.js\";\nimport { generateEmbedding } from \"../memory/embeddings.js\";\n\nexport interface ConverseOptions {\n /** The user's question or topic to explore. */\n question: string;\n /** project_id for vault chunks in memory_chunks. */\n vaultProjectId: number;\n /** Graph expansion depth. Default 2. */\n depth?: number;\n /** Maximum number of relevant notes to return. Default 15. */\n limit?: number;\n}\n\nexport interface ConverseConnection {\n fromPath: string;\n toPath: string;\n /** Top-level folder of fromPath. */\n fromDomain: string;\n /** Top-level folder of toPath. */\n toDomain: string;\n /** Link count between these two notes (can be > 1). */\n strength: number;\n}\n\nexport interface ConverseResult {\n relevantNotes: Array<{\n path: string;\n title: string | null;\n snippet: string;\n score: number;\n domain: string;\n }>;\n /** Cross-domain connections found among the selected notes. */\n connections: ConverseConnection[];\n /** Unique domains involved across all selected notes. */\n domains: string[];\n /** AI-ready prompt combining notes + connections for insight generation. */\n synthesisPrompt: string;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract the top-level folder from a vault path (first path segment). */\nfunction extractDomain(vaultPath: string): string {\n const slash = vaultPath.indexOf(\"/\");\n return slash === -1 ? vaultPath : vaultPath.slice(0, slash);\n}\n\n/**\n * Expand one level of graph neighbors for a set of paths.\n * Returns all outbound and inbound neighbor paths (excluding already-visited).\n */\nasync function expandNeighbors(backend: StorageBackend, paths: Set<string>): Promise<string[]> {\n if (paths.size === 0) return [];\n const pathList = Array.from(paths);\n\n const [forwardLinks, backwardLinks] = await Promise.all([\n backend.getVaultLinksFromPaths(pathList),\n Promise.all(pathList.map(p => backend.getLinksToTarget(p))),\n ]);\n\n const neighbors: string[] = [];\n for (const link of forwardLinks) {\n if (link.targetPath) neighbors.push(link.targetPath);\n }\n for (const linkList of backwardLinks) {\n for (const link of linkList) {\n neighbors.push(link.sourcePath);\n }\n }\n return neighbors;\n}\n\n/**\n * Hybrid search combining keyword + semantic results using the StorageBackend.\n */\nasync function hybridSearch(\n backend: StorageBackend,\n query: string,\n queryEmbedding: Float32Array,\n opts: { projectIds?: number[]; maxResults?: number },\n): Promise<SearchResult[]> {\n const maxResults = opts.maxResults ?? 10;\n const kw = 0.5;\n const sw = 0.5;\n\n const [keywordResults, semanticResults] = await Promise.all([\n backend.searchKeyword(query, { ...opts, maxResults: 50 }),\n backend.searchSemantic(queryEmbedding, { ...opts, maxResults: 50 }),\n ]);\n\n if (keywordResults.length === 0 && semanticResults.length === 0) return [];\n\n const keyFor = (r: SearchResult) =>\n `${r.projectId}:${r.path}:${r.startLine}:${r.endLine}`;\n\n function minMaxNormalize(scores: number[]): number[] {\n const min = Math.min(...scores);\n const max = Math.max(...scores);\n const range = max - min;\n if (range === 0) return scores.map(() => 1.0);\n return scores.map(s => (s - min) / range);\n }\n\n const kwNorm = minMaxNormalize(keywordResults.map(r => r.score));\n const semNorm = minMaxNormalize(semanticResults.map(r => r.score));\n\n const combined = new Map<string, SearchResult & { combinedScore: number }>();\n\n for (let i = 0; i < keywordResults.length; i++) {\n const r = keywordResults[i];\n const k = keyFor(r);\n combined.set(k, { ...r, combinedScore: kw * kwNorm[i] });\n }\n\n for (let i = 0; i < semanticResults.length; i++) {\n const r = semanticResults[i];\n const k = keyFor(r);\n const existing = combined.get(k);\n if (existing) {\n existing.combinedScore += sw * semNorm[i];\n } else {\n combined.set(k, { ...r, combinedScore: sw * semNorm[i] });\n }\n }\n\n const sorted = Array.from(combined.values())\n .sort((a, b) => b.combinedScore - a.combinedScore)\n .slice(0, maxResults);\n\n return sorted.map(r => ({ ...r, score: r.combinedScore }));\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Let the vault \"talk back\" — find notes relevant to a question, expand\n * through the link graph, identify cross-domain connections, and return a\n * structured result including a synthesis prompt for an AI to generate insights.\n */\nexport async function zettelConverse(\n backend: StorageBackend,\n opts: ConverseOptions,\n): Promise<ConverseResult> {\n const depth = Math.max(opts.depth ?? 2, 0);\n const limit = Math.max(opts.limit ?? 15, 1);\n const candidateLimit = 20;\n\n // ------------------------------------------------------------------\n // 1. Hybrid search: find top candidates via BM25 + semantic similarity\n // ------------------------------------------------------------------\n const queryEmbedding = await generateEmbedding(opts.question, true);\n\n const searchResults = await hybridSearch(\n backend,\n opts.question,\n queryEmbedding,\n {\n projectIds: [opts.vaultProjectId],\n maxResults: candidateLimit,\n },\n );\n\n // Map of path -> best score + snippet from search results\n const searchHits = new Map<string, { score: number; snippet: string }>();\n for (const r of searchResults) {\n const existing = searchHits.get(r.path);\n if (!existing || r.score > existing.score) {\n searchHits.set(r.path, { score: r.score, snippet: r.snippet });\n }\n }\n\n // ------------------------------------------------------------------\n // 2. Graph expansion: BFS from each search result up to `depth` levels\n // ------------------------------------------------------------------\n const allPaths = new Set<string>(searchHits.keys());\n let frontier = new Set<string>(searchHits.keys());\n\n for (let d = 0; d < depth; d++) {\n const neighbors = await expandNeighbors(backend, frontier);\n const newFrontier = new Set<string>();\n for (const n of neighbors) {\n if (!allPaths.has(n)) {\n allPaths.add(n);\n newFrontier.add(n);\n }\n }\n if (newFrontier.size === 0) break;\n frontier = newFrontier;\n }\n\n // ------------------------------------------------------------------\n // 3. Deduplicate + trim to limit\n // ------------------------------------------------------------------\n const searchRanked = Array.from(searchHits.entries())\n .sort((a, b) => b[1].score - a[1].score)\n .map(([path, info]) => ({ path, ...info, isSearchResult: true }));\n\n const neighborPaths = Array.from(allPaths).filter((p) => !searchHits.has(p));\n\n // Fetch health data for neighbor ranking\n const neighborHealthRows = await Promise.all(\n neighborPaths.map(p => backend.getVaultHealth(p))\n );\n const neighborRanked = neighborPaths\n .map((path, idx) => ({\n path,\n score: 0,\n snippet: \"\",\n inbound: neighborHealthRows[idx]?.inboundCount ?? 0,\n isSearchResult: false,\n }))\n .sort((a, b) => b.inbound - a.inbound);\n\n const budgetForNeighbors = Math.max(limit - searchRanked.length, 0);\n const selectedNeighbors = neighborRanked.slice(0, budgetForNeighbors);\n\n const selectedSearchPaths = searchRanked.slice(0, limit);\n const selectedPaths = new Set<string>([\n ...selectedSearchPaths.map((r) => r.path),\n ...selectedNeighbors.map((r) => r.path),\n ]);\n\n // ------------------------------------------------------------------\n // 4. Build relevantNotes with titles + domains\n // ------------------------------------------------------------------\n\n // Fetch titles in bulk\n const allSelectedPaths = Array.from(selectedPaths);\n const fileRows = await backend.getVaultFilesByPaths(allSelectedPaths);\n const titleMap = new Map<string, string | null>(fileRows.map(f => [f.vaultPath, f.title]));\n\n const relevantNotes: ConverseResult[\"relevantNotes\"] = [];\n\n for (const r of selectedSearchPaths) {\n if (!selectedPaths.has(r.path)) continue;\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: r.score,\n domain: extractDomain(r.path),\n });\n }\n\n for (const r of selectedNeighbors) {\n relevantNotes.push({\n path: r.path,\n title: titleMap.get(r.path) ?? null,\n snippet: r.snippet,\n score: 0,\n domain: extractDomain(r.path),\n });\n }\n\n // ------------------------------------------------------------------\n // 5. Find connections between the selected notes\n // ------------------------------------------------------------------\n let connections: ConverseConnection[] = [];\n\n if (selectedPaths.size > 0) {\n const pathList = Array.from(selectedPaths);\n const pathSet = new Set(pathList);\n\n // Get all outbound links from the selected paths\n const linkRows = await backend.getVaultLinksFromPaths(pathList);\n\n // Count links between selected paths\n const edgeCounts = new Map<string, number>();\n for (const link of linkRows) {\n if (link.targetPath && pathSet.has(link.targetPath)) {\n const key = `${link.sourcePath}|||${link.targetPath}`;\n edgeCounts.set(key, (edgeCounts.get(key) ?? 0) + 1);\n }\n }\n\n for (const [key, cnt] of edgeCounts) {\n const [sourcePath, targetPath] = key.split(\"|||\");\n connections.push({\n fromPath: sourcePath,\n toPath: targetPath,\n fromDomain: extractDomain(sourcePath),\n toDomain: extractDomain(targetPath),\n strength: cnt,\n });\n }\n }\n\n // ------------------------------------------------------------------\n // 6. Domains + cross-domain filter\n // ------------------------------------------------------------------\n const domainSet = new Set<string>(relevantNotes.map((n) => n.domain));\n const domains = Array.from(domainSet).sort();\n\n const crossDomainConnections = connections.filter(\n (c) => c.fromDomain !== c.toDomain,\n );\n\n // ------------------------------------------------------------------\n // 7. Build synthesis prompt\n // ------------------------------------------------------------------\n const notesSummary = relevantNotes\n .map((n, i) => {\n const title = n.title ? `\"${n.title}\"` : \"(untitled)\";\n const domain = n.domain;\n const scoreLabel = n.score > 0 ? ` [relevance: ${n.score.toFixed(3)}]` : \" [context]\";\n const snippet = n.snippet.trim().slice(0, 300);\n return `${i + 1}. [${domain}] ${title}${scoreLabel}\\n Path: ${n.path}\\n \"${snippet}\"`;\n })\n .join(\"\\n\\n\");\n\n const connectionSummary =\n crossDomainConnections.length > 0\n ? crossDomainConnections\n .map(\n (c) =>\n `- \"${c.fromPath}\" (${c.fromDomain}) → \"${c.toPath}\" (${c.toDomain}) [strength: ${c.strength}]`,\n )\n .join(\"\\n\")\n : \"(no cross-domain connections found)\";\n\n const domainList = domains.join(\", \");\n\n const synthesisPrompt = `You are a Zettelkasten research assistant. The vault has surfaced the following notes in response to this question:\n\nQUESTION: ${opts.question}\n\n---\n\nRELEVANT NOTES (${relevantNotes.length} notes across ${domains.length} domain(s): ${domainList}):\n\n${notesSummary}\n\n---\n\nCROSS-DOMAIN CONNECTIONS (links bridging different knowledge areas):\n\n${connectionSummary}\n\n---\n\nSYNTHESIS TASK:\n\nBased on these notes and the connections between them, please:\n\n1. Identify the key insights that emerge in direct response to the question.\n2. Highlight any unexpected connections between notes from different domains (${domainList}).\n3. Point out tensions, contradictions, or open questions the vault raises but does not resolve.\n4. Suggest what is notably absent — what the vault does NOT yet contain that would strengthen the understanding of this topic.\n5. Propose 2-3 new notes that would meaningfully extend this knowledge cluster.\n\nThink like a scholar who has deeply internalized these ideas and is now synthesizing them for the first time.`;\n\n return {\n relevantNotes,\n connections: crossDomainConnections,\n domains,\n synthesisPrompt,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface HealthOptions {\n scope?: \"full\" | \"recent\" | \"project\";\n projectPath?: string;\n recentDays?: number;\n include?: Array<\"dead_links\" | \"orphans\" | \"disconnected\" | \"low_connectivity\">;\n}\n\nexport interface DeadLink {\n sourcePath: string;\n targetRaw: string;\n lineNumber: number;\n}\n\nexport interface HealthResult {\n totalFiles: number;\n totalLinks: number;\n deadLinks: DeadLink[];\n orphans: string[];\n disconnectedClusters: number;\n lowConnectivity: string[];\n healthScore: number;\n computedAt: number;\n /** Breakdown of links by confidence level (if confidence tagging is active). */\n linkConfidence?: {\n extracted: number;\n inferred: number;\n ambiguous: number;\n };\n}\n\nfunction countComponents(nodes: string[], edges: Array<{ source: string; target: string }>): number {\n if (nodes.length === 0) return 0;\n\n const parent = new Map<string, string>();\n const rank = new Map<string, number>();\n\n for (const n of nodes) {\n parent.set(n, n);\n rank.set(n, 0);\n }\n\n function find(x: string): string {\n let root = x;\n while (parent.get(root) !== root) {\n root = parent.get(root)!;\n }\n let current = x;\n while (current !== root) {\n const next = parent.get(current)!;\n parent.set(current, root);\n current = next;\n }\n return root;\n }\n\n function union(a: string, b: string): void {\n const ra = find(a);\n const rb = find(b);\n if (ra === rb) return;\n const rankA = rank.get(ra) ?? 0;\n const rankB = rank.get(rb) ?? 0;\n if (rankA < rankB) {\n parent.set(ra, rb);\n } else if (rankA > rankB) {\n parent.set(rb, ra);\n } else {\n parent.set(rb, ra);\n rank.set(ra, rankA + 1);\n }\n }\n\n for (const { source, target } of edges) {\n if (parent.has(source) && parent.has(target)) {\n union(source, target);\n }\n }\n\n const roots = new Set<string>();\n for (const n of nodes) {\n roots.add(find(n));\n }\n return roots.size;\n}\n\n/**\n * Audit the structural health of the Zettelkasten vault using graph metrics.\n */\nexport async function zettelHealth(backend: StorageBackend, opts?: HealthOptions): Promise<HealthResult> {\n const options = opts ?? {};\n const scope = options.scope ?? \"full\";\n const include = options.include ?? [\"dead_links\", \"orphans\", \"disconnected\", \"low_connectivity\"];\n\n const computedAt = Date.now();\n\n // --- totalFiles ---\n let totalFiles = 0;\n if (scope === \"full\") {\n totalFiles = await backend.countVaultFiles();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalFiles = await backend.countVaultFilesWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalFiles = await backend.countVaultFilesAfter(cutoff);\n }\n\n // --- totalLinks ---\n let totalLinks = 0;\n if (scope === \"full\") {\n // Count total links via link graph length\n const graph = await backend.getVaultLinkGraph();\n totalLinks = graph.length;\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n totalLinks = await backend.countVaultLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n totalLinks = await backend.countVaultLinksAfter(cutoff);\n }\n\n // --- deadLinks ---\n let deadLinks: DeadLink[] = [];\n if (include.includes(\"dead_links\")) {\n if (scope === \"full\") {\n deadLinks = await backend.getDeadLinksWithLineNumbers();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n deadLinks = await backend.getDeadLinksWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n deadLinks = await backend.getDeadLinksAfter(cutoff);\n }\n }\n\n // --- orphans ---\n let orphans: string[] = [];\n if (include.includes(\"orphans\")) {\n if (scope === \"full\") {\n const orphanRows = await backend.getOrphans();\n orphans = orphanRows.map(r => r.vaultPath);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n orphans = await backend.getOrphansWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n orphans = await backend.getOrphansAfter(cutoff);\n }\n }\n\n // --- disconnectedClusters (union-find) ---\n let disconnectedClusters = 1;\n if (include.includes(\"disconnected\")) {\n let allNodes: string[];\n let allEdges: Array<{ source: string; target: string }>;\n\n if (scope === \"full\") {\n [allNodes, allEdges] = await Promise.all([\n backend.getAllVaultFilePaths(),\n backend.getVaultLinkEdges(),\n ]);\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsWithPrefix(prefix),\n backend.getVaultLinkEdgesWithPrefix(prefix),\n ]);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n [allNodes, allEdges] = await Promise.all([\n backend.getVaultFilePathsAfter(cutoff),\n backend.getVaultLinkEdgesAfter(cutoff),\n ]);\n }\n\n disconnectedClusters = countComponents(allNodes, allEdges);\n }\n\n // --- lowConnectivity ---\n let lowConnectivity: string[] = [];\n if (include.includes(\"low_connectivity\")) {\n if (scope === \"full\") {\n lowConnectivity = await backend.getLowConnectivity();\n } else if (scope === \"project\") {\n const prefix = options.projectPath ?? \"\";\n lowConnectivity = await backend.getLowConnectivityWithPrefix(prefix);\n } else {\n const days = options.recentDays ?? 30;\n const cutoff = computedAt - days * 86400000;\n lowConnectivity = await backend.getLowConnectivityAfter(cutoff);\n }\n }\n\n // --- linkConfidence ---\n // Count links by confidence level using a sampling of all vault file paths.\n // We reuse the link graph data we already fetched for totalLinks when scope=full.\n let linkConfidence: HealthResult[\"linkConfidence\"] | undefined;\n if (scope === \"full\") {\n try {\n // Get all file paths to iterate their outbound links with confidence\n const samplePaths = await backend.getAllVaultFilePaths();\n const sampleSize = Math.min(samplePaths.length, 200); // limit for performance\n const sampled = samplePaths.slice(0, sampleSize);\n let extracted = 0;\n let inferred = 0;\n let ambiguous = 0;\n for (const path of sampled) {\n const links = await backend.getLinksFromSource(path);\n for (const link of links) {\n const c = link.confidence ?? \"EXTRACTED\";\n if (c === \"EXTRACTED\") extracted++;\n else if (c === \"INFERRED\") inferred++;\n else ambiguous++;\n }\n }\n // Extrapolate if we sampled\n const factor = samplePaths.length > 0 ? samplePaths.length / sampleSize : 1;\n linkConfidence = {\n extracted: Math.round(extracted * factor),\n inferred: Math.round(inferred * factor),\n ambiguous: Math.round(ambiguous * factor),\n };\n } catch {\n // Backend may not support confidence field yet — ignore\n }\n }\n\n // --- healthScore ---\n const deadRatio = totalLinks > 0 ? deadLinks.length / totalLinks : 0;\n const orphanRatio = totalFiles > 0 ? orphans.length / totalFiles : 0;\n const lowConnRatio = totalFiles > 0 ? lowConnectivity.length / totalFiles : 0;\n const healthScore = Math.round(\n 100 * (1 - deadRatio) * (1 - orphanRatio * 0.5) * (1 - lowConnRatio * 0.3),\n );\n\n return {\n totalFiles,\n totalLinks,\n deadLinks,\n orphans,\n disconnectedClusters,\n lowConnectivity,\n healthScore,\n computedAt,\n linkConfidence,\n };\n}\n","import type { StorageBackend } from \"../storage/interface.js\";\nimport { deserializeEmbedding, cosineSimilarity } from \"../memory/embeddings.js\";\nimport { basename } from \"node:path\";\nimport { STOP_WORDS } from \"../utils/stop-words.js\";\n\nexport interface SuggestOptions {\n notePath: string;\n vaultProjectId: number;\n limit?: number;\n excludeLinked?: boolean;\n}\n\nexport interface Suggestion {\n path: string;\n title: string | null;\n score: number;\n semanticScore: number;\n tagScore: number;\n neighborScore: number;\n reason: string;\n suggestedWikilink: string;\n}\n\nconst MAX_CHUNKS = 5000;\nconst SEMANTIC_WEIGHT = 0.5;\nconst TAG_WEIGHT = 0.2;\nconst NEIGHBOR_WEIGHT = 0.3;\n\n// STOP_WORDS imported from utils/stop-words.ts\n\nfunction extractTagsFromChunkTexts(texts: string[]): Set<string> {\n const tags = new Set<string>();\n for (const text of texts) {\n // Match YAML frontmatter tags block: \"tags:\\n - tag1\\n - tag2\"\n const match = text.match(/^tags:\\s*\\n((?:[ \\t]*-[ \\t]*.+\\n?)*)/m);\n if (!match) continue;\n const block = match[1];\n const lines = block.split(\"\\n\");\n for (const line of lines) {\n const tagMatch = line.match(/^[ \\t]*-[ \\t]*(.+)/);\n if (tagMatch) {\n const tag = tagMatch[1].trim().toLowerCase();\n if (tag) tags.add(tag);\n }\n }\n }\n return tags;\n}\n\nfunction jaccardSimilarity(a: Set<string>, b: Set<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const tag of a) {\n if (b.has(tag)) intersection++;\n }\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nfunction buildReason(\n semanticScore: number,\n tagScore: number,\n neighborScore: number,\n neighborCount: number,\n): string {\n const signals: Array<{ label: string; value: number }> = [\n { label: `Semantically similar (${semanticScore.toFixed(2)})`, value: semanticScore * SEMANTIC_WEIGHT },\n { label: `Shared tags (${tagScore.toFixed(2)} Jaccard)`, value: tagScore * TAG_WEIGHT },\n { label: `Linked by ${neighborCount} mutual connection${neighborCount !== 1 ? \"s\" : \"\"}`, value: neighborScore * NEIGHBOR_WEIGHT },\n ];\n signals.sort((a, b) => b.value - a.value);\n return signals[0].label;\n}\n\nfunction suggestedWikilink(vaultPath: string): string {\n const base = basename(vaultPath);\n const name = base.endsWith(\".md\") ? base.slice(0, -3) : base;\n return `[[${name}]]`;\n}\n\n/**\n * Proactively find notes worth linking to a given note, combining semantic similarity,\n * shared tags, and graph-neighborhood signals into a ranked list of suggestions.\n */\nexport async function zettelSuggest(\n backend: StorageBackend,\n opts: SuggestOptions,\n): Promise<Suggestion[]> {\n const limit = opts.limit ?? 5;\n const excludeLinked = opts.excludeLinked ?? true;\n\n // Step 1: get current outbound links\n const outboundLinks = await backend.getLinksFromSource(opts.notePath);\n const linkedPaths = new Set(outboundLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string));\n\n // Step 2a: get all file-level embeddings for semantic scoring\n const chunkRows = await backend.getChunksWithEmbeddings(opts.vaultProjectId, MAX_CHUNKS);\n\n const byPath = new Map<string, { sum: Float32Array; count: number }>();\n for (const row of chunkRows) {\n const vec = deserializeEmbedding(row.embedding);\n const entry = byPath.get(row.path);\n if (!entry) {\n byPath.set(row.path, { sum: new Float32Array(vec), count: 1 });\n } else {\n for (let i = 0; i < vec.length; i++) {\n entry.sum[i] += vec[i];\n }\n entry.count++;\n }\n }\n\n const allEmbeddings = new Map<string, Float32Array>();\n for (const [path, { sum, count }] of byPath) {\n const avg = new Float32Array(sum.length);\n for (let i = 0; i < sum.length; i++) {\n avg[i] = sum[i] / count;\n }\n allEmbeddings.set(path, avg);\n }\n allEmbeddings.delete(opts.notePath);\n\n // Step 2b: get source embedding\n const sourceEmbedding = allEmbeddings.get(opts.notePath) ?? null;\n\n // Step 2c: get source tags\n const sourceChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, opts.notePath, 5);\n const sourceTags = extractTagsFromChunkTexts(sourceChunkTexts.map(r => r.text));\n\n // Step 2d: compute graph neighborhood (friends-of-friends)\n const directLinks = await backend.getLinksFromSource(opts.notePath);\n const directTargets = directLinks.filter(l => l.targetPath !== null).map(l => l.targetPath as string);\n\n const friendLinkCounts = new Map<string, number>();\n for (const target of directTargets) {\n const friendLinks = await backend.getLinksFromSource(target);\n for (const link of friendLinks) {\n if (link.targetPath && link.targetPath !== opts.notePath) {\n friendLinkCounts.set(link.targetPath, (friendLinkCounts.get(link.targetPath) ?? 0) + 1);\n }\n }\n }\n const maxFriendLinks = Math.max(1, ...friendLinkCounts.values());\n\n // Get all vault files to enumerate candidates\n const allFiles = await backend.getAllVaultFiles();\n\n const suggestions: Suggestion[] = [];\n\n for (const fileRow of allFiles) {\n const vault_path = fileRow.vaultPath;\n const title = fileRow.title;\n\n if (vault_path === opts.notePath) continue;\n if (excludeLinked && linkedPaths.has(vault_path)) continue;\n\n // Semantic score\n let semanticScore = 0;\n if (sourceEmbedding) {\n const candidateEmbedding = allEmbeddings.get(vault_path);\n if (candidateEmbedding) {\n semanticScore = Math.max(0, cosineSimilarity(sourceEmbedding, candidateEmbedding));\n }\n }\n\n // Tag score (only compute if candidate might have chunks)\n let tagScore = 0;\n if (allEmbeddings.has(vault_path)) {\n const candidateChunkTexts = await backend.getChunksForPath(opts.vaultProjectId, vault_path, 5);\n const candidateTags = extractTagsFromChunkTexts(candidateChunkTexts.map(r => r.text));\n tagScore = jaccardSimilarity(sourceTags, candidateTags);\n }\n\n // Neighbor score\n const friendCount = friendLinkCounts.get(vault_path) ?? 0;\n const neighborScore = friendCount / maxFriendLinks;\n\n const score =\n SEMANTIC_WEIGHT * semanticScore +\n TAG_WEIGHT * tagScore +\n NEIGHBOR_WEIGHT * neighborScore;\n\n // Only include if there is at least some signal\n if (score <= 0) continue;\n\n const reason = buildReason(semanticScore, tagScore, neighborScore, friendCount);\n\n suggestions.push({\n path: vault_path,\n title,\n score,\n semanticScore,\n tagScore,\n neighborScore,\n reason,\n suggestedWikilink: suggestedWikilink(vault_path),\n });\n }\n\n suggestions.sort((a, b) => b.score - a.score);\n return suggestions.slice(0, limit);\n}\n","/**\n * God-note detection — find hub notes via degree centrality on the vault link graph.\n *\n * Hub notes (or \"god notes\") are notes with unusually high inbound link counts.\n * They represent core concepts that the rest of the vault frequently references.\n * Structural pages (index, MOC, master notes, tag pages) are filtered out.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface GodNoteOptions {\n /** Maximum number of hub notes to return. Default: 20. */\n limit?: number;\n /** Minimum inbound link count to qualify as a hub. Default: 3. */\n minInbound?: number;\n /** Include outbound counts in the result. Default: true. */\n includeOutbound?: boolean;\n}\n\nexport interface GodNote {\n path: string;\n title: string | null;\n inboundCount: number;\n outboundCount: number;\n /** Ratio of inbound to total degree — higher means more \"sink\"-like. */\n inboundRatio: number;\n}\n\nexport interface GodNoteResult {\n godNotes: GodNote[];\n totalVaultFiles: number;\n /** Median inbound count across all vault files (for context). */\n medianInbound: number;\n}\n\n/** Patterns that identify structural/meta pages rather than concept notes. */\nconst STRUCTURAL_PATTERNS = [\n /\\bindex\\b/i,\n /\\bMOC\\b/,\n /\\bmaster\\b/i,\n /\\btag\\s*page/i,\n /\\bhome\\b/i,\n /\\bdashboard\\b/i,\n /\\btemplate/i,\n /^_/,\n];\n\nfunction isStructuralNote(title: string | null, path: string): boolean {\n const text = title ?? path;\n return STRUCTURAL_PATTERNS.some((pattern) => pattern.test(text));\n}\n\n/**\n * Find hub/\"god\" notes in the vault — notes with the highest inbound link counts,\n * excluding structural pages.\n */\nexport async function zettelGodNotes(\n backend: StorageBackend,\n opts?: GodNoteOptions,\n): Promise<GodNoteResult> {\n const limit = opts?.limit ?? 20;\n const minInbound = opts?.minInbound ?? 3;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n\n // Count inbound and outbound per path\n const inboundCounts = new Map<string, number>();\n const outboundCounts = new Map<string, number>();\n\n for (const { source_path, target_path } of linkGraph) {\n inboundCounts.set(target_path, (inboundCounts.get(target_path) ?? 0) + 1);\n outboundCounts.set(source_path, (outboundCounts.get(source_path) ?? 0) + 1);\n }\n\n // Get all vault files for title lookup and total count\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n const totalVaultFiles = allFiles.length;\n\n // Compute median inbound for context\n const allInbounds = allFiles.map((f) => inboundCounts.get(f.vaultPath) ?? 0);\n allInbounds.sort((a, b) => a - b);\n const medianInbound =\n allInbounds.length > 0\n ? allInbounds[Math.floor(allInbounds.length / 2)]\n : 0;\n\n // Build candidate list: all paths with inbound >= minInbound, excluding structural\n const candidates: GodNote[] = [];\n\n for (const [path, inbound] of inboundCounts) {\n if (inbound < minInbound) continue;\n\n const title = titleMap.get(path) ?? null;\n if (isStructuralNote(title, path)) continue;\n\n const outbound = outboundCounts.get(path) ?? 0;\n const totalDegree = inbound + outbound;\n const inboundRatio = totalDegree > 0 ? inbound / totalDegree : 0;\n\n candidates.push({\n path,\n title,\n inboundCount: inbound,\n outboundCount: outbound,\n inboundRatio: Math.round(inboundRatio * 1000) / 1000,\n });\n }\n\n // Sort by inbound count descending\n candidates.sort((a, b) => b.inboundCount - a.inboundCount);\n\n return {\n godNotes: candidates.slice(0, limit),\n totalVaultFiles,\n medianInbound,\n };\n}\n","/**\n * Community detection for the Zettelkasten vault link graph.\n *\n * Implements the Louvain algorithm in pure TypeScript for discovering\n * densely connected clusters of notes. This reveals emergent knowledge\n * communities that may not be obvious from folder structure alone.\n *\n * The Louvain method optimizes modularity in two phases:\n * 1. Local: each node greedily moves to the community that maximizes modularity gain.\n * 2. Aggregation: communities are collapsed into super-nodes, forming a new graph.\n * Repeat until no further improvement.\n */\n\nimport type { StorageBackend } from \"../storage/interface.js\";\n\nexport interface CommunityOptions {\n /** Minimum community size to include in results. Default: 3. */\n minSize?: number;\n /** Maximum number of communities to return. Default: 20. */\n maxCommunities?: number;\n /** Resolution parameter for Louvain (higher = more communities). Default: 1.0. */\n resolution?: number;\n}\n\nexport interface CommunityNode {\n path: string;\n title: string | null;\n /** Degree (inbound + outbound) within the community. */\n internalDegree: number;\n}\n\nexport interface Community {\n id: number;\n /** Label derived from most common words in note titles. */\n label: string;\n nodes: CommunityNode[];\n size: number;\n /** Cohesion: ratio of internal edges to total possible internal edges. */\n cohesion: number;\n /** Top folders represented in this community. */\n topFolders: string[];\n}\n\nexport interface CommunityResult {\n communities: Community[];\n totalNodes: number;\n totalEdges: number;\n /** Global modularity score of the partition. */\n modularity: number;\n}\n\n// ---------------------------------------------------------------------------\n// Louvain algorithm implementation\n// ---------------------------------------------------------------------------\n\ninterface Graph {\n /** All node IDs */\n nodes: string[];\n /** Adjacency list: node -> Map<neighbor, weight> */\n adj: Map<string, Map<string, number>>;\n /** Total edge weight (sum of all edge weights, counting undirected edges once) */\n totalWeight: number;\n /** Weighted degree per node */\n degree: Map<string, number>;\n}\n\nfunction buildUndirectedGraph(\n edges: Array<{ source_path: string; target_path: string }>\n): Graph {\n const adj = new Map<string, Map<string, number>>();\n const nodeSet = new Set<string>();\n\n function getOrCreate(node: string): Map<string, number> {\n let m = adj.get(node);\n if (!m) {\n m = new Map();\n adj.set(node, m);\n }\n nodeSet.add(node);\n return m;\n }\n\n let totalWeight = 0;\n\n for (const { source_path, target_path } of edges) {\n if (source_path === target_path) continue; // skip self-loops\n\n const aMap = getOrCreate(source_path);\n const bMap = getOrCreate(target_path);\n\n // Undirected: add weight in both directions\n aMap.set(target_path, (aMap.get(target_path) ?? 0) + 1);\n bMap.set(source_path, (bMap.get(source_path) ?? 0) + 1);\n totalWeight += 1; // each undirected edge counted once\n }\n\n // Compute weighted degree\n const degree = new Map<string, number>();\n for (const [node, neighbors] of adj) {\n let d = 0;\n for (const w of neighbors.values()) d += w;\n degree.set(node, d);\n }\n\n return {\n nodes: Array.from(nodeSet),\n adj,\n totalWeight,\n degree,\n };\n}\n\n/**\n * Run one pass of Phase 1: local node movement.\n * Returns true if any node changed community.\n */\nfunction louvainPhase1(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): boolean {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return false;\n\n // Community -> sum of degrees of nodes in community\n const communityDegreeSum = new Map<number, number>();\n // Community -> sum of internal edge weights\n const communityInternalWeight = new Map<number, number>();\n\n for (const node of graph.nodes) {\n const c = community.get(node)!;\n communityDegreeSum.set(c, (communityDegreeSum.get(c) ?? 0) + (graph.degree.get(node) ?? 0));\n }\n\n // Compute internal weights\n for (const [node, neighbors] of graph.adj) {\n const nc = community.get(node)!;\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === nc) {\n communityInternalWeight.set(nc, (communityInternalWeight.get(nc) ?? 0) + weight);\n }\n }\n }\n // Each internal edge is counted twice (both endpoints), so divide\n for (const [c, w] of communityInternalWeight) {\n communityInternalWeight.set(c, w / 2);\n }\n\n let improved = false;\n // Shuffle node order for better convergence\n const shuffled = [...graph.nodes];\n for (let i = shuffled.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];\n }\n\n for (const node of shuffled) {\n const currentComm = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n const neighbors = graph.adj.get(node) ?? new Map();\n\n // Compute weight to each neighboring community\n const weightToComm = new Map<number, number>();\n for (const [neighbor, weight] of neighbors) {\n const nc = community.get(neighbor)!;\n weightToComm.set(nc, (weightToComm.get(nc) ?? 0) + weight);\n }\n\n // Remove node from its current community\n communityDegreeSum.set(currentComm, (communityDegreeSum.get(currentComm) ?? 0) - ki);\n const weightToCurrent = weightToComm.get(currentComm) ?? 0;\n communityInternalWeight.set(\n currentComm,\n (communityInternalWeight.get(currentComm) ?? 0) - weightToCurrent,\n );\n\n // Find best community\n let bestComm = currentComm;\n let bestDelta = 0;\n\n for (const [candidateComm, weightToCandidate] of weightToComm) {\n const sigmaTot = communityDegreeSum.get(candidateComm) ?? 0;\n // Modularity gain of moving node to candidateComm\n const delta = weightToCandidate - resolution * (ki * sigmaTot) / m2;\n if (delta > bestDelta) {\n bestDelta = delta;\n bestComm = candidateComm;\n }\n }\n\n // Also consider staying (delta = 0, but need to re-add)\n // Move node to best community\n community.set(node, bestComm);\n communityDegreeSum.set(bestComm, (communityDegreeSum.get(bestComm) ?? 0) + ki);\n const weightToBest = weightToComm.get(bestComm) ?? 0;\n communityInternalWeight.set(\n bestComm,\n (communityInternalWeight.get(bestComm) ?? 0) + weightToBest,\n );\n\n if (bestComm !== currentComm) {\n improved = true;\n }\n }\n\n return improved;\n}\n\n/**\n * Compute modularity for a given partition.\n */\nfunction computeModularity(\n graph: Graph,\n community: Map<string, number>,\n resolution: number,\n): number {\n const m2 = 2 * graph.totalWeight;\n if (m2 === 0) return 0;\n\n let q = 0;\n for (const [node, neighbors] of graph.adj) {\n const ci = community.get(node)!;\n const ki = graph.degree.get(node) ?? 0;\n\n for (const [neighbor, weight] of neighbors) {\n if (community.get(neighbor) === ci) {\n q += weight - resolution * (ki * (graph.degree.get(neighbor) ?? 0)) / m2;\n }\n }\n }\n return q / m2;\n}\n\n/**\n * Run Louvain community detection on the vault link graph.\n */\nfunction runLouvain(\n graph: Graph,\n resolution: number,\n): Map<string, number> {\n // Initialize: each node in its own community\n const community = new Map<string, number>();\n let nextComm = 0;\n for (const node of graph.nodes) {\n community.set(node, nextComm++);\n }\n\n // Phase 1: iteratively move nodes until no improvement\n const MAX_ITERATIONS = 20;\n for (let iter = 0; iter < MAX_ITERATIONS; iter++) {\n const improved = louvainPhase1(graph, community, resolution);\n if (!improved) break;\n }\n\n // Compact community IDs\n const commRemap = new Map<number, number>();\n let remapIdx = 0;\n for (const [, c] of community) {\n if (!commRemap.has(c)) {\n commRemap.set(c, remapIdx++);\n }\n }\n for (const [node, c] of community) {\n community.set(node, commRemap.get(c)!);\n }\n\n return community;\n}\n\n// ---------------------------------------------------------------------------\n// Label generation\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS = new Set([\n \"the\", \"and\", \"for\", \"are\", \"but\", \"not\", \"you\", \"all\", \"can\", \"her\",\n \"was\", \"one\", \"our\", \"out\", \"has\", \"had\", \"how\", \"its\", \"may\", \"new\",\n \"now\", \"old\", \"see\", \"way\", \"who\", \"did\", \"get\", \"let\", \"say\", \"she\",\n \"too\", \"use\", \"from\", \"with\", \"this\", \"that\", \"will\", \"been\", \"have\",\n \"each\", \"make\", \"like\", \"long\", \"look\", \"many\", \"them\", \"then\", \"what\",\n \"when\", \"some\", \"time\", \"very\", \"your\", \"about\", \"could\", \"into\", \"just\",\n \"more\", \"note\", \"notes\", \"than\", \"over\",\n]);\n\nfunction generateCommunityLabel(titles: Array<string | null>): string {\n const wordCounts = new Map<string, number>();\n for (const title of titles) {\n if (!title) continue;\n const words = title\n .toLowerCase()\n .replace(/[^a-z0-9\\s]/g, \" \")\n .split(/\\s+/)\n .filter((w) => w.length > 2 && !STOP_WORDS.has(w));\n for (const word of words) {\n wordCounts.set(word, (wordCounts.get(word) ?? 0) + 1);\n }\n }\n const sorted = [...wordCounts.entries()].sort((a, b) => b[1] - a[1]);\n return sorted\n .slice(0, 3)\n .map(([w]) => w)\n .join(\" / \") || \"unnamed\";\n}\n\nfunction getTopFolder(path: string): string {\n const slash = path.indexOf(\"/\");\n return slash === -1 ? path : path.slice(0, slash);\n}\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Detect communities in the vault link graph using the Louvain algorithm.\n * Returns clusters of densely connected notes with labels and cohesion scores.\n */\nexport async function zettelCommunities(\n backend: StorageBackend,\n opts?: CommunityOptions,\n): Promise<CommunityResult> {\n const minSize = opts?.minSize ?? 3;\n const maxCommunities = opts?.maxCommunities ?? 20;\n const resolution = opts?.resolution ?? 1.0;\n\n // Get the full link graph\n const linkGraph = await backend.getVaultLinkGraph();\n const graph = buildUndirectedGraph(linkGraph);\n\n if (graph.nodes.length === 0) {\n return {\n communities: [],\n totalNodes: 0,\n totalEdges: 0,\n modularity: 0,\n };\n }\n\n // Run Louvain\n const communityMap = runLouvain(graph, resolution);\n\n // Compute modularity\n const modularity = computeModularity(graph, communityMap, resolution);\n\n // Group nodes by community\n const groups = new Map<number, string[]>();\n for (const [node, comm] of communityMap) {\n const arr = groups.get(comm);\n if (arr) arr.push(node);\n else groups.set(comm, [node]);\n }\n\n // Get all vault files for title lookup\n const allFiles = await backend.getAllVaultFiles();\n const titleMap = new Map<string, string | null>();\n for (const f of allFiles) {\n titleMap.set(f.vaultPath, f.title);\n }\n\n // Build community results\n const communities: Community[] = [];\n let commId = 0;\n\n for (const [, members] of groups) {\n if (members.length < minSize) continue;\n\n const memberSet = new Set(members);\n const titles = members.map((p) => titleMap.get(p) ?? null);\n const label = generateCommunityLabel(titles);\n\n // Compute internal degree per node\n const nodes: CommunityNode[] = members.map((path) => {\n const neighbors = graph.adj.get(path) ?? new Map();\n let internalDeg = 0;\n for (const [neighbor, weight] of neighbors) {\n if (memberSet.has(neighbor)) internalDeg += weight;\n }\n return {\n path,\n title: titleMap.get(path) ?? null,\n internalDegree: internalDeg,\n };\n });\n\n // Sort nodes by internal degree descending\n nodes.sort((a, b) => b.internalDegree - a.internalDegree);\n\n // Compute cohesion: ratio of actual internal edges to possible\n let internalEdges = 0;\n for (const node of nodes) {\n internalEdges += node.internalDegree;\n }\n internalEdges /= 2; // each edge counted twice\n const possibleEdges = (members.length * (members.length - 1)) / 2;\n const cohesion = possibleEdges > 0 ? Math.round((internalEdges / possibleEdges) * 1000) / 1000 : 0;\n\n // Top folders\n const folderCounts = new Map<string, number>();\n for (const path of members) {\n const folder = getTopFolder(path);\n folderCounts.set(folder, (folderCounts.get(folder) ?? 0) + 1);\n }\n const topFolders = [...folderCounts.entries()]\n .sort((a, b) => b[1] - a[1])\n .slice(0, 5)\n .map(([f]) => f);\n\n communities.push({\n id: commId++,\n label,\n nodes,\n size: members.length,\n cohesion,\n topFolders,\n });\n }\n\n // Sort by size descending\n communities.sort((a, b) => b.size - a.size);\n\n return {\n communities: communities.slice(0, maxCommunities),\n totalNodes: graph.nodes.length,\n totalEdges: graph.totalWeight,\n modularity: Math.round(modularity * 10000) / 10000,\n };\n}\n"],"mappings":"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"}