studiograph 1.3.3 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/dist/agent/orchestrator.d.ts +10 -0
  2. package/dist/agent/orchestrator.js +26 -7
  3. package/dist/agent/orchestrator.js.map +1 -1
  4. package/dist/agent/skills/sync-configuration.md +4 -29
  5. package/dist/agent/skills/sync-setup.md +2 -4
  6. package/dist/agent/tools/graph-tools.d.ts +5 -1
  7. package/dist/agent/tools/graph-tools.js +161 -9
  8. package/dist/agent/tools/graph-tools.js.map +1 -1
  9. package/dist/agent/tools/ops-tools.js +15 -126
  10. package/dist/agent/tools/ops-tools.js.map +1 -1
  11. package/dist/agent/tools/permission-tools.d.ts +15 -14
  12. package/dist/agent/tools/permission-tools.js +65 -128
  13. package/dist/agent/tools/permission-tools.js.map +1 -1
  14. package/dist/agent/tools/sync-tools.d.ts +7 -6
  15. package/dist/agent/tools/sync-tools.js +205 -178
  16. package/dist/agent/tools/sync-tools.js.map +1 -1
  17. package/dist/cli/commands/about.d.ts +13 -0
  18. package/dist/cli/commands/about.js +97 -0
  19. package/dist/cli/commands/about.js.map +1 -0
  20. package/dist/cli/commands/clone.d.ts +5 -2
  21. package/dist/cli/commands/clone.js +131 -62
  22. package/dist/cli/commands/clone.js.map +1 -1
  23. package/dist/cli/commands/connector.d.ts +2 -16
  24. package/dist/cli/commands/connector.js +32 -109
  25. package/dist/cli/commands/connector.js.map +1 -1
  26. package/dist/cli/commands/deploy.d.ts +0 -1
  27. package/dist/cli/commands/deploy.js +13 -103
  28. package/dist/cli/commands/deploy.js.map +1 -1
  29. package/dist/cli/commands/init.js +6 -93
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/join.d.ts +3 -2
  32. package/dist/cli/commands/join.js +142 -97
  33. package/dist/cli/commands/join.js.map +1 -1
  34. package/dist/cli/commands/redeploy.js +1 -2
  35. package/dist/cli/commands/redeploy.js.map +1 -1
  36. package/dist/cli/commands/serve.d.ts +1 -3
  37. package/dist/cli/commands/serve.js +29 -109
  38. package/dist/cli/commands/serve.js.map +1 -1
  39. package/dist/cli/commands/start.js +1 -1
  40. package/dist/cli/commands/start.js.map +1 -1
  41. package/dist/cli/commands/sync-collection.d.ts +14 -0
  42. package/dist/cli/commands/sync-collection.js +366 -0
  43. package/dist/cli/commands/sync-collection.js.map +1 -0
  44. package/dist/cli/commands/sync.d.ts +4 -2
  45. package/dist/cli/commands/sync.js +529 -94
  46. package/dist/cli/commands/sync.js.map +1 -1
  47. package/dist/cli/index.js +15 -30
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/cli/setup-wizard.d.ts +0 -13
  50. package/dist/cli/setup-wizard.js +6 -81
  51. package/dist/cli/setup-wizard.js.map +1 -1
  52. package/dist/core/graph.d.ts +8 -2
  53. package/dist/core/graph.js +11 -7
  54. package/dist/core/graph.js.map +1 -1
  55. package/dist/core/types.d.ts +149 -21
  56. package/dist/core/types.js +16 -4
  57. package/dist/core/types.js.map +1 -1
  58. package/dist/core/workspace-manager.js +1 -5
  59. package/dist/core/workspace-manager.js.map +1 -1
  60. package/dist/core/workspace.d.ts +11 -4
  61. package/dist/core/workspace.js +61 -26
  62. package/dist/core/workspace.js.map +1 -1
  63. package/dist/integrations/asana.d.ts +26 -0
  64. package/dist/integrations/asana.js +77 -0
  65. package/dist/integrations/asana.js.map +1 -0
  66. package/dist/integrations/figma-local.d.ts +16 -0
  67. package/dist/integrations/figma-local.js +10 -0
  68. package/dist/integrations/figma-local.js.map +1 -0
  69. package/dist/integrations/figma.d.ts +17 -0
  70. package/dist/integrations/figma.js +16 -0
  71. package/dist/integrations/figma.js.map +1 -0
  72. package/dist/integrations/granola.d.ts +24 -0
  73. package/dist/integrations/granola.js +59 -0
  74. package/dist/integrations/granola.js.map +1 -0
  75. package/dist/integrations/linear.d.ts +14 -0
  76. package/dist/integrations/linear.js +70 -0
  77. package/dist/integrations/linear.js.map +1 -0
  78. package/dist/integrations/paper-local.d.ts +14 -0
  79. package/dist/integrations/paper-local.js +10 -0
  80. package/dist/integrations/paper-local.js.map +1 -0
  81. package/dist/integrations/paper.d.ts +2 -0
  82. package/dist/integrations/paper.js +10 -0
  83. package/dist/integrations/paper.js.map +1 -0
  84. package/dist/integrations/pipedrive.d.ts +26 -0
  85. package/dist/integrations/pipedrive.js +97 -0
  86. package/dist/integrations/pipedrive.js.map +1 -0
  87. package/dist/integrations/registry.d.ts +15 -0
  88. package/dist/integrations/registry.js +27 -0
  89. package/dist/integrations/registry.js.map +1 -0
  90. package/dist/integrations/types.d.ts +34 -0
  91. package/dist/integrations/types.js +9 -0
  92. package/dist/integrations/types.js.map +1 -0
  93. package/dist/mcp/connector-manager.d.ts +45 -31
  94. package/dist/mcp/connector-manager.js +164 -116
  95. package/dist/mcp/connector-manager.js.map +1 -1
  96. package/dist/mcp/server-oauth-provider.d.ts +56 -0
  97. package/dist/mcp/server-oauth-provider.js +138 -0
  98. package/dist/mcp/server-oauth-provider.js.map +1 -0
  99. package/dist/server/chrome/chrome.css +142 -28
  100. package/dist/server/chrome/chrome.js +46 -220
  101. package/dist/server/collab-authority.d.ts +70 -0
  102. package/dist/server/collab-authority.js +218 -0
  103. package/dist/server/collab-authority.js.map +1 -0
  104. package/dist/server/collab.d.ts +29 -0
  105. package/dist/server/collab.js +195 -0
  106. package/dist/server/collab.js.map +1 -0
  107. package/dist/server/commit-scheduler.d.ts +39 -0
  108. package/dist/server/commit-scheduler.js +113 -0
  109. package/dist/server/commit-scheduler.js.map +1 -0
  110. package/dist/server/index.d.ts +0 -2
  111. package/dist/server/index.js +166 -55
  112. package/dist/server/index.js.map +1 -1
  113. package/dist/server/routes/auth-api.d.ts +6 -0
  114. package/dist/server/routes/auth-api.js +78 -0
  115. package/dist/server/routes/auth-api.js.map +1 -1
  116. package/dist/server/routes/chat.js +4 -0
  117. package/dist/server/routes/chat.js.map +1 -1
  118. package/dist/server/routes/collab.d.ts +6 -0
  119. package/dist/server/routes/collab.js +10 -0
  120. package/dist/server/routes/collab.js.map +1 -0
  121. package/dist/server/routes/git-http.d.ts +23 -0
  122. package/dist/server/routes/git-http.js +251 -0
  123. package/dist/server/routes/git-http.js.map +1 -0
  124. package/dist/server/routes/graph-api.d.ts +6 -2
  125. package/dist/server/routes/graph-api.js +266 -82
  126. package/dist/server/routes/graph-api.js.map +1 -1
  127. package/dist/server/routes/mcp.d.ts +12 -0
  128. package/dist/server/routes/mcp.js +35 -0
  129. package/dist/server/routes/mcp.js.map +1 -0
  130. package/dist/server/routes/permissions-api.d.ts +6 -4
  131. package/dist/server/routes/permissions-api.js +53 -167
  132. package/dist/server/routes/permissions-api.js.map +1 -1
  133. package/dist/server/routes/sync-api.d.ts +26 -0
  134. package/dist/server/routes/sync-api.js +757 -0
  135. package/dist/server/routes/sync-api.js.map +1 -0
  136. package/dist/server/routes/ws.d.ts +9 -0
  137. package/dist/server/routes/ws.js +131 -0
  138. package/dist/server/routes/ws.js.map +1 -0
  139. package/dist/server/session-manager.d.ts +40 -0
  140. package/dist/server/session-manager.js +132 -0
  141. package/dist/server/session-manager.js.map +1 -0
  142. package/dist/server/ws-hub.d.ts +130 -0
  143. package/dist/server/ws-hub.js +250 -0
  144. package/dist/server/ws-hub.js.map +1 -0
  145. package/dist/server/yjs-manager.d.ts +59 -0
  146. package/dist/server/yjs-manager.js +194 -0
  147. package/dist/server/yjs-manager.js.map +1 -0
  148. package/dist/services/auth-service.d.ts +74 -0
  149. package/dist/services/auth-service.js +286 -6
  150. package/dist/services/auth-service.js.map +1 -1
  151. package/dist/services/git.d.ts +6 -0
  152. package/dist/services/git.js +32 -2
  153. package/dist/services/git.js.map +1 -1
  154. package/dist/services/sync/collection-sync.d.ts +73 -0
  155. package/dist/services/sync/collection-sync.js +726 -0
  156. package/dist/services/sync/collection-sync.js.map +1 -0
  157. package/dist/services/sync/commit.js +5 -20
  158. package/dist/services/sync/commit.js.map +1 -1
  159. package/dist/services/sync/data-fetcher.d.ts +31 -0
  160. package/dist/services/sync/data-fetcher.js +12 -0
  161. package/dist/services/sync/data-fetcher.js.map +1 -0
  162. package/dist/services/sync/entity-refresh.d.ts +30 -0
  163. package/dist/services/sync/entity-refresh.js +275 -0
  164. package/dist/services/sync/entity-refresh.js.map +1 -0
  165. package/dist/services/sync/frontmatter-extractor.d.ts +2 -2
  166. package/dist/services/sync/frontmatter-extractor.js +1 -2
  167. package/dist/services/sync/frontmatter-extractor.js.map +1 -1
  168. package/dist/services/sync/graph-match.js +1 -1
  169. package/dist/services/sync/graph-match.js.map +1 -1
  170. package/dist/services/sync/mcp-client.d.ts +16 -4
  171. package/dist/services/sync/mcp-client.js +34 -20
  172. package/dist/services/sync/mcp-client.js.map +1 -1
  173. package/dist/services/sync/prompts.js +1 -1
  174. package/dist/services/sync/reconciler.js +1 -2
  175. package/dist/services/sync/reconciler.js.map +1 -1
  176. package/dist/services/sync/rest-client.d.ts +40 -0
  177. package/dist/services/sync/rest-client.js +100 -0
  178. package/dist/services/sync/rest-client.js.map +1 -0
  179. package/dist/services/sync/source-config.d.ts +23 -1
  180. package/dist/services/sync/source-config.js +112 -16
  181. package/dist/services/sync/source-config.js.map +1 -1
  182. package/dist/services/sync/source-definitions/asana.d.ts +3 -4
  183. package/dist/services/sync/source-definitions/asana.js +7 -13
  184. package/dist/services/sync/source-definitions/asana.js.map +1 -1
  185. package/dist/services/sync/source-definitions/definitions.d.ts +5 -18
  186. package/dist/services/sync/source-definitions/definitions.js +4 -22
  187. package/dist/services/sync/source-definitions/definitions.js.map +1 -1
  188. package/dist/services/sync/source-definitions/granola.d.ts +1 -1
  189. package/dist/services/sync/source-definitions/granola.js +11 -18
  190. package/dist/services/sync/source-definitions/granola.js.map +1 -1
  191. package/dist/services/sync/source-definitions/linear.d.ts +1 -1
  192. package/dist/services/sync/source-definitions/linear.js +17 -15
  193. package/dist/services/sync/source-definitions/linear.js.map +1 -1
  194. package/dist/services/sync/source-definitions/pipedrive.d.ts +1 -1
  195. package/dist/services/sync/source-definitions/pipedrive.js +6 -15
  196. package/dist/services/sync/source-definitions/pipedrive.js.map +1 -1
  197. package/dist/services/sync/staging.js +1 -2
  198. package/dist/services/sync/staging.js.map +1 -1
  199. package/dist/services/sync/structured-extractor.d.ts +8 -2
  200. package/dist/services/sync/structured-extractor.js +243 -35
  201. package/dist/services/sync/structured-extractor.js.map +1 -1
  202. package/dist/services/sync/types.d.ts +192 -23
  203. package/dist/services/sync/unstructured-extractor.d.ts +1 -3
  204. package/dist/services/sync/unstructured-extractor.js +2 -14
  205. package/dist/services/sync/unstructured-extractor.js.map +1 -1
  206. package/dist/utils/git.d.ts +33 -20
  207. package/dist/utils/git.js +119 -62
  208. package/dist/utils/git.js.map +1 -1
  209. package/dist/utils/preflight.d.ts +1 -15
  210. package/dist/utils/preflight.js +1 -35
  211. package/dist/utils/preflight.js.map +1 -1
  212. package/dist/web/_app/immutable/assets/0.CupILLQs.css +1 -0
  213. package/dist/web/_app/immutable/assets/3.CtJi4Cy9.css +1 -0
  214. package/dist/web/_app/immutable/assets/5.CydFyZSu.css +1 -0
  215. package/dist/web/_app/immutable/assets/6.kqeOo0OW.css +1 -0
  216. package/dist/web/_app/immutable/assets/7.CseIx7qQ.css +1 -0
  217. package/dist/web/_app/immutable/assets/8.BYpFDZHK.css +1 -0
  218. package/dist/web/_app/immutable/assets/AppShell.Ch_ef9hJ.css +1 -0
  219. package/dist/web/_app/immutable/assets/ChatPanel.CP-_8txt.css +1 -0
  220. package/dist/web/_app/immutable/chunks/0oxpWEgM.js +1 -0
  221. package/dist/web/_app/immutable/chunks/B1y7Wy5O.js +18 -0
  222. package/dist/web/_app/immutable/chunks/B7eduG_j.js +64 -0
  223. package/dist/web/_app/immutable/chunks/BBLgaWN8.js +1 -0
  224. package/dist/web/_app/immutable/chunks/BCB5cYCz.js +2 -0
  225. package/dist/web/_app/immutable/chunks/{aosHekRC.js → BPUy9_sS.js} +1 -1
  226. package/dist/web/_app/immutable/chunks/BVBRzmeQ.js +7 -0
  227. package/dist/web/_app/immutable/chunks/{CUzqHQY_.js → BXuvR8Ks.js} +2 -1
  228. package/dist/web/_app/immutable/chunks/BeBar3OL.js +1 -0
  229. package/dist/web/_app/immutable/chunks/BuOTIbJu.js +1 -0
  230. package/dist/web/_app/immutable/chunks/CLFba8FK.js +5 -0
  231. package/dist/web/_app/immutable/chunks/CQCkXCml.js +1 -0
  232. package/dist/web/_app/immutable/chunks/CXuhHL4d.js +1 -0
  233. package/dist/web/_app/immutable/chunks/Cg9NOuOl.js +27 -0
  234. package/dist/web/_app/immutable/chunks/Cs5oz2oJ.js +5 -0
  235. package/dist/web/_app/immutable/chunks/Cs_ROD7H.js +2 -0
  236. package/dist/web/_app/immutable/chunks/D2aTbzFm.js +3 -0
  237. package/dist/web/_app/immutable/chunks/D4FXhiC2.js +1 -0
  238. package/dist/web/_app/immutable/chunks/D4VHRYeB.js +1 -0
  239. package/dist/web/_app/immutable/chunks/DCGSm8Hl.js +1 -0
  240. package/dist/web/_app/immutable/chunks/DP09rP34.js +2 -0
  241. package/dist/web/_app/immutable/chunks/DiP47fAp.js +1 -0
  242. package/dist/web/_app/immutable/chunks/DptGlK8O.js +1 -0
  243. package/dist/web/_app/immutable/chunks/O0fx2ss6.js +1 -0
  244. package/dist/web/_app/immutable/chunks/xBRYfpah.js +1 -0
  245. package/dist/web/_app/immutable/entry/app.FgnywZP_.js +2 -0
  246. package/dist/web/_app/immutable/entry/start.Bsa-zlPf.js +1 -0
  247. package/dist/web/_app/immutable/nodes/0.D3SW-LMc.js +10 -0
  248. package/dist/web/_app/immutable/nodes/1.y0c5TQTP.js +1 -0
  249. package/dist/web/_app/immutable/nodes/2.BQfSep9-.js +1 -0
  250. package/dist/web/_app/immutable/nodes/3.CC4Y-xMM.js +11 -0
  251. package/dist/web/_app/immutable/nodes/{5.BBpmYkAu.js → 4.Dp0Z-oPW.js} +2 -2
  252. package/dist/web/_app/immutable/nodes/5.gjZ03DON.js +2 -0
  253. package/dist/web/_app/immutable/nodes/6.dRNIwcJQ.js +1 -0
  254. package/dist/web/_app/immutable/nodes/7.I4Gjes3o.js +2 -0
  255. package/dist/web/_app/immutable/nodes/8.Dj14D7uH.js +1 -0
  256. package/dist/web/_app/version.json +1 -1
  257. package/dist/web/index.html +10 -12
  258. package/package.json +12 -2
  259. package/dist/web/_app/immutable/assets/0.CDbX4Cwz.css +0 -1
  260. package/dist/web/_app/immutable/assets/3.BJy7pVXi.css +0 -1
  261. package/dist/web/_app/immutable/assets/4.Ad16uh9o.css +0 -1
  262. package/dist/web/_app/immutable/assets/6.Bm2i7O0j.css +0 -1
  263. package/dist/web/_app/immutable/assets/AppShell.D0rmbdqF.css +0 -1
  264. package/dist/web/_app/immutable/assets/ChatPanel.RFD5GGYI.css +0 -1
  265. package/dist/web/_app/immutable/assets/editor.CPAf2SRV.css +0 -1
  266. package/dist/web/_app/immutable/chunks/479TgXB4.js +0 -1
  267. package/dist/web/_app/immutable/chunks/4QY4j-jX.js +0 -1
  268. package/dist/web/_app/immutable/chunks/BFb0g4TQ.js +0 -64
  269. package/dist/web/_app/immutable/chunks/Bopa-Ask.js +0 -1
  270. package/dist/web/_app/immutable/chunks/COwytaCP.js +0 -1
  271. package/dist/web/_app/immutable/chunks/DEJSHbC3.js +0 -1
  272. package/dist/web/_app/immutable/chunks/DNywhIex.js +0 -23
  273. package/dist/web/_app/immutable/chunks/DTUXhwEY.js +0 -1
  274. package/dist/web/_app/immutable/chunks/DThXpa0U.js +0 -6
  275. package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +0 -18
  276. package/dist/web/_app/immutable/chunks/Dml-u95b.js +0 -2
  277. package/dist/web/_app/immutable/chunks/DnlgZ_Tk.js +0 -5
  278. package/dist/web/_app/immutable/chunks/DtVH--hH.js +0 -6
  279. package/dist/web/_app/immutable/chunks/DvKVaE7M.js +0 -1
  280. package/dist/web/_app/immutable/chunks/MbiSz-iW.js +0 -2
  281. package/dist/web/_app/immutable/chunks/bSAC733J.js +0 -1
  282. package/dist/web/_app/immutable/entry/app.BvodXQQ0.js +0 -2
  283. package/dist/web/_app/immutable/entry/start.Bkui3Kyw.js +0 -1
  284. package/dist/web/_app/immutable/nodes/0.DfbCOBhn.js +0 -2
  285. package/dist/web/_app/immutable/nodes/1.vtxUGpe6.js +0 -1
  286. package/dist/web/_app/immutable/nodes/2.Cq29oW4h.js +0 -1
  287. package/dist/web/_app/immutable/nodes/3.SquslPZy.js +0 -1
  288. package/dist/web/_app/immutable/nodes/4.COV8FR8b.js +0 -16
  289. package/dist/web/_app/immutable/nodes/6.BBbh6z9I.js +0 -2
  290. /package/dist/web/_app/immutable/assets/{5.BhKgiXd2.css → 4.BhKgiXd2.css} +0 -0
@@ -0,0 +1,726 @@
1
+ /**
2
+ * Collection-Scoped Sync Runner
3
+ *
4
+ * Given a collection with sync rules, connects to sources via MCP,
5
+ * extracts records, derives secondary entities, deduplicates against
6
+ * existing entities, and writes new entities to the collection.
7
+ *
8
+ * Sync-once semantics: existing entities (matched by source_ref) are
9
+ * never updated during a pull. Use entity-refresh for single-entity updates.
10
+ */
11
+ import { existsSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { SyncMCPClient } from './mcp-client.js';
14
+ import { StructuredExtractor, slugify, resolveFieldPath, applyTransform } from './structured-extractor.js';
15
+ import { FrontmatterExtractor } from './frontmatter-extractor.js';
16
+ import { SyncStateManager } from './sync-state.js';
17
+ import { SourceConfigManager } from './source-config.js';
18
+ import { BaseGraphManager } from '../../core/graph.js';
19
+ import { SchemaRegistry } from '../../core/schema-registry.js';
20
+ import { ConnectorManager, KNOWN_CONNECTORS } from '../../mcp/connector-manager.js';
21
+ import { ServerOAuthProvider } from '../../mcp/server-oauth-provider.js';
22
+ import { RESTClient } from './rest-client.js';
23
+ import { SOURCE_DEFINITIONS } from './source-definitions/definitions.js';
24
+ /** Create a DataFetcher — REST or MCP depending on source definition's adapter field. */
25
+ function createDataFetcher(connectorConfig, definition, opts) {
26
+ // REST adapter — direct HTTP, no MCP
27
+ if (definition?.adapter === 'rest' && definition.rest) {
28
+ const headers = {};
29
+ if (connectorConfig.headers) {
30
+ for (const [k, v] of Object.entries(connectorConfig.headers)) {
31
+ headers[k] = ConnectorManager.resolveEnvVar(v);
32
+ }
33
+ }
34
+ // Fall back to OAuth tokens if no manual headers
35
+ if (!headers['Authorization'] && opts?.userId && opts?.authService) {
36
+ const oauthData = opts.authService.getOAuthData(opts.userId, connectorConfig.name);
37
+ if (oauthData?.tokens?.access_token) {
38
+ headers['Authorization'] = `Bearer ${oauthData.tokens.access_token}`;
39
+ }
40
+ }
41
+ // Resolve query-param auth (e.g. api_token for Pipedrive)
42
+ const queryAuth = {};
43
+ if (definition.rest.query_auth) {
44
+ for (const [k, v] of Object.entries(definition.rest.query_auth)) {
45
+ queryAuth[k] = ConnectorManager.resolveEnvVar(v);
46
+ }
47
+ }
48
+ return new RESTClient(definition.rest.base_url, headers, definition.entities, queryAuth);
49
+ }
50
+ // MCP adapter (default)
51
+ const known = KNOWN_CONNECTORS[connectorConfig.name];
52
+ const auth = connectorConfig.auth ?? known?.auth;
53
+ if (auth === 'oauth' && opts?.userId && opts?.authService && opts?.serverUrl) {
54
+ const provider = new ServerOAuthProvider(connectorConfig.name, opts.userId, opts.authService, opts.serverUrl, known?.oauth);
55
+ return new SyncMCPClient(connectorConfig, provider);
56
+ }
57
+ if (auth === 'oauth') {
58
+ throw new Error(`OAuth authorization required for "${connectorConfig.name}". Please authorize in Settings > Connectors.`);
59
+ }
60
+ return new SyncMCPClient(connectorConfig);
61
+ }
62
+ /**
63
+ * Run sync for a collection. Processes each sync rule in the collection's config.
64
+ */
65
+ export async function syncCollection(options) {
66
+ const { workspacePath, collectionName, workspaceConfig, schemaExtensions, onProgress, userId, authService, serverUrl } = options;
67
+ const log = onProgress ?? (() => { });
68
+ // Find collection config
69
+ const repoConfig = workspaceConfig.repos.find(r => r.name === collectionName);
70
+ if (!repoConfig) {
71
+ throw new Error(`Collection "${collectionName}" not found in workspace config`);
72
+ }
73
+ const syncRules = repoConfig.sync;
74
+ if (!syncRules || syncRules.length === 0) {
75
+ throw new Error(`Collection "${collectionName}" has no sync rules configured`);
76
+ }
77
+ const results = [];
78
+ for (const rule of syncRules) {
79
+ log(`\nSyncing from source: ${rule.source}`);
80
+ try {
81
+ const result = await syncOneRule({
82
+ workspacePath,
83
+ repoConfig,
84
+ rule,
85
+ workspaceConfig,
86
+ schemaExtensions,
87
+ onProgress,
88
+ userId,
89
+ authService,
90
+ serverUrl,
91
+ });
92
+ results.push(result);
93
+ }
94
+ catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ log(` Error: ${msg}`);
97
+ results.push({
98
+ collection: collectionName,
99
+ source: rule.source,
100
+ created: 0,
101
+ skipped: 0,
102
+ derived: 0,
103
+ errors: [msg],
104
+ });
105
+ }
106
+ }
107
+ return results;
108
+ }
109
+ /**
110
+ * Run all sync rules that apply to a collection:
111
+ * - Collection-level rules (if the collection has its own sync array)
112
+ * - Workspace-level rules that include this collection as a target
113
+ *
114
+ * This is the preferred entry point for per-collection sync — it handles both
115
+ * rule types without callers needing to know the distinction.
116
+ */
117
+ export async function syncRepo(options) {
118
+ const { workspacePath, collectionName, workspaceConfig, schemaExtensions, onProgress, userId, authService, serverUrl } = options;
119
+ const log = onProgress ?? (() => { });
120
+ const collectionResults = [];
121
+ const workspaceResults = [];
122
+ // Run collection-level rules (if any)
123
+ const repoConfig = workspaceConfig.repos.find(r => r.name === collectionName);
124
+ const collectionRules = repoConfig?.sync;
125
+ if (collectionRules && collectionRules.length > 0) {
126
+ const results = await syncCollection({ workspacePath, collectionName, workspaceConfig, schemaExtensions, onProgress, userId, authService, serverUrl });
127
+ collectionResults.push(...results);
128
+ }
129
+ // Run workspace-level rules that target this collection
130
+ const workspaceRules = workspaceConfig.sync ?? [];
131
+ for (const rule of workspaceRules) {
132
+ const matchingTargets = rule.targets.filter(t => t.repo === collectionName);
133
+ if (matchingTargets.length === 0)
134
+ continue;
135
+ log(`\nSyncing workspace rule: ${rule.source} → ${collectionName}`);
136
+ try {
137
+ const result = await syncWorkspaceRule({
138
+ workspacePath,
139
+ rule: { ...rule, targets: matchingTargets },
140
+ workspaceConfig,
141
+ schemaExtensions,
142
+ onProgress,
143
+ userId,
144
+ authService,
145
+ serverUrl,
146
+ });
147
+ workspaceResults.push(result);
148
+ }
149
+ catch (err) {
150
+ const msg = err instanceof Error ? err.message : String(err);
151
+ log(` Error: ${msg}`);
152
+ workspaceResults.push({
153
+ source: rule.source,
154
+ targets: matchingTargets.map(t => ({ repo: t.repo, created: 0, skipped: 0, derived: 0, errors: [msg] })),
155
+ });
156
+ }
157
+ }
158
+ if (collectionResults.length === 0 && workspaceResults.length === 0) {
159
+ throw new Error(`Collection "${collectionName}" has no sync rules configured (collection-level or workspace-level)`);
160
+ }
161
+ return { collectionResults, workspaceResults };
162
+ }
163
+ /**
164
+ * Run one workspace-level sync rule: fetch from source once, distribute to all targets.
165
+ * Each target receives a filtered subset of the extracted records.
166
+ */
167
+ export async function syncWorkspaceRule(options) {
168
+ const { workspacePath, rule, workspaceConfig, schemaExtensions, onProgress, userId, authService, serverUrl } = options;
169
+ const log = onProgress ?? (() => { });
170
+ const result = { source: rule.source, targets: [] };
171
+ const configMgr = new SourceConfigManager(workspacePath);
172
+ const definition = configMgr.getDefinition(rule.source);
173
+ const oldConfig = !definition ? configMgr.get(rule.source) : null;
174
+ if (!definition && !oldConfig) {
175
+ throw new Error(`Source "${rule.source}" not found in .studiograph/sources/`);
176
+ }
177
+ const connectorName = definition?.connector ?? oldConfig.connector;
178
+ const connectorConfigs = ConnectorManager.loadConfigs(workspacePath);
179
+ const connectorConfig = connectorConfigs.find(c => c.name === connectorName);
180
+ if (!connectorConfig) {
181
+ throw new Error(`Connector "${connectorName}" not configured. Run: studiograph connector add ${connectorName}`);
182
+ }
183
+ const client = createDataFetcher(connectorConfig, definition, { userId, authService, serverUrl });
184
+ try {
185
+ log(` Connecting to ${connectorName}${definition?.adapter === 'rest' ? ' (REST)' : ''}...`);
186
+ try {
187
+ await client.connect();
188
+ }
189
+ catch (err) {
190
+ const msg = err instanceof Error ? err.message : String(err);
191
+ throw new Error(`Failed to connect to connector "${connectorName}" at ${connectorConfig.url}. (${msg})`);
192
+ }
193
+ log(` Available tools: ${client.getTools().map(t => t.name).join(', ')}`);
194
+ const schemaRegistry = new SchemaRegistry(schemaExtensions);
195
+ const syncState = new SyncStateManager(workspacePath);
196
+ // ── Extract ALL records once (no target filter at this stage) ──────────────
197
+ let allRecords;
198
+ const filterFieldDefs = definition?.filter_fields ?? SOURCE_DEFINITIONS[connectorName]?.filter_fields ?? [];
199
+ if (definition) {
200
+ const primaryType = rule.entity_type ?? findPrimaryType(definition);
201
+ const primaryConfig = definition.entities[primaryType];
202
+ if (!primaryConfig) {
203
+ throw new Error(`Entity type "${primaryType}" not found in source definition`);
204
+ }
205
+ // Extract without any filter — filtering happens per-target below
206
+ const mapping = sourceEntityConfigToMapping(primaryType, primaryConfig, rule.source, '_workspace_', undefined);
207
+ const oldStyleConfig = {
208
+ name: rule.source,
209
+ connector: definition.connector,
210
+ enabled: true,
211
+ added_at: new Date().toISOString(),
212
+ entity_mappings: [mapping],
213
+ };
214
+ log(` Extracting ${primaryType} records...`);
215
+ if (primaryConfig.extraction_mode === 'frontmatter') {
216
+ const extractor = new FrontmatterExtractor(client, oldStyleConfig);
217
+ allRecords = await extractor.extract(mapping, onProgress);
218
+ }
219
+ else {
220
+ const extractor = new StructuredExtractor(client, oldStyleConfig, syncState);
221
+ allRecords = await extractor.extract(mapping, false, onProgress);
222
+ }
223
+ }
224
+ else {
225
+ // Old-format source
226
+ const mapping = { ...oldConfig.entity_mappings[0], target_repo: '_workspace_' };
227
+ const extractor = new StructuredExtractor(client, oldConfig, syncState);
228
+ allRecords = await extractor.extract(mapping, false, onProgress);
229
+ }
230
+ log(` Extracted ${allRecords.length} records (total)`);
231
+ // ── Write to each target ──────────────────────────────────────────────────
232
+ for (const target of rule.targets) {
233
+ const repoConfig = workspaceConfig.repos.find(r => r.name === target.repo);
234
+ if (!repoConfig) {
235
+ result.targets.push({ repo: target.repo, created: 0, skipped: 0, derived: 0, errors: [`Collection "${target.repo}" not found`] });
236
+ continue;
237
+ }
238
+ const repoPath = join(workspacePath, repoConfig.path);
239
+ if (!existsSync(repoPath)) {
240
+ result.targets.push({ repo: target.repo, created: 0, skipped: 0, derived: 0, errors: [`Collection path does not exist: ${repoPath}`] });
241
+ continue;
242
+ }
243
+ log(` Target: ${target.repo}${target.filter ? ` (filtered)` : ' (all records)'}`);
244
+ const graph = new BaseGraphManager({
245
+ repoPath,
246
+ repoName: target.repo,
247
+ gitUser: { id: 'sync', name: 'Studiograph Sync', email: 'sync@studiograph.local' },
248
+ schemaRegistry,
249
+ });
250
+ const existingSourceRefs = buildSourceRefIndex(graph);
251
+ // Apply target-specific filter
252
+ let targetRecords = applyClientFilters(allRecords, target.filter, filterFieldDefs, log);
253
+ // Override target_repo on records before writing
254
+ targetRecords = targetRecords.map(r => ({ ...r, target_repo: target.repo }));
255
+ const targetResult = { repo: target.repo, created: 0, skipped: 0, derived: 0, errors: [] };
256
+ const writeResults = await writeEntities(targetRecords, rule.source, graph, existingSourceRefs, log);
257
+ targetResult.created += writeResults.created;
258
+ targetResult.skipped += writeResults.skipped;
259
+ targetResult.errors.push(...writeResults.errors);
260
+ // Derive secondary entities (if definition has derive_from configs)
261
+ if (definition) {
262
+ const primaryType = rule.entity_type ?? findPrimaryType(definition);
263
+ for (const [entityType, entityConfig] of Object.entries(definition.entities)) {
264
+ if (entityType === primaryType || !entityConfig.derive_from)
265
+ continue;
266
+ log(` Deriving ${entityType} entities for ${target.repo}...`);
267
+ const derived = deriveEntities(targetRecords, entityType, entityConfig, rule.source);
268
+ const existingDedupeValues = entityConfig.dedupe_field
269
+ ? buildDedupeIndex(graph, entityType, entityConfig.dedupe_field)
270
+ : new Map();
271
+ const deduped = derived.filter(rec => {
272
+ if (existingSourceRefs.has(rec.source_ref))
273
+ return false;
274
+ if (entityConfig.dedupe_field) {
275
+ const val = rec.frontmatter[entityConfig.dedupe_field];
276
+ if (val && existingDedupeValues.has(String(val).toLowerCase()))
277
+ return false;
278
+ }
279
+ return true;
280
+ });
281
+ const derivedResults = await writeEntities(deduped, rule.source, graph, existingSourceRefs, log);
282
+ targetResult.derived += derivedResults.created;
283
+ targetResult.skipped += derivedResults.skipped;
284
+ targetResult.errors.push(...derivedResults.errors);
285
+ }
286
+ }
287
+ result.targets.push(targetResult);
288
+ }
289
+ }
290
+ finally {
291
+ await client.close();
292
+ }
293
+ return result;
294
+ }
295
+ async function syncOneRule(options) {
296
+ const { workspacePath, repoConfig, rule, workspaceConfig, schemaExtensions, onProgress, userId, authService, serverUrl } = options;
297
+ const log = onProgress ?? (() => { });
298
+ const collectionName = repoConfig.name;
299
+ const configMgr = new SourceConfigManager(workspacePath);
300
+ const result = {
301
+ collection: collectionName,
302
+ source: rule.source,
303
+ created: 0,
304
+ skipped: 0,
305
+ derived: 0,
306
+ errors: [],
307
+ };
308
+ // Load source definition (try new format first, then old format)
309
+ const definition = configMgr.getDefinition(rule.source);
310
+ const oldConfig = !definition ? configMgr.get(rule.source) : null;
311
+ if (!definition && !oldConfig) {
312
+ throw new Error(`Source "${rule.source}" not found in .studiograph/sources/`);
313
+ }
314
+ // Resolve connector config
315
+ const connectorName = definition?.connector ?? oldConfig.connector;
316
+ const connectorConfigs = ConnectorManager.loadConfigs(workspacePath);
317
+ const connectorConfig = connectorConfigs.find(c => c.name === connectorName);
318
+ if (!connectorConfig) {
319
+ throw new Error(`Connector "${connectorName}" not configured. Run: studiograph connector add ${connectorName}`);
320
+ }
321
+ // Connect to source
322
+ const client = createDataFetcher(connectorConfig, definition, { userId, authService, serverUrl });
323
+ try {
324
+ log(` Connecting to ${connectorName}${definition?.adapter === 'rest' ? ' (REST)' : ''}...`);
325
+ try {
326
+ await client.connect();
327
+ }
328
+ catch (err) {
329
+ const msg = err instanceof Error ? err.message : String(err);
330
+ throw new Error(`Failed to connect to connector "${connectorName}" at ${connectorConfig.url}. (${msg})`);
331
+ }
332
+ log(` Available tools: ${client.getTools().map(t => t.name).join(', ')}`);
333
+ // Build graph manager for the collection
334
+ const repoPath = join(workspacePath, repoConfig.path);
335
+ if (!existsSync(repoPath)) {
336
+ throw new Error(`Collection path does not exist: ${repoPath}`);
337
+ }
338
+ const schemaRegistry = new SchemaRegistry(schemaExtensions);
339
+ const graph = new BaseGraphManager({
340
+ repoPath,
341
+ repoName: collectionName,
342
+ gitUser: { id: 'sync', name: 'Studiograph Sync', email: 'sync@studiograph.local' },
343
+ schemaRegistry,
344
+ });
345
+ // Build index of existing source_refs in this collection
346
+ const existingSourceRefs = buildSourceRefIndex(graph);
347
+ log(` ${existingSourceRefs.size} existing synced entities in collection`);
348
+ if (definition) {
349
+ // ── New-format source definition ──
350
+ await syncNewFormat({
351
+ definition,
352
+ sourceName: rule.source,
353
+ rule,
354
+ graph,
355
+ collectionName,
356
+ client,
357
+ existingSourceRefs,
358
+ schemaRegistry,
359
+ workspacePath,
360
+ schemaExtensions,
361
+ result,
362
+ onProgress,
363
+ });
364
+ }
365
+ else {
366
+ // ── Old-format source config (backwards compatibility) ──
367
+ await syncOldFormat({
368
+ config: oldConfig,
369
+ rule,
370
+ graph,
371
+ collectionName,
372
+ client,
373
+ existingSourceRefs,
374
+ schemaRegistry,
375
+ workspacePath,
376
+ schemaExtensions,
377
+ result,
378
+ onProgress,
379
+ });
380
+ }
381
+ }
382
+ finally {
383
+ await client.close();
384
+ }
385
+ return result;
386
+ }
387
+ async function syncNewFormat(options) {
388
+ const { definition, sourceName, rule, graph, collectionName, client, existingSourceRefs, schemaRegistry, workspacePath, schemaExtensions, result, onProgress } = options;
389
+ const log = onProgress ?? (() => { });
390
+ // Find primary entity config
391
+ const primaryType = rule.entity_type ?? findPrimaryType(definition);
392
+ const primaryConfig = definition.entities[primaryType];
393
+ if (!primaryConfig) {
394
+ throw new Error(`Entity type "${primaryType}" not found in source definition`);
395
+ }
396
+ // Convert to EntityMapping for the extractor
397
+ const mapping = sourceEntityConfigToMapping(primaryType, primaryConfig, sourceName, collectionName, rule.filter);
398
+ // Extract records
399
+ const syncState = new SyncStateManager(workspacePath);
400
+ const oldStyleConfig = {
401
+ name: sourceName,
402
+ connector: definition.connector,
403
+ enabled: true,
404
+ added_at: new Date().toISOString(),
405
+ entity_mappings: [mapping],
406
+ };
407
+ log(` Extracting ${primaryType} records...`);
408
+ let records;
409
+ if (primaryConfig.extraction_mode === 'frontmatter') {
410
+ const extractor = new FrontmatterExtractor(client, oldStyleConfig);
411
+ records = await extractor.extract(mapping, onProgress);
412
+ }
413
+ else {
414
+ const extractor = new StructuredExtractor(client, oldStyleConfig, syncState);
415
+ records = await extractor.extract(mapping, false, onProgress);
416
+ }
417
+ log(` Extracted ${records.length} records`);
418
+ // Apply client-side filters declared by the source definition
419
+ records = applyClientFilters(records, rule.filter, definition.filter_fields ?? [], log);
420
+ // Write primary entities
421
+ log(` Writing ${records.length} records...`);
422
+ const writeResults = await writeEntities(records, sourceName, graph, existingSourceRefs, log);
423
+ result.created += writeResults.created;
424
+ result.skipped += writeResults.skipped;
425
+ result.errors.push(...writeResults.errors);
426
+ // Derive secondary entities
427
+ for (const [entityType, entityConfig] of Object.entries(definition.entities)) {
428
+ if (entityType === primaryType || !entityConfig.derive_from)
429
+ continue;
430
+ log(` Deriving ${entityType} entities...`);
431
+ const derived = deriveEntities(records, entityType, entityConfig, sourceName);
432
+ log(` Found ${derived.length} ${entityType} candidates`);
433
+ // Dedup derived entities against existing collection
434
+ const existingDedupeValues = entityConfig.dedupe_field
435
+ ? buildDedupeIndex(graph, entityType, entityConfig.dedupe_field)
436
+ : new Map();
437
+ const dedupedDerived = derived.filter(rec => {
438
+ // Skip if source_ref already exists
439
+ if (existingSourceRefs.has(rec.source_ref))
440
+ return false;
441
+ // Skip if dedupe field matches existing entity
442
+ if (entityConfig.dedupe_field) {
443
+ const dedupeVal = rec.frontmatter[entityConfig.dedupe_field];
444
+ if (dedupeVal && existingDedupeValues.has(String(dedupeVal).toLowerCase()))
445
+ return false;
446
+ }
447
+ return true;
448
+ });
449
+ const derivedResults = await writeEntities(dedupedDerived, sourceName, graph, existingSourceRefs, log);
450
+ result.derived += derivedResults.created;
451
+ result.skipped += derivedResults.skipped;
452
+ result.errors.push(...derivedResults.errors);
453
+ }
454
+ }
455
+ async function syncOldFormat(options) {
456
+ const { config, rule, graph, collectionName, client, existingSourceRefs, schemaRegistry, workspacePath, result, onProgress } = options;
457
+ const log = onProgress ?? (() => { });
458
+ const syncState = new SyncStateManager(workspacePath);
459
+ for (const mapping of config.entity_mappings) {
460
+ // Override target_repo to write to this collection
461
+ const adjustedMapping = { ...mapping, target_repo: collectionName };
462
+ // Merge rule filter into list_params
463
+ if (rule.filter) {
464
+ const existing = adjustedMapping.list_params ?? {};
465
+ adjustedMapping.list_params = Array.isArray(existing)
466
+ ? existing.map(p => ({ ...p, ...rule.filter }))
467
+ : { ...existing, ...rule.filter };
468
+ }
469
+ log(` Extracting ${mapping.entity_type} from ${mapping.source_type}...`);
470
+ let records;
471
+ if (adjustedMapping.extraction_mode === 'frontmatter') {
472
+ const extractor = new FrontmatterExtractor(client, config);
473
+ records = await extractor.extract(adjustedMapping, onProgress);
474
+ }
475
+ else {
476
+ const extractor = new StructuredExtractor(client, config, syncState);
477
+ records = await extractor.extract(adjustedMapping, false, onProgress);
478
+ }
479
+ log(` Extracted ${records.length} records`);
480
+ // Apply client-side filters declared by the connector's source template
481
+ const templateDef = SOURCE_DEFINITIONS[config.connector];
482
+ records = applyClientFilters(records, rule.filter, templateDef?.filter_fields ?? [], log);
483
+ const writeResults = await writeEntities(records, config.name, graph, existingSourceRefs, log);
484
+ result.created += writeResults.created;
485
+ result.skipped += writeResults.skipped;
486
+ result.errors.push(...writeResults.errors);
487
+ }
488
+ }
489
+ async function writeEntities(records, sourceName, graph, existingSourceRefs, log) {
490
+ const results = { created: 0, skipped: 0, errors: [] };
491
+ for (const record of records) {
492
+ const sourceRef = `${sourceName}:${record.source_ref}`;
493
+ // Skip if this source_ref already exists
494
+ if (existingSourceRefs.has(sourceRef)) {
495
+ results.skipped++;
496
+ continue;
497
+ }
498
+ try {
499
+ const now = new Date().toISOString();
500
+ await graph.create({
501
+ entityType: record.entity_type,
502
+ entityId: record.entity_id,
503
+ data: {
504
+ ...record.frontmatter,
505
+ entity_type: record.entity_type,
506
+ entity_id: record.entity_id,
507
+ created_at: now,
508
+ updated_at: now,
509
+ created_by: 'sync',
510
+ updated_by: 'sync',
511
+ source_ref: sourceRef,
512
+ sync_source: sourceName,
513
+ synced_at: now,
514
+ },
515
+ content: record.content,
516
+ skipCommit: true,
517
+ });
518
+ existingSourceRefs.set(sourceRef, record.entity_id);
519
+ results.created++;
520
+ }
521
+ catch (err) {
522
+ const msg = err instanceof Error ? err.message : String(err);
523
+ // Entity already exists (duplicate entity_id) — skip rather than fail
524
+ if (msg.includes('already exists')) {
525
+ results.skipped++;
526
+ }
527
+ else {
528
+ results.errors.push(`${record.entity_id}: ${msg}`);
529
+ }
530
+ }
531
+ }
532
+ if (results.errors.length > 0) {
533
+ log(` Errors (first 5): ${results.errors.slice(0, 5).join('; ')}`);
534
+ }
535
+ log(` Created ${results.created}, skipped ${results.skipped}`);
536
+ return results;
537
+ }
538
+ // ── Utility functions ────────────────────────────────────────────────────────
539
+ /**
540
+ * Build a map of source_ref → entity_id for all entities in a collection.
541
+ */
542
+ function buildSourceRefIndex(graph) {
543
+ const index = new Map();
544
+ try {
545
+ const entities = graph.list();
546
+ for (const entity of entities) {
547
+ const sourceRef = entity.data?.source_ref;
548
+ if (sourceRef && typeof sourceRef === 'string') {
549
+ index.set(sourceRef, entity.data.entity_id);
550
+ }
551
+ }
552
+ }
553
+ catch {
554
+ // Collection may be empty
555
+ }
556
+ return index;
557
+ }
558
+ /**
559
+ * Build a map of dedupe_field values for existing entities of a given type.
560
+ */
561
+ function buildDedupeIndex(graph, entityType, dedupeField) {
562
+ const index = new Map();
563
+ try {
564
+ const entities = graph.list(entityType);
565
+ for (const entity of entities) {
566
+ const val = entity.data?.[dedupeField];
567
+ if (val && typeof val === 'string') {
568
+ index.set(val.toLowerCase(), entity.data.entity_id);
569
+ }
570
+ }
571
+ }
572
+ catch {
573
+ // Entity type folder may not exist yet
574
+ }
575
+ return index;
576
+ }
577
+ /**
578
+ * Find the primary entity type in a source definition.
579
+ */
580
+ function findPrimaryType(def) {
581
+ const entries = Object.entries(def.entities);
582
+ const primary = entries.find(([_, config]) => config.primary);
583
+ if (primary)
584
+ return primary[0];
585
+ // If only one entity type, it's implicitly primary
586
+ if (entries.length === 1)
587
+ return entries[0][0];
588
+ throw new Error('Source definition has multiple entity types but none marked as primary');
589
+ }
590
+ /**
591
+ * Convert a new-format SourceEntityConfig to an old-format EntityMapping
592
+ * so existing extractors work unchanged.
593
+ */
594
+ export function sourceEntityConfigToMapping(entityType, config, sourceName, targetRepo, filter) {
595
+ const mapping = {
596
+ source_type: config.source_type ?? entityType,
597
+ entity_type: entityType,
598
+ target_repo: targetRepo,
599
+ extraction_mode: config.extraction_mode ?? 'structured',
600
+ field_map: config.field_map,
601
+ status_map: config.status_map,
602
+ id_from: config.id_from,
603
+ content_from: config.content_from,
604
+ fetch_details: config.fetch_details,
605
+ list_limit: config.list_limit,
606
+ list_tool: config.list_tool,
607
+ read_tool: config.read_tool,
608
+ list_endpoint: config.list_endpoint,
609
+ detail_endpoint: config.detail_endpoint,
610
+ detail_params: config.detail_params,
611
+ page_size: config.page_size,
612
+ content_tool: config.content_tool,
613
+ directory_patterns: config.directory_patterns,
614
+ };
615
+ // Merge filter into list_params
616
+ if (filter || config.list_params) {
617
+ const baseParams = config.list_params ?? {};
618
+ if (filter) {
619
+ mapping.list_params = Array.isArray(baseParams)
620
+ ? baseParams.map(p => ({ ...p, ...filter }))
621
+ : { ...baseParams, ...filter };
622
+ }
623
+ else {
624
+ mapping.list_params = baseParams;
625
+ }
626
+ }
627
+ return mapping;
628
+ }
629
+ /**
630
+ * Apply client-side filters to extracted records based on source filter field definitions.
631
+ * Fields with mode='client' are matched against frontmatter after extraction.
632
+ */
633
+ function applyClientFilters(records, filter, filterFieldDefs, log) {
634
+ if (!filter || filterFieldDefs.length === 0)
635
+ return records;
636
+ let result = records;
637
+ for (const fieldDef of filterFieldDefs) {
638
+ if (fieldDef.mode !== 'client')
639
+ continue;
640
+ const filterValue = filter[fieldDef.key];
641
+ if (!filterValue)
642
+ continue;
643
+ const keyword = String(filterValue).toLowerCase();
644
+ const before = result.length;
645
+ const checkFields = fieldDef.fields ?? ['title', 'name'];
646
+ // Normalize for comparison: strip wikilink brackets, replace hyphens with spaces
647
+ const normalize = (s) => s.toLowerCase().replace(/^\[\[|\]\]$/g, '').replace(/-/g, ' ');
648
+ const normalizedKeyword = normalize(keyword);
649
+ result = result.filter(rec => {
650
+ for (const field of checkFields) {
651
+ const val = rec.frontmatter[field];
652
+ if (val != null) {
653
+ // Handle array values (e.g. folders: ["Ridge AI", "Other"])
654
+ const values = Array.isArray(val) ? val.map(v => normalize(String(v))) : [normalize(String(val))];
655
+ for (const str of values) {
656
+ if (fieldDef.op === 'exact' ? str === normalizedKeyword : str.includes(normalizedKeyword))
657
+ return true;
658
+ }
659
+ }
660
+ }
661
+ return normalize(rec.entity_id).includes(normalizedKeyword);
662
+ });
663
+ log(` Filter "${fieldDef.key}=${filterValue}": ${result.length} of ${before} match`);
664
+ }
665
+ return result;
666
+ }
667
+ /**
668
+ * Derive secondary entities from primary extracted records.
669
+ * Uses the derive_from field in the source entity config to find
670
+ * the source field containing the data to derive from.
671
+ */
672
+ function deriveEntities(primaryRecords, entityType, config, sourceName) {
673
+ if (!config.derive_from)
674
+ return [];
675
+ const derived = [];
676
+ const seenIds = new Set();
677
+ for (const record of primaryRecords) {
678
+ const sourceData = record.frontmatter[config.derive_from];
679
+ if (!sourceData)
680
+ continue;
681
+ // Handle both single values and arrays
682
+ const items = Array.isArray(sourceData) ? sourceData : [sourceData];
683
+ for (const item of items) {
684
+ if (!item)
685
+ continue;
686
+ const frontmatter = { entity_type: entityType };
687
+ // Apply field map to the derived item
688
+ for (const [sourceField, target] of Object.entries(config.field_map)) {
689
+ const value = typeof item === 'object' ? resolveFieldPath(item, sourceField) : item;
690
+ if (value === undefined || value === null)
691
+ continue;
692
+ if (typeof target === 'string') {
693
+ frontmatter[target] = value;
694
+ }
695
+ else {
696
+ const transform = target;
697
+ frontmatter[transform.field] = applyTransform(value, transform.transform);
698
+ }
699
+ }
700
+ // Generate entity_id
701
+ const idField = config.id_from ?? 'name';
702
+ const idValue = frontmatter[idField] ?? frontmatter.name ?? frontmatter.email;
703
+ if (!idValue)
704
+ continue;
705
+ const entityId = slugify(String(idValue));
706
+ if (!entityId || seenIds.has(entityId))
707
+ continue;
708
+ seenIds.add(entityId);
709
+ frontmatter.entity_id = entityId;
710
+ // Generate synthetic source_ref
711
+ const sourceRef = `#${entityType}/${entityId}`;
712
+ derived.push({
713
+ entity_type: entityType,
714
+ entity_id: entityId,
715
+ target_repo: record.target_repo,
716
+ frontmatter,
717
+ content: '',
718
+ source_name: sourceName,
719
+ source_ref: sourceRef,
720
+ confidence: 0.7,
721
+ });
722
+ }
723
+ }
724
+ return derived;
725
+ }
726
+ //# sourceMappingURL=collection-sync.js.map