@lucern/graph-primitives 1.0.29 → 1.0.30

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 (319) hide show
  1. package/dist/{beliefDecay-DZ6tkLYq.d.ts → beliefDecay-BmkEk5OJ.d.ts} +3 -3
  2. package/dist/beliefDecay.d.ts +1 -1
  3. package/dist/beliefDecay.js +448 -314
  4. package/dist/beliefDecay.js.map +1 -1
  5. package/dist/{beliefEvidenceLinks-CWOXxxJg.d.ts → beliefEvidenceLinks-BzfjON_6.d.ts} +13 -13
  6. package/dist/beliefEvidenceLinks.d.ts +1 -1
  7. package/dist/beliefEvidenceLinks.js +843 -624
  8. package/dist/beliefEvidenceLinks.js.map +1 -1
  9. package/dist/beliefEvidenceLinks.operational.d.ts +7 -5
  10. package/dist/beliefEvidenceLinks.operational.js +91 -18
  11. package/dist/beliefEvidenceLinks.operational.js.map +1 -1
  12. package/dist/beliefLifecycle.js.map +1 -1
  13. package/dist/confidencePropagationDispatch.d.ts +28 -27
  14. package/dist/confidencePropagationDispatch.js +157 -99
  15. package/dist/confidencePropagationDispatch.js.map +1 -1
  16. package/dist/{contradictions-51VLsESq.d.ts → contradictions-BATPuZTL.d.ts} +10 -10
  17. package/dist/contradictions.d.ts +1 -1
  18. package/dist/contradictions.js +395 -225
  19. package/dist/contradictions.js.map +1 -1
  20. package/dist/convex.d.ts +65 -30
  21. package/dist/convex.js +7 -3
  22. package/dist/convex.js.map +1 -1
  23. package/dist/debug.js.map +1 -1
  24. package/dist/edgeValidation.js +293 -85
  25. package/dist/edgeValidation.js.map +1 -1
  26. package/dist/edges/contains.d.ts +1 -1
  27. package/dist/edges/contains.js.map +1 -1
  28. package/dist/edges/contradicts.d.ts +1 -1
  29. package/dist/edges/contradicts.js.map +1 -1
  30. package/dist/edges/{dependsOn.d.ts → depends-on.d.ts} +1 -1
  31. package/dist/edges/{dependsOn.js → depends-on.js} +4 -4
  32. package/dist/edges/depends-on.js.map +1 -0
  33. package/dist/edges/{derivedFrom.d.ts → derived-from.d.ts} +1 -1
  34. package/dist/edges/{derivedFrom.js → derived-from.js} +3 -3
  35. package/dist/edges/derived-from.js.map +1 -0
  36. package/dist/edges/elaborates.d.ts +1 -1
  37. package/dist/edges/elaborates.js.map +1 -1
  38. package/dist/edges/index.d.ts +7 -3
  39. package/dist/edges/index.js +7 -4
  40. package/dist/edges/index.js.map +1 -1
  41. package/dist/edges/informs.d.ts +1 -1
  42. package/dist/edges/informs.js.map +1 -1
  43. package/dist/edges/{propagationTypes.d.ts → propagation-types.d.ts} +14 -14
  44. package/dist/edges/{propagationTypes.js → propagation-types.js} +3 -3
  45. package/dist/edges/propagation-types.js.map +1 -0
  46. package/dist/edges/refutes.d.ts +1 -1
  47. package/dist/edges/refutes.js.map +1 -1
  48. package/dist/edges/supports.d.ts +1 -1
  49. package/dist/edges/supports.js.map +1 -1
  50. package/dist/edges/tests.d.ts +1 -1
  51. package/dist/edges/tests.js.map +1 -1
  52. package/dist/edges/utils.d.ts +1 -1
  53. package/dist/edges/utils.js.map +1 -1
  54. package/dist/embeddingTrigger.d.ts +14 -6
  55. package/dist/embeddingTrigger.js +11 -14
  56. package/dist/embeddingTrigger.js.map +1 -1
  57. package/dist/{entityBridge-DMaKooYn.d.ts → entityBridge-BhVDM3pc.d.ts} +5 -5
  58. package/dist/entityBridge.d.ts +1 -1
  59. package/dist/entityBridge.js +602 -225
  60. package/dist/entityBridge.js.map +1 -1
  61. package/dist/entityCanonicalMatch.d.ts +14 -12
  62. package/dist/entityCanonicalMatch.js.map +1 -1
  63. package/dist/{entityLifecycle-CvgSK5FV.d.ts → entityLifecycle-BsfCz9pS.d.ts} +5 -9
  64. package/dist/entityLifecycle.d.ts +1 -1
  65. package/dist/entityLifecycle.js +854 -480
  66. package/dist/entityLifecycle.js.map +1 -1
  67. package/dist/{entityValidation-KLZ_Xl2D.d.ts → entityValidation-B1yNEHJx.d.ts} +7 -6
  68. package/dist/entityValidation.d.ts +3 -1
  69. package/dist/entityValidation.js +60 -8
  70. package/dist/entityValidation.js.map +1 -1
  71. package/dist/{epistemicAnswers-C5ib4z6_.d.ts → epistemicAnswers-f47YMu9U.d.ts} +6 -6
  72. package/dist/epistemicAnswers.d.ts +1 -1
  73. package/dist/epistemicAnswers.js +587 -545
  74. package/dist/epistemicAnswers.js.map +1 -1
  75. package/dist/epistemicBeliefs.admin.d.ts +8 -8
  76. package/dist/epistemicBeliefs.admin.js +365 -166
  77. package/dist/epistemicBeliefs.admin.js.map +1 -1
  78. package/dist/epistemicBeliefs.backfills.d.ts +8 -8
  79. package/dist/epistemicBeliefs.backfills.js +655 -289
  80. package/dist/epistemicBeliefs.backfills.js.map +1 -1
  81. package/dist/epistemicBeliefs.confidence.d.ts +19 -15
  82. package/dist/epistemicBeliefs.confidence.js +633 -386
  83. package/dist/epistemicBeliefs.confidence.js.map +1 -1
  84. package/dist/epistemicBeliefs.core.d.ts +6 -6
  85. package/dist/epistemicBeliefs.core.js +717 -371
  86. package/dist/epistemicBeliefs.core.js.map +1 -1
  87. package/dist/epistemicBeliefs.d.ts +11 -9
  88. package/dist/epistemicBeliefs.forkEvidence.d.ts +2 -0
  89. package/dist/epistemicBeliefs.forkEvidence.js +8 -8
  90. package/dist/epistemicBeliefs.forkEvidence.js.map +1 -1
  91. package/dist/epistemicBeliefs.helpers.d.ts +68 -49
  92. package/dist/epistemicBeliefs.helpers.js +358 -211
  93. package/dist/epistemicBeliefs.helpers.js.map +1 -1
  94. package/dist/epistemicBeliefs.internal.d.ts +5 -5
  95. package/dist/epistemicBeliefs.internal.js +1248 -1026
  96. package/dist/epistemicBeliefs.internal.js.map +1 -1
  97. package/dist/epistemicBeliefs.js +4942 -3590
  98. package/dist/epistemicBeliefs.js.map +1 -1
  99. package/dist/epistemicBeliefs.lifecycle.d.ts +5 -5
  100. package/dist/epistemicBeliefs.lifecycle.js +1138 -781
  101. package/dist/epistemicBeliefs.lifecycle.js.map +1 -1
  102. package/dist/epistemicBeliefs.links.d.ts +7 -7
  103. package/dist/epistemicBeliefs.links.js +404 -267
  104. package/dist/epistemicBeliefs.links.js.map +1 -1
  105. package/dist/epistemicBeliefs.queries.d.ts +4 -4
  106. package/dist/epistemicBeliefs.queries.js +175 -20
  107. package/dist/epistemicBeliefs.queries.js.map +1 -1
  108. package/dist/epistemicBeliefs.topicAnchor.d.ts +6 -4
  109. package/dist/epistemicBeliefs.topicAnchor.js +12 -5
  110. package/dist/epistemicBeliefs.topicAnchor.js.map +1 -1
  111. package/dist/epistemicContracts.d.ts +28 -3
  112. package/dist/epistemicContracts.evaluators.d.ts +2 -0
  113. package/dist/epistemicContracts.evaluators.js +1062 -576
  114. package/dist/epistemicContracts.evaluators.js.map +1 -1
  115. package/dist/epistemicContracts.handlers.d.ts +15 -32
  116. package/dist/epistemicContracts.handlers.js +1829 -1351
  117. package/dist/epistemicContracts.handlers.js.map +1 -1
  118. package/dist/epistemicContracts.js +1131 -636
  119. package/dist/epistemicContracts.js.map +1 -1
  120. package/dist/epistemicContracts.metrics.d.ts +2 -0
  121. package/dist/epistemicContracts.metrics.js +375 -158
  122. package/dist/epistemicContracts.metrics.js.map +1 -1
  123. package/dist/epistemicContracts.types.d.ts +87 -81
  124. package/dist/epistemicEdgeCreation.d.ts +2 -0
  125. package/dist/epistemicEdgeCreation.js +87 -16
  126. package/dist/epistemicEdgeCreation.js.map +1 -1
  127. package/dist/{epistemicEdges-BF-cn4i3.d.ts → epistemicEdges-BGBh0QSP.d.ts} +4 -7
  128. package/dist/epistemicEdges.d.ts +6 -5
  129. package/dist/epistemicEdges.handlers.d.ts +3 -3
  130. package/dist/epistemicEdges.handlers.js +129 -24
  131. package/dist/epistemicEdges.handlers.js.map +1 -1
  132. package/dist/epistemicEdges.helpers.d.ts +6 -4
  133. package/dist/epistemicEdges.helpers.js +37 -2
  134. package/dist/epistemicEdges.helpers.js.map +1 -1
  135. package/dist/epistemicEdges.js +1966 -1202
  136. package/dist/epistemicEdges.js.map +1 -1
  137. package/dist/epistemicEdges.mutations.d.ts +7 -7
  138. package/dist/epistemicEdges.mutations.js +956 -579
  139. package/dist/epistemicEdges.mutations.js.map +1 -1
  140. package/dist/epistemicEdges.queries.d.ts +16 -16
  141. package/dist/epistemicEdges.queries.js +639 -367
  142. package/dist/epistemicEdges.queries.js.map +1 -1
  143. package/dist/epistemicEdges.types.d.ts +10 -8
  144. package/dist/epistemicEvidence.d.ts +4 -1
  145. package/dist/epistemicEvidence.js +933 -532
  146. package/dist/epistemicEvidence.js.map +1 -1
  147. package/dist/epistemicEvidenceHelpers.d.ts +26 -10
  148. package/dist/epistemicEvidenceHelpers.js +239 -200
  149. package/dist/epistemicEvidenceHelpers.js.map +1 -1
  150. package/dist/epistemicEvidenceMutations.d.ts +8 -8
  151. package/dist/epistemicEvidenceMutations.js +840 -692
  152. package/dist/epistemicEvidenceMutations.js.map +1 -1
  153. package/dist/epistemicEvidenceQueries.d.ts +8 -8
  154. package/dist/epistemicEvidenceQueries.js +514 -238
  155. package/dist/epistemicEvidenceQueries.js.map +1 -1
  156. package/dist/epistemicHelpers.d.ts +4 -2
  157. package/dist/epistemicHelpers.js +308 -134
  158. package/dist/epistemicHelpers.js.map +1 -1
  159. package/dist/epistemicInsert.d.ts +16 -4
  160. package/dist/epistemicInsert.js +6 -3
  161. package/dist/epistemicInsert.js.map +1 -1
  162. package/dist/epistemicLayerRules.d.ts +10 -8
  163. package/dist/epistemicLayerRules.js +1 -5
  164. package/dist/epistemicLayerRules.js.map +1 -1
  165. package/dist/{epistemicLinking-CfE00tHJ.d.ts → epistemicLinking-CsCDv2cN.d.ts} +3 -3
  166. package/dist/epistemicLinking.d.ts +1 -1
  167. package/dist/epistemicLinking.js +177 -100
  168. package/dist/epistemicLinking.js.map +1 -1
  169. package/dist/epistemicNodeCreation.d.ts +2 -0
  170. package/dist/epistemicNodeCreation.js +203 -40
  171. package/dist/epistemicNodeCreation.js.map +1 -1
  172. package/dist/{epistemicNodes-BCQxpYx_.d.ts → epistemicNodes-CokAgBHg.d.ts} +3 -3
  173. package/dist/epistemicNodes.d.ts +3 -3
  174. package/dist/epistemicNodes.helpers.d.ts +24 -15
  175. package/dist/epistemicNodes.helpers.js.map +1 -1
  176. package/dist/epistemicNodes.internal.d.ts +6 -6
  177. package/dist/epistemicNodes.internal.js +389 -319
  178. package/dist/epistemicNodes.internal.js.map +1 -1
  179. package/dist/epistemicNodes.js +700 -504
  180. package/dist/epistemicNodes.js.map +1 -1
  181. package/dist/epistemicNodes.mutations.d.ts +6 -6
  182. package/dist/epistemicNodes.mutations.js +560 -463
  183. package/dist/epistemicNodes.mutations.js.map +1 -1
  184. package/dist/epistemicNodes.queries.d.ts +8 -8
  185. package/dist/epistemicNodes.queries.js +311 -314
  186. package/dist/epistemicNodes.queries.js.map +1 -1
  187. package/dist/epistemicNodes.validators.d.ts +2 -2
  188. package/dist/epistemicNodes.validators.js.map +1 -1
  189. package/dist/epistemicQuestions.conviction.d.ts +8 -8
  190. package/dist/epistemicQuestions.conviction.js +665 -484
  191. package/dist/epistemicQuestions.conviction.js.map +1 -1
  192. package/dist/epistemicQuestions.create.d.ts +4 -4
  193. package/dist/epistemicQuestions.create.js +640 -612
  194. package/dist/epistemicQuestions.create.js.map +1 -1
  195. package/dist/epistemicQuestions.d.ts +8 -5
  196. package/dist/epistemicQuestions.evidence.d.ts +2 -2
  197. package/dist/epistemicQuestions.evidence.js +475 -383
  198. package/dist/epistemicQuestions.evidence.js.map +1 -1
  199. package/dist/epistemicQuestions.helpers.d.ts +125 -24
  200. package/dist/epistemicQuestions.helpers.js +240 -209
  201. package/dist/epistemicQuestions.helpers.js.map +1 -1
  202. package/dist/epistemicQuestions.js +3474 -2823
  203. package/dist/epistemicQuestions.js.map +1 -1
  204. package/dist/epistemicQuestions.lifecycle.d.ts +2 -2
  205. package/dist/epistemicQuestions.lifecycle.js +607 -546
  206. package/dist/epistemicQuestions.lifecycle.js.map +1 -1
  207. package/dist/epistemicQuestions.queries.d.ts +12 -7
  208. package/dist/epistemicQuestions.queries.js +305 -244
  209. package/dist/epistemicQuestions.queries.js.map +1 -1
  210. package/dist/epistemicQuestions.sprint.d.ts +2 -2
  211. package/dist/epistemicQuestions.sprint.js +600 -394
  212. package/dist/epistemicQuestions.sprint.js.map +1 -1
  213. package/dist/epistemicQuestions.tail.d.ts +6 -6
  214. package/dist/epistemicQuestions.tail.js +572 -433
  215. package/dist/epistemicQuestions.tail.js.map +1 -1
  216. package/dist/{epistemicSources-dlKj58Jp.d.ts → epistemicSources-DQtaEkWs.d.ts} +4 -4
  217. package/dist/epistemicSources.d.ts +1 -1
  218. package/dist/epistemicSources.js +351 -311
  219. package/dist/epistemicSources.js.map +1 -1
  220. package/dist/evaluators/index.d.ts +8 -6
  221. package/dist/evaluators/index.js +399 -167
  222. package/dist/evaluators/index.js.map +1 -1
  223. package/dist/evaluators/lint-checker-evaluator.d.ts +16 -0
  224. package/dist/evaluators/{lintCheckerEvaluator.js → lint-checker-evaluator.js} +10 -5
  225. package/dist/evaluators/lint-checker-evaluator.js.map +1 -0
  226. package/dist/evaluators/{sentryCheckerEvaluator.d.ts → sentry-checker-evaluator.d.ts} +7 -2
  227. package/dist/evaluators/{sentryCheckerEvaluator.js → sentry-checker-evaluator.js} +3 -3
  228. package/dist/evaluators/sentry-checker-evaluator.js.map +1 -0
  229. package/dist/evaluators/shared.d.ts +2 -2
  230. package/dist/evaluators/shared.js +3 -1
  231. package/dist/evaluators/shared.js.map +1 -1
  232. package/dist/evaluators/{testRunnerEvaluator.d.ts → test-runner-evaluator.d.ts} +6 -1
  233. package/dist/evaluators/{testRunnerEvaluator.js → test-runner-evaluator.js} +6 -4
  234. package/dist/evaluators/test-runner-evaluator.js.map +1 -0
  235. package/dist/evaluators/tsc-checker-evaluator.d.ts +16 -0
  236. package/dist/evaluators/{tscCheckerEvaluator.js → tsc-checker-evaluator.js} +10 -5
  237. package/dist/evaluators/tsc-checker-evaluator.js.map +1 -0
  238. package/dist/graphTypes.js +6 -2
  239. package/dist/graphTypes.js.map +1 -1
  240. package/dist/helpers.d.ts +2 -0
  241. package/dist/helpers.js +313 -93
  242. package/dist/helpers.js.map +1 -1
  243. package/dist/{index-C-Kyd7hD.d.ts → index-DZxyC9Pb.d.ts} +7 -6
  244. package/dist/index.d.ts +86 -83
  245. package/dist/index.js +16914 -11760
  246. package/dist/index.js.map +1 -1
  247. package/dist/invariantEnforcement.d.ts +3 -3
  248. package/dist/invariantEnforcement.js.map +1 -1
  249. package/dist/logicalRoleInference.d.ts +2 -0
  250. package/dist/logicalRoleInference.js +1 -1
  251. package/dist/logicalRoleInference.js.map +1 -1
  252. package/dist/matcherFeedbackUtils.d.ts +2 -2
  253. package/dist/matcherFeedbackUtils.js.map +1 -1
  254. package/dist/{ontology-matching-C6rrz2VP.d.ts → ontology-matching-C-mYFrir.d.ts} +16 -16
  255. package/dist/ontology-matching.d.ts +1 -1
  256. package/dist/{ontologyApproval-CFYmqKmk.d.ts → ontologyApproval-BVt0feJi.d.ts} +10 -10
  257. package/dist/ontologyApproval.d.ts +1 -1
  258. package/dist/ontologyApproval.js +7 -1
  259. package/dist/ontologyApproval.js.map +1 -1
  260. package/dist/ontologyDefinitions.d.ts +14 -24
  261. package/dist/ontologyDefinitions.js +269 -34
  262. package/dist/ontologyDefinitions.js.map +1 -1
  263. package/dist/ontologyHelpers.d.ts +13 -13
  264. package/dist/ontologyHelpers.js.map +1 -1
  265. package/dist/{ontologyRegistry-B67rPJ16.d.ts → ontologyRegistry-CljS-ENv.d.ts} +2 -2
  266. package/dist/ontologyRegistry.d.ts +1 -1
  267. package/dist/ontologyRegistry.js +34 -6
  268. package/dist/ontologyRegistry.js.map +1 -1
  269. package/dist/{projectionReconciliation-jww2fBI0.d.ts → projectionReconciliation-DnrSgHSQ.d.ts} +4 -4
  270. package/dist/projectionReconciliation.d.ts +1 -1
  271. package/dist/projectionReconciliation.js +57 -10
  272. package/dist/projectionReconciliation.js.map +1 -1
  273. package/dist/{projectionStaleness-CmdbpjVK.d.ts → projectionStaleness-C8ImQ2zP.d.ts} +17 -17
  274. package/dist/projectionStaleness.d.ts +1 -1
  275. package/dist/projectionStaleness.js +8 -2
  276. package/dist/projectionStaleness.js.map +1 -1
  277. package/dist/proof-attestation.json +1 -1
  278. package/dist/{questionEvidenceLinks-DFlyPpAj.d.ts → questionEvidenceLinks-_nPRa-LY.d.ts} +10 -10
  279. package/dist/questionEvidenceLinks.d.ts +1 -1
  280. package/dist/questionEvidenceLinks.js +564 -347
  281. package/dist/questionEvidenceLinks.js.map +1 -1
  282. package/dist/{resolverTypes-CC8Ea2E2.d.ts → resolverTypes-BOXPxLET.d.ts} +8 -7
  283. package/dist/resolverTypes.d.ts +4 -2
  284. package/dist/{resolvers-Br1a6eLV.d.ts → resolvers-B1TIBmRO.d.ts} +3 -1
  285. package/dist/resolvers.d.ts +5 -3
  286. package/dist/resolvers.js +121 -77
  287. package/dist/resolvers.js.map +1 -1
  288. package/dist/scopeResolverCompat.d.ts +10 -7
  289. package/dist/scopeResolverCompat.js +106 -123
  290. package/dist/scopeResolverCompat.js.map +1 -1
  291. package/dist/{text-matching-DNg4M5Wd.d.ts → text-matching-DzFooju6.d.ts} +7 -7
  292. package/dist/text-matching.d.ts +1 -1
  293. package/dist/topicOntologyResolver.d.ts +22 -21
  294. package/dist/topicOntologyResolver.js +54 -32
  295. package/dist/topicOntologyResolver.js.map +1 -1
  296. package/dist/topicProjectOverlay.d.ts +30 -20
  297. package/dist/topicProjectOverlay.js +120 -76
  298. package/dist/topicProjectOverlay.js.map +1 -1
  299. package/dist/{topicScope-7zhyeGl7.d.ts → topicScope-DJVa0mLa.d.ts} +22 -7
  300. package/dist/topicScope.d.ts +3 -1
  301. package/dist/topicScope.js +104 -119
  302. package/dist/topicScope.js.map +1 -1
  303. package/dist/workflowBridge.d.ts +26 -15
  304. package/dist/workflowBridge.js +140 -144
  305. package/dist/workflowBridge.js.map +1 -1
  306. package/dist/workspaceIsolation.d.ts +14 -12
  307. package/dist/workspaceIsolation.js +108 -122
  308. package/dist/workspaceIsolation.js.map +1 -1
  309. package/package.json +4 -4
  310. package/dist/edges/dependsOn.js.map +0 -1
  311. package/dist/edges/derivedFrom.js.map +0 -1
  312. package/dist/edges/propagationTypes.js.map +0 -1
  313. package/dist/evaluators/lintCheckerEvaluator.d.ts +0 -11
  314. package/dist/evaluators/lintCheckerEvaluator.js.map +0 -1
  315. package/dist/evaluators/sentryCheckerEvaluator.js.map +0 -1
  316. package/dist/evaluators/testRunnerEvaluator.js.map +0 -1
  317. package/dist/evaluators/tscCheckerEvaluator.d.ts +0 -11
  318. package/dist/evaluators/tscCheckerEvaluator.js.map +0 -1
  319. package/dist/{epistemicQuestions-bwHd2FWE.d.ts → epistemicQuestions-Do1fhYm5.d.ts} +4 -4
@@ -1,17 +1,25 @@
1
- import { v } from 'convex/values';
2
- import { componentsGeneric, anyApi, mutationGeneric, internalMutationGeneric, queryGeneric } from 'convex/server';
3
1
  import { checkScopeAccess } from '@lucern/access-control/access';
4
- import { generateGlobalId, assertUuidV7Identity } from '@lucern/contracts/ids';
5
2
  import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
3
+ import { v } from 'convex/values';
4
+ import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
5
+ import { componentsGeneric, mutationGeneric, internalMutationGeneric, queryGeneric } from 'convex/server';
6
6
  import '@lucern/contracts';
7
+ import { generateGlobalId, assertUuidV7Identity } from '@lucern/contracts/ids';
7
8
 
8
9
  // src/epistemicAnswers.ts
9
- var api = anyApi;
10
+ var unsafeApi = unsafeConvexAnyApi(
11
+ "graph-primitives top-level module bundle lacks a committed Convex _generated/api surface"
12
+ );
13
+ var api = unsafeApi;
10
14
  componentsGeneric();
11
- var internal = anyApi;
15
+ var internal = unsafeApi;
12
16
  var internalMutation = internalMutationGeneric;
13
17
  var mutation = mutationGeneric;
14
18
  var query = queryGeneric;
19
+ function insertEpistemicNode(ctx, doc) {
20
+ assertUuidV7Identity("epistemicNodes", doc.globalId);
21
+ return ctx.db.insert("epistemicNodes", doc);
22
+ }
15
23
 
16
24
  // src/debug.ts
17
25
  function isGraphPrimitiveDebugEnabled() {
@@ -24,8 +32,6 @@ function debugGraphPrimitiveFallback(message, context) {
24
32
  }
25
33
  console.debug(message, context ?? {});
26
34
  }
27
-
28
- // src/topicScope.ts
29
35
  var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
30
36
  async function resolveTopicNodeScopeOrNull(ctx, ref) {
31
37
  if (!ctx?.db || typeof ctx.db.query !== "function") {
@@ -60,13 +66,15 @@ function asMappedProjectId(topic) {
60
66
  if (!topic) {
61
67
  return;
62
68
  }
63
- const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD]);
69
+ const directLegacyProjectId = normalizeScopeValue(
70
+ topic[LEGACY_SCOPE_FIELD]
71
+ );
64
72
  if (directLegacyProjectId) {
65
73
  return directLegacyProjectId;
66
74
  }
67
75
  const metadata = topic.metadata || {};
68
76
  const candidate = metadata[LEGACY_SCOPE_FIELD] || metadata.legacyProjectId || metadata.projectId || metadata.scopeProjectId;
69
- return candidate ? candidate : void 0;
77
+ return typeof candidate === "string" ? normalizeScopeValue(candidate) : void 0;
70
78
  }
71
79
  function normalizeScopeValue(value) {
72
80
  if (typeof value !== "string") {
@@ -91,8 +99,9 @@ function pickPrimaryTopic(candidates) {
91
99
  })[0];
92
100
  }
93
101
  async function findTopicsByScopeAlias(ctx, scopeId) {
102
+ const query2 = ctx.db.query("topics");
94
103
  try {
95
- return await ctx.db.query("topics").withIndex(
104
+ return await query2.withIndex(
96
105
  "by_graph_scope_project",
97
106
  (q) => q.eq(LEGACY_SCOPE_FIELD, scopeId)
98
107
  ).collect();
@@ -104,7 +113,7 @@ async function findTopicsByScopeAlias(ctx, scopeId) {
104
113
  scopeId
105
114
  }
106
115
  );
107
- const topics = await ctx.db.query("topics").collect();
116
+ const topics = await query2.collect();
108
117
  return topics.filter((topic) => {
109
118
  const normalizedGlobalId = normalizeScopeValue(topic.globalId);
110
119
  const mappedProjectId = asMappedProjectId(topic);
@@ -160,137 +169,115 @@ async function resolveInheritedWorkspaceScope(ctx, topic) {
160
169
  let current = topic;
161
170
  for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
162
171
  current = await ctx.db.get(current.parentTopicId);
163
- if (!current) break;
172
+ if (!current) {
173
+ break;
174
+ }
164
175
  if (!tenantId) {
165
176
  tenantId = normalizeScopeValue(current.tenantId);
166
177
  }
167
178
  if (!workspaceId) {
168
179
  workspaceId = normalizeScopeValue(current.workspaceId);
169
180
  }
170
- if (tenantId && workspaceId) break;
181
+ if (tenantId && workspaceId) {
182
+ break;
183
+ }
171
184
  }
172
185
  return { tenantId, workspaceId };
173
186
  }
174
187
  async function resolveTopicProjectScope(ctx, args) {
175
188
  if (args.topicId) {
176
- let topic = null;
177
- try {
178
- topic = await ctx.db.get(
179
- args.topicId
180
- );
181
- } catch (error) {
182
- debugGraphPrimitiveFallback(
183
- "[topicScope] Failed to load topic by direct id",
184
- {
185
- error,
186
- topicId: args.topicId
187
- }
188
- );
189
- }
190
- if (!topic) {
191
- topic = await tryResolveHostTopicById(ctx, String(args.topicId));
192
- }
193
- if (!topic) {
194
- topic = pickPrimaryTopic(
195
- await findTopicsByScopeAlias(ctx, String(args.topicId))
196
- ) ?? null;
197
- }
198
- if (!topic) {
199
- const nodeScope = await resolveTopicNodeScopeOrNull(
200
- ctx,
201
- String(args.topicId)
202
- );
203
- if (nodeScope) {
204
- return nodeScope;
205
- }
206
- throw new Error(`Topic not found: ${String(args.topicId)}`);
207
- }
208
- const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
209
- const mapped = asMappedProjectId(topic);
210
- if (mapped) {
211
- return {
212
- topicId: topic._id,
213
- projectId: mapped,
214
- tenantId: inherited.tenantId,
215
- workspaceId: inherited.workspaceId,
216
- source: "topic"
217
- };
218
- }
219
- return {
220
- topicId: topic._id,
221
- tenantId: inherited.tenantId,
222
- workspaceId: inherited.workspaceId,
223
- source: "topic"
224
- };
189
+ return await resolveScopeFromTopicId(ctx, args.topicId);
225
190
  }
226
191
  if (args.projectId) {
227
- let directTopic = null;
228
- try {
229
- directTopic = await ctx.db.get(
230
- args.projectId
231
- );
232
- } catch (error) {
233
- debugGraphPrimitiveFallback(
234
- "[topicScope] Failed to load direct project topic",
235
- {
236
- error,
237
- projectId: args.projectId
238
- }
239
- );
240
- }
241
- if (directTopic) {
242
- const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
243
- const mapped = asMappedProjectId(directTopic);
244
- return {
245
- topicId: directTopic._id,
246
- projectId: mapped ?? args.projectId,
247
- tenantId: inherited.tenantId,
248
- workspaceId: inherited.workspaceId,
249
- source: "topic_inferred"
250
- };
251
- }
252
- directTopic = await tryResolveHostTopicByLegacyScope(ctx, args.projectId);
253
- if (directTopic) {
254
- const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
255
- const mapped = asMappedProjectId(directTopic);
256
- return {
257
- topicId: directTopic._id,
258
- projectId: mapped ?? args.projectId,
259
- tenantId: inherited.tenantId,
260
- workspaceId: inherited.workspaceId,
261
- source: "topic_inferred"
262
- };
263
- }
264
- const topics = await findTopicsByScopeAlias(ctx, args.projectId);
265
- const primary = pickPrimaryTopic(topics);
266
- if (primary) {
267
- const inherited = await resolveInheritedWorkspaceScope(ctx, primary);
268
- return {
269
- topicId: primary._id,
270
- projectId: args.projectId,
271
- tenantId: inherited.tenantId,
272
- workspaceId: inherited.workspaceId,
273
- source: "project_mapped_topic"
274
- };
275
- }
276
- const nodeScope = await resolveTopicNodeScopeOrNull(
277
- ctx,
278
- String(args.projectId)
279
- );
280
- if (nodeScope) {
281
- return {
282
- ...nodeScope,
283
- projectId: nodeScope.projectId ?? String(args.projectId)
284
- };
285
- }
286
- throw new Error(
287
- `Legacy project scope ${String(args.projectId)} has no mapped topic.`
288
- );
192
+ return await resolveScopeFromLegacyProjectId(ctx, args.projectId);
289
193
  }
290
194
  throw new Error(
291
195
  "Missing scope: provide topicId (preferred) or legacy projectId alias."
292
196
  );
293
197
  }
198
+ async function resolveScopeFromTopicId(ctx, topicId) {
199
+ const topic = await resolveTopicDocFromTopicId(ctx, topicId);
200
+ if (topic) {
201
+ return await buildTopicScope(ctx, topic, "topic");
202
+ }
203
+ const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
204
+ if (nodeScope) {
205
+ return nodeScope;
206
+ }
207
+ throw new Error(`Topic not found: ${String(topicId)}`);
208
+ }
209
+ async function resolveTopicDocFromTopicId(ctx, topicId) {
210
+ const direct = await tryReadTopicDoc(ctx, topicId, {
211
+ failureLog: "[topicScope] Failed to load topic by direct id",
212
+ idLogKey: "topicId"
213
+ });
214
+ if (direct) {
215
+ return direct;
216
+ }
217
+ const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
218
+ if (hostTopic) {
219
+ return hostTopic;
220
+ }
221
+ return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
222
+ }
223
+ async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
224
+ const directTopic = await resolveDirectLegacyProjectTopic(
225
+ ctx,
226
+ legacyProjectId
227
+ );
228
+ if (directTopic) {
229
+ return await buildTopicScope(ctx, directTopic, "topic_inferred", {
230
+ fallbackProjectId: legacyProjectId
231
+ });
232
+ }
233
+ const primary = pickPrimaryTopic(
234
+ await findTopicsByScopeAlias(ctx, legacyProjectId)
235
+ );
236
+ if (primary) {
237
+ return await buildTopicScope(ctx, primary, "project_mapped_topic", {
238
+ fallbackProjectId: legacyProjectId
239
+ });
240
+ }
241
+ const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
242
+ if (nodeScope) {
243
+ return {
244
+ ...nodeScope,
245
+ projectId: nodeScope.projectId ?? legacyProjectId
246
+ };
247
+ }
248
+ throw new Error(
249
+ `Legacy project scope ${legacyProjectId} has no mapped topic.`
250
+ );
251
+ }
252
+ async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
253
+ const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
254
+ failureLog: "[topicScope] Failed to load direct project topic",
255
+ idLogKey: "projectId"
256
+ });
257
+ return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
258
+ }
259
+ async function tryReadTopicDoc(ctx, id, log) {
260
+ try {
261
+ return await ctx.db.get(id);
262
+ } catch (error) {
263
+ debugGraphPrimitiveFallback(log.failureLog, {
264
+ error,
265
+ [log.idLogKey]: id
266
+ });
267
+ return null;
268
+ }
269
+ }
270
+ async function buildTopicScope(ctx, topic, source, options = {}) {
271
+ const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
272
+ const mapped = asMappedProjectId(topic);
273
+ return {
274
+ topicId: topic._id,
275
+ ...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
276
+ tenantId: inherited.tenantId,
277
+ workspaceId: inherited.workspaceId,
278
+ source
279
+ };
280
+ }
294
281
  var optionalScopeArgs = {
295
282
  projectId: v.optional(v.string()),
296
283
  topicId: v.optional(v.string())
@@ -308,7 +295,7 @@ function normalizeScopeValue2(value) {
308
295
  async function resolveScope(ctx, args) {
309
296
  const topicId = normalizeScopeValue2(args.topicId);
310
297
  const projectId = normalizeScopeValue2(args.projectId);
311
- if (!topicId && !projectId) {
298
+ if (!(topicId || projectId)) {
312
299
  return null;
313
300
  }
314
301
  try {
@@ -328,12 +315,84 @@ async function resolveScope(ctx, args) {
328
315
  return null;
329
316
  }
330
317
  }
331
- async function insertEpistemicNode(ctx, doc) {
332
- assertUuidV7Identity("epistemicNodes", doc.globalId);
333
- return ctx.db.insert("epistemicNodes", doc);
334
- }
335
318
 
336
319
  // src/epistemicAnswers.ts
320
+ function isRecord(value) {
321
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
322
+ }
323
+ function readConvexId(value) {
324
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
325
+ }
326
+ function readOptionalNumber(value) {
327
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
328
+ }
329
+ function readOptionalString(value) {
330
+ return typeof value === "string" && value.trim().length > 0 ? value : void 0;
331
+ }
332
+ function readRecord(value) {
333
+ return isRecord(value) ? value : void 0;
334
+ }
335
+ function readAnswerNodeRow(value) {
336
+ if (!isRecord(value)) {
337
+ return null;
338
+ }
339
+ const id = readConvexId(value._id);
340
+ const globalId = readOptionalString(value.globalId);
341
+ if (!(id && globalId)) {
342
+ return null;
343
+ }
344
+ const row = { _id: id, globalId };
345
+ const canonicalText = readOptionalString(value.canonicalText);
346
+ const createdAt = readOptionalNumber(value.createdAt);
347
+ const createdBy = readOptionalString(value.createdBy);
348
+ const metadata = readRecord(value.metadata);
349
+ const nodeType = readOptionalString(value.nodeType);
350
+ const projectId = readOptionalString(value.projectId);
351
+ const status = readOptionalString(value.status);
352
+ const supersededBy = readConvexId(value.supersededBy);
353
+ const topicId = readConvexId(value.topicId);
354
+ if (canonicalText !== void 0) {
355
+ row.canonicalText = canonicalText;
356
+ }
357
+ if (createdAt !== void 0) {
358
+ row.createdAt = createdAt;
359
+ }
360
+ if (createdBy !== void 0) {
361
+ row.createdBy = createdBy;
362
+ }
363
+ if (metadata !== void 0) {
364
+ row.metadata = metadata;
365
+ }
366
+ if (nodeType !== void 0) {
367
+ row.nodeType = nodeType;
368
+ }
369
+ if (projectId !== void 0) {
370
+ row.projectId = projectId;
371
+ }
372
+ if (status !== void 0) {
373
+ row.status = status;
374
+ }
375
+ if (supersededBy !== void 0) {
376
+ row.supersededBy = supersededBy;
377
+ }
378
+ if (topicId !== void 0) {
379
+ row.topicId = topicId;
380
+ }
381
+ return row;
382
+ }
383
+ function readAnswerEdgeRow(value) {
384
+ if (!isRecord(value)) {
385
+ return null;
386
+ }
387
+ const fromNodeId = readConvexId(value.fromNodeId);
388
+ return fromNodeId ? { fromNodeId } : null;
389
+ }
390
+ function readRowList(values, reader) {
391
+ return values.flatMap((value) => {
392
+ const row = reader(value);
393
+ return row ? [row] : [];
394
+ });
395
+ }
337
396
  function generateContentHash(text) {
338
397
  const content = `answer:${text.trim().toLowerCase().replace(/\s+/g, " ").slice(0, 500)}`;
339
398
  let hash = 5381;
@@ -343,6 +402,283 @@ function generateContentHash(text) {
343
402
  }
344
403
  return Math.abs(hash).toString(16).padStart(8, "0");
345
404
  }
405
+ async function getActiveAnswersForQuestion(ctx, questionNode, topicId, questionNodeId) {
406
+ const answers = readRowList(
407
+ await ctx.db.query("epistemicNodes").withIndex(
408
+ "by_topic_type",
409
+ (q) => q.eq("topicId", questionNode.topicId ?? topicId).eq("nodeType", "answer")
410
+ ).collect(),
411
+ readAnswerNodeRow
412
+ );
413
+ return answers.filter((answerNode) => {
414
+ const answerMeta = answerNode.metadata ?? {};
415
+ return answerMeta.questionNodeId === questionNodeId && answerMeta.isLatest === true && answerNode.status === "active";
416
+ });
417
+ }
418
+ function getNextAnswerVersion(activeAnswers) {
419
+ if (activeAnswers.length === 0) {
420
+ return 1;
421
+ }
422
+ return Math.max(
423
+ ...activeAnswers.map((answerNode) => {
424
+ const answerMeta = answerNode.metadata ?? {};
425
+ return readOptionalNumber(answerMeta.versionNumber) ?? 0;
426
+ })
427
+ ) + 1;
428
+ }
429
+ async function supersedeActiveAnswers(ctx, activeAnswers, now, clearSupersededByBeforeInsert) {
430
+ for (const activeAnswer of activeAnswers) {
431
+ const metadata = {
432
+ ...activeAnswer.metadata ?? {},
433
+ isLatest: false
434
+ };
435
+ await ctx.db.patch(activeAnswer._id, {
436
+ status: "superseded",
437
+ updatedAt: now,
438
+ metadata,
439
+ ...clearSupersededByBeforeInsert ? {
440
+ supersededBy: void 0
441
+ } : {}
442
+ });
443
+ }
444
+ }
445
+ async function collectEvidenceGlobalIds(ctx, evidenceNodeIds) {
446
+ const evidenceGlobalIds = [];
447
+ if (!evidenceNodeIds) {
448
+ return evidenceGlobalIds;
449
+ }
450
+ for (const evidenceNodeId of evidenceNodeIds) {
451
+ const evidenceNode = readAnswerNodeRow(await ctx.db.get(evidenceNodeId));
452
+ if (evidenceNode) {
453
+ evidenceGlobalIds.push(evidenceNode.globalId);
454
+ }
455
+ }
456
+ return evidenceGlobalIds;
457
+ }
458
+ function resolveAnswerSource(answerSource) {
459
+ return answerSource === "human" ? "human" : "ai_generated";
460
+ }
461
+ function insertAnswerNodeRecord(ctx, args) {
462
+ return insertEpistemicNode(ctx, {
463
+ globalId: args.globalId,
464
+ projectId: args.topicId,
465
+ topicId: args.questionNode.topicId,
466
+ nodeType: "answer",
467
+ epistemicLayer: "L2",
468
+ canonicalText: args.answerText,
469
+ contentHash: args.contentHash,
470
+ status: "active",
471
+ sourceType: resolveAnswerSource(args.args.answerSource),
472
+ createdBy: args.args.userId,
473
+ createdAt: args.now,
474
+ updatedAt: args.now,
475
+ metadata: {
476
+ questionGlobalId: args.questionNode.globalId,
477
+ questionNodeId: String(args.questionNode._id),
478
+ versionNumber: args.versionNumber,
479
+ isLatest: true,
480
+ evidenceGlobalIds: args.evidenceGlobalIds,
481
+ evidenceCount: args.evidenceGlobalIds.length,
482
+ answeredBy: args.args.userId,
483
+ answerSource: args.args.answerSource || "human",
484
+ sprintId: args.args.sprintId,
485
+ ...args.args.backfilledFromQuestionId ? {
486
+ backfilledFromQuestionId: args.args.backfilledFromQuestionId
487
+ } : {}
488
+ }
489
+ });
490
+ }
491
+ async function setAnswerSupersession(ctx, activeAnswers, newNodeId) {
492
+ for (const activeAnswer of activeAnswers) {
493
+ await ctx.db.patch(activeAnswer._id, {
494
+ supersededBy: newNodeId
495
+ });
496
+ }
497
+ }
498
+ async function scheduleAnswerSyncJobs(ctx, args) {
499
+ await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
500
+ nodeId: args.nodeId,
501
+ operation: "upsert"
502
+ });
503
+ await ctx.scheduler.runAfter(
504
+ 0,
505
+ api.embeddingActions.generateEpistemicNodeEmbedding,
506
+ {
507
+ nodeId: args.nodeId,
508
+ projectId: args.topicId,
509
+ topicId: args.questionNode.topicId ? String(args.questionNode.topicId) : void 0,
510
+ createdBy: args.userId,
511
+ nodeType: "answer",
512
+ text: args.answerText.slice(0, 2e4)
513
+ }
514
+ );
515
+ await ctx.scheduler.runAfter(
516
+ 0,
517
+ internal.sprints.embedProjectThemeCollectiveInternal,
518
+ {
519
+ projectId: args.topicId,
520
+ userId: args.userId
521
+ }
522
+ );
523
+ }
524
+ async function scheduleAnswerGraphEdges(ctx, args) {
525
+ await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
526
+ globalId: generateGlobalId(),
527
+ fromGlobalId: args.answerGlobalId,
528
+ toGlobalId: args.questionNode.globalId,
529
+ edgeType: "responds_to",
530
+ weight: 1,
531
+ createdBy: args.userId,
532
+ topicId: args.edgeTopicId,
533
+ fromNodeType: "answer",
534
+ toNodeType: "question",
535
+ fromLayer: "L2",
536
+ toLayer: "L3"
537
+ });
538
+ for (const activeAnswer of args.activeAnswers) {
539
+ await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
540
+ globalId: generateGlobalId(),
541
+ fromGlobalId: args.answerGlobalId,
542
+ toGlobalId: activeAnswer.globalId,
543
+ edgeType: "supersedes",
544
+ createdBy: args.userId,
545
+ topicId: args.edgeTopicId,
546
+ fromNodeType: "answer",
547
+ toNodeType: "answer",
548
+ fromLayer: "L2",
549
+ toLayer: "L2"
550
+ });
551
+ }
552
+ if (args.evidenceNodeIds) {
553
+ for (const evidenceNodeId of args.evidenceNodeIds) {
554
+ const evidenceNode = readAnswerNodeRow(await ctx.db.get(evidenceNodeId));
555
+ if (evidenceNode) {
556
+ await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
557
+ globalId: generateGlobalId(),
558
+ fromGlobalId: args.answerGlobalId,
559
+ toGlobalId: evidenceNode.globalId,
560
+ edgeType: "derived_from",
561
+ createdBy: args.userId,
562
+ topicId: args.edgeTopicId,
563
+ fromNodeType: "answer",
564
+ toNodeType: "evidence",
565
+ fromLayer: "L2",
566
+ toLayer: "L2"
567
+ });
568
+ }
569
+ }
570
+ }
571
+ const evidenceEdges = readRowList(
572
+ await ctx.db.query("epistemicEdges").withIndex(
573
+ "by_to_type",
574
+ (q) => q.eq("toNodeId", args.questionNodeId).eq("edgeType", "derived_from")
575
+ ).collect(),
576
+ readAnswerEdgeRow
577
+ );
578
+ for (const evidenceEdge of evidenceEdges) {
579
+ const evidenceNode = readAnswerNodeRow(
580
+ await ctx.db.get(evidenceEdge.fromNodeId)
581
+ );
582
+ if (!evidenceNode) {
583
+ continue;
584
+ }
585
+ await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
586
+ globalId: generateGlobalId(),
587
+ fromGlobalId: evidenceNode.globalId,
588
+ toGlobalId: args.answerGlobalId,
589
+ edgeType: "derived_from",
590
+ createdBy: args.userId,
591
+ topicId: args.edgeTopicId,
592
+ fromNodeType: "evidence",
593
+ toNodeType: "answer",
594
+ fromLayer: "L2",
595
+ toLayer: "L2"
596
+ });
597
+ }
598
+ }
599
+ async function createAnswerMutationResult(ctx, args) {
600
+ const now = Date.now();
601
+ const answerText = args.answerText;
602
+ const globalId = generateGlobalId();
603
+ const contentHash = generateContentHash(answerText);
604
+ const activeAnswers = await getActiveAnswersForQuestion(
605
+ ctx,
606
+ args.questionNode,
607
+ args.topicId,
608
+ args.questionNodeId
609
+ );
610
+ const versionNumber = getNextAnswerVersion(activeAnswers);
611
+ await supersedeActiveAnswers(
612
+ ctx,
613
+ activeAnswers,
614
+ now,
615
+ !!args.clearSupersededByBeforeInsert
616
+ );
617
+ const evidenceGlobalIds = await collectEvidenceGlobalIds(
618
+ ctx,
619
+ args.evidenceNodeIds
620
+ );
621
+ const nodeId = await insertAnswerNodeRecord(ctx, {
622
+ questionNode: args.questionNode,
623
+ topicId: args.topicId,
624
+ answerText,
625
+ contentHash,
626
+ versionNumber,
627
+ evidenceGlobalIds,
628
+ globalId,
629
+ now,
630
+ args: {
631
+ answerSource: args.answerSource,
632
+ sprintId: args.sprintId,
633
+ userId: args.userId,
634
+ backfilledFromQuestionId: args.backfilledFromQuestionId
635
+ }
636
+ });
637
+ await setAnswerSupersession(ctx, activeAnswers, nodeId);
638
+ await scheduleAnswerSyncJobs(ctx, {
639
+ nodeId,
640
+ topicId: args.topicId,
641
+ questionNode: args.questionNode,
642
+ userId: args.userId,
643
+ answerText
644
+ });
645
+ await scheduleAnswerGraphEdges(ctx, {
646
+ questionNode: args.questionNode,
647
+ questionNodeId: args.questionNodeId,
648
+ userId: args.userId,
649
+ evidenceNodeIds: args.evidenceNodeIds,
650
+ activeAnswers,
651
+ answerGlobalId: globalId,
652
+ edgeTopicId: args.edgeTopicId
653
+ });
654
+ const answerQuality = args.confidence || "moderate";
655
+ await ctx.db.patch(args.questionNodeId, {
656
+ answerQuality,
657
+ updatedAt: now
658
+ });
659
+ const auditState = args.backfilledFromQuestionId ? {
660
+ answerText: answerText.slice(0, 200),
661
+ versionNumber,
662
+ questionNodeId: String(args.questionNodeId),
663
+ backfilled: true
664
+ } : {
665
+ answerText: answerText.slice(0, 200),
666
+ versionNumber,
667
+ questionNodeId: String(args.questionNodeId),
668
+ confidence: answerQuality
669
+ };
670
+ await ctx.db.insert("epistemicAudit", {
671
+ entityType: "answer",
672
+ entityId: nodeId,
673
+ changeType: "created",
674
+ changedAt: now,
675
+ changedBy: args.userId,
676
+ isAgent: false,
677
+ projectId: args.topicId,
678
+ newState: auditState
679
+ });
680
+ return { nodeId, globalId, versionNumber };
681
+ }
346
682
  var create = mutation({
347
683
  args: {
348
684
  ...scopeArgs,
@@ -367,190 +703,25 @@ var create = mutation({
367
703
  const scope = await resolveScope(ctx, args);
368
704
  const topicId = scope?.topicId ? String(scope.topicId) : args.topicId || "";
369
705
  await checkScopeAccess(ctx, topicId, args.userId);
370
- const questionNode = await ctx.db.get(args.questionNodeId);
371
- if (!questionNode || questionNode.nodeType !== "question") {
372
- throw new Error("Question node not found or not a question type");
373
- }
374
- const now = Date.now();
375
- const globalId = generateGlobalId();
376
- const contentHash = generateContentHash(args.answerText);
377
- const existingAnswers = await ctx.db.query("epistemicNodes").withIndex(
378
- "by_topic_type",
379
- (q) => q.eq("topicId", questionNode.topicId ?? topicId).eq("nodeType", "answer")
380
- ).collect();
381
- const activeAnswersForQuestion = existingAnswers.filter((n) => {
382
- const meta = n.metadata || {};
383
- return meta.questionNodeId === args.questionNodeId && meta.isLatest === true && n.status === "active";
384
- });
385
- const versionNumber = activeAnswersForQuestion.length > 0 ? Math.max(
386
- ...activeAnswersForQuestion.map((a) => {
387
- const meta = a.metadata || {};
388
- return meta.versionNumber || 0;
389
- })
390
- ) + 1 : 1;
391
- for (const oldAnswer of activeAnswersForQuestion) {
392
- await ctx.db.patch(oldAnswer._id, {
393
- status: "superseded",
394
- supersededBy: void 0,
395
- // Will be set after insert
396
- updatedAt: now,
397
- metadata: {
398
- ...oldAnswer.metadata || {},
399
- isLatest: false
400
- }
401
- });
402
- }
403
- const evidenceGlobalIds = [];
404
- if (args.evidenceNodeIds) {
405
- for (const evId of args.evidenceNodeIds) {
406
- const evNode = await ctx.db.get(evId);
407
- if (evNode) {
408
- evidenceGlobalIds.push(evNode.globalId);
409
- }
410
- }
411
- }
412
- const nodeId = await insertEpistemicNode(ctx, {
413
- globalId,
414
- projectId: topicId,
415
- topicId: questionNode.topicId,
416
- nodeType: "answer",
417
- epistemicLayer: "L2",
418
- canonicalText: args.answerText,
419
- contentHash,
420
- status: "active",
421
- sourceType: args.answerSource === "human" ? "human" : "ai_generated",
422
- createdBy: args.userId,
423
- createdAt: now,
424
- updatedAt: now,
425
- metadata: {
426
- questionGlobalId: questionNode.globalId,
427
- questionNodeId: String(args.questionNodeId),
428
- versionNumber,
429
- isLatest: true,
430
- evidenceGlobalIds,
431
- evidenceCount: evidenceGlobalIds.length,
432
- answeredBy: args.userId,
433
- answerSource: args.answerSource || "human",
434
- sprintId: args.sprintId
435
- }
436
- });
437
- for (const oldAnswer of activeAnswersForQuestion) {
438
- await ctx.db.patch(oldAnswer._id, {
439
- supersededBy: nodeId
440
- });
441
- }
442
- await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
443
- nodeId,
444
- operation: "upsert"
445
- });
446
- await ctx.scheduler.runAfter(
447
- 0,
448
- api.embeddingActions.generateEpistemicNodeEmbedding,
449
- {
450
- nodeId,
451
- projectId: topicId,
452
- topicId: questionNode.topicId ? String(questionNode.topicId) : void 0,
453
- createdBy: args.userId,
454
- nodeType: "answer",
455
- text: args.answerText.slice(0, 2e4)
456
- }
457
- );
458
- await ctx.scheduler.runAfter(
459
- 0,
460
- internal.sprints.embedProjectThemeCollectiveInternal,
461
- {
462
- projectId: topicId,
463
- userId: args.userId
464
- }
706
+ const questionNode = readAnswerNodeRow(
707
+ await ctx.db.get(args.questionNodeId)
465
708
  );
466
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
467
- globalId: generateGlobalId(),
468
- fromGlobalId: globalId,
469
- toGlobalId: questionNode.globalId,
470
- edgeType: "responds_to",
471
- weight: 1,
472
- createdBy: args.userId,
473
- topicId: String(args.topicId ?? ""),
474
- fromNodeType: "answer",
475
- toNodeType: "question",
476
- fromLayer: "L2",
477
- toLayer: "L3"
478
- });
479
- for (const oldAnswer of activeAnswersForQuestion) {
480
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
481
- globalId: generateGlobalId(),
482
- fromGlobalId: globalId,
483
- toGlobalId: oldAnswer.globalId,
484
- edgeType: "supersedes",
485
- createdBy: args.userId,
486
- topicId: String(args.topicId ?? ""),
487
- fromNodeType: "answer",
488
- toNodeType: "answer",
489
- fromLayer: "L2",
490
- toLayer: "L2"
491
- });
492
- }
493
- if (args.evidenceNodeIds) {
494
- for (const evId of args.evidenceNodeIds) {
495
- const evNode = await ctx.db.get(evId);
496
- if (evNode) {
497
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
498
- globalId: generateGlobalId(),
499
- fromGlobalId: globalId,
500
- toGlobalId: evNode.globalId,
501
- edgeType: "derived_from",
502
- createdBy: args.userId,
503
- topicId: String(args.topicId ?? ""),
504
- fromNodeType: "answer",
505
- toNodeType: "evidence",
506
- fromLayer: "L2",
507
- toLayer: "L2"
508
- });
509
- }
510
- }
511
- }
512
- const evidenceEdges = await ctx.db.query("epistemicEdges").withIndex(
513
- "by_to_type",
514
- (q) => q.eq("toNodeId", args.questionNodeId).eq("edgeType", "derived_from")
515
- ).collect();
516
- for (const edge of evidenceEdges) {
517
- const evidenceNode = await ctx.db.get(edge.fromNodeId);
518
- if (evidenceNode) {
519
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
520
- globalId: generateGlobalId(),
521
- fromGlobalId: evidenceNode.globalId,
522
- toGlobalId: globalId,
523
- edgeType: "derived_from",
524
- createdBy: args.userId,
525
- topicId: String(args.topicId ?? ""),
526
- fromNodeType: "evidence",
527
- toNodeType: "answer",
528
- fromLayer: "L2",
529
- toLayer: "L2"
530
- });
531
- }
709
+ if (questionNode?.nodeType !== "question") {
710
+ throw new Error("Question node not found or not a question type");
532
711
  }
533
- const answerQuality = args.confidence || "moderate";
534
- await ctx.db.patch(args.questionNodeId, {
535
- answerQuality,
536
- updatedAt: now
537
- });
538
- await ctx.db.insert("epistemicAudit", {
539
- entityType: "answer",
540
- entityId: nodeId,
541
- changeType: "created",
542
- changedAt: now,
543
- changedBy: args.userId,
544
- isAgent: false,
545
- projectId: topicId,
546
- newState: {
547
- answerText: args.answerText.slice(0, 200),
548
- versionNumber,
549
- questionNodeId: String(args.questionNodeId),
550
- confidence: answerQuality
551
- }
712
+ return createAnswerMutationResult(ctx, {
713
+ questionNode,
714
+ questionNodeId: args.questionNodeId,
715
+ answerText: args.answerText,
716
+ confidence: args.confidence,
717
+ evidenceNodeIds: args.evidenceNodeIds,
718
+ answerSource: args.answerSource,
719
+ sprintId: args.sprintId,
720
+ userId: args.userId,
721
+ topicId,
722
+ edgeTopicId: String(args.topicId ?? ""),
723
+ clearSupersededByBeforeInsert: true
552
724
  });
553
- return { nodeId, globalId, versionNumber };
554
725
  }
555
726
  });
556
727
  var createInternal = internalMutation({
@@ -569,189 +740,25 @@ var createInternal = internalMutation({
569
740
  handler: async (ctx, args) => {
570
741
  const scope = await resolveScope(ctx, args);
571
742
  const topicId = scope?.topicId ? String(scope.topicId) : args.topicId || "";
572
- const questionNode = await ctx.db.get(args.questionNodeId);
573
- if (!questionNode || questionNode.nodeType !== "question") {
574
- throw new Error("Question node not found or not a question type");
575
- }
576
- const now = Date.now();
577
- const globalId = generateGlobalId();
578
- const contentHash = generateContentHash(args.answerText);
579
- const existingAnswers = await ctx.db.query("epistemicNodes").withIndex(
580
- "by_topic_type",
581
- (q) => q.eq("topicId", questionNode.topicId ?? topicId).eq("nodeType", "answer")
582
- ).collect();
583
- const activeAnswersForQuestion = existingAnswers.filter((n) => {
584
- const meta = n.metadata || {};
585
- return meta.questionNodeId === String(args.questionNodeId) && meta.isLatest === true && n.status === "active";
586
- });
587
- const versionNumber = activeAnswersForQuestion.length > 0 ? Math.max(
588
- ...activeAnswersForQuestion.map((a) => {
589
- const meta = a.metadata || {};
590
- return meta.versionNumber || 0;
591
- })
592
- ) + 1 : 1;
593
- for (const oldAnswer of activeAnswersForQuestion) {
594
- await ctx.db.patch(oldAnswer._id, {
595
- status: "superseded",
596
- updatedAt: now,
597
- metadata: {
598
- ...oldAnswer.metadata || {},
599
- isLatest: false
600
- }
601
- });
602
- }
603
- const evidenceGlobalIds = [];
604
- if (args.evidenceNodeIds) {
605
- for (const evId of args.evidenceNodeIds) {
606
- const evNode = await ctx.db.get(evId);
607
- if (evNode) {
608
- evidenceGlobalIds.push(evNode.globalId);
609
- }
610
- }
611
- }
612
- const nodeId = await insertEpistemicNode(ctx, {
613
- globalId,
614
- projectId: topicId,
615
- topicId: questionNode.topicId,
616
- nodeType: "answer",
617
- epistemicLayer: "L2",
618
- canonicalText: args.answerText,
619
- contentHash,
620
- status: "active",
621
- sourceType: args.answerSource === "human" ? "human" : "ai_generated",
622
- createdBy: args.userId,
623
- createdAt: now,
624
- updatedAt: now,
625
- metadata: {
626
- questionGlobalId: questionNode.globalId,
627
- questionNodeId: String(args.questionNodeId),
628
- versionNumber,
629
- isLatest: true,
630
- evidenceGlobalIds,
631
- evidenceCount: evidenceGlobalIds.length,
632
- answeredBy: args.userId,
633
- answerSource: args.answerSource || "human",
634
- sprintId: args.sprintId,
635
- backfilledFromQuestionId: args.backfilledFromQuestionId
636
- }
637
- });
638
- for (const oldAnswer of activeAnswersForQuestion) {
639
- await ctx.db.patch(oldAnswer._id, {
640
- supersededBy: nodeId
641
- });
642
- }
643
- await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
644
- nodeId,
645
- operation: "upsert"
646
- });
647
- await ctx.scheduler.runAfter(
648
- 0,
649
- api.embeddingActions.generateEpistemicNodeEmbedding,
650
- {
651
- nodeId,
652
- projectId: topicId,
653
- topicId: questionNode.topicId ? String(questionNode.topicId) : void 0,
654
- createdBy: args.userId,
655
- nodeType: "answer",
656
- text: args.answerText.slice(0, 2e4)
657
- }
658
- );
659
- await ctx.scheduler.runAfter(
660
- 0,
661
- internal.sprints.embedProjectThemeCollectiveInternal,
662
- {
663
- projectId: topicId,
664
- userId: args.userId
665
- }
743
+ const questionNode = readAnswerNodeRow(
744
+ await ctx.db.get(args.questionNodeId)
666
745
  );
667
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
668
- globalId: generateGlobalId(),
669
- fromGlobalId: globalId,
670
- toGlobalId: questionNode.globalId,
671
- edgeType: "responds_to",
672
- weight: 1,
673
- createdBy: args.userId,
674
- topicId: String(args.topicId ?? ""),
675
- fromNodeType: "answer",
676
- toNodeType: "question",
677
- fromLayer: "L2",
678
- toLayer: "L3"
679
- });
680
- for (const oldAnswer of activeAnswersForQuestion) {
681
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
682
- globalId: generateGlobalId(),
683
- fromGlobalId: globalId,
684
- toGlobalId: oldAnswer.globalId,
685
- edgeType: "supersedes",
686
- createdBy: args.userId,
687
- topicId: String(args.topicId ?? ""),
688
- fromNodeType: "answer",
689
- toNodeType: "answer",
690
- fromLayer: "L2",
691
- toLayer: "L2"
692
- });
693
- }
694
- if (args.evidenceNodeIds) {
695
- for (const evId of args.evidenceNodeIds) {
696
- const evNode = await ctx.db.get(evId);
697
- if (evNode) {
698
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
699
- globalId: generateGlobalId(),
700
- fromGlobalId: globalId,
701
- toGlobalId: evNode.globalId,
702
- edgeType: "derived_from",
703
- createdBy: args.userId,
704
- topicId: String(args.topicId ?? ""),
705
- fromNodeType: "answer",
706
- toNodeType: "evidence",
707
- fromLayer: "L2",
708
- toLayer: "L2"
709
- });
710
- }
711
- }
712
- }
713
- const evidenceEdges = await ctx.db.query("epistemicEdges").withIndex(
714
- "by_to_type",
715
- (q) => q.eq("toNodeId", args.questionNodeId).eq("edgeType", "derived_from")
716
- ).collect();
717
- for (const edge of evidenceEdges) {
718
- const evidenceNode = await ctx.db.get(edge.fromNodeId);
719
- if (evidenceNode) {
720
- await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
721
- globalId: generateGlobalId(),
722
- fromGlobalId: evidenceNode.globalId,
723
- toGlobalId: globalId,
724
- edgeType: "derived_from",
725
- createdBy: args.userId,
726
- topicId: String(args.topicId ?? ""),
727
- fromNodeType: "evidence",
728
- toNodeType: "answer",
729
- fromLayer: "L2",
730
- toLayer: "L2"
731
- });
732
- }
746
+ if (questionNode?.nodeType !== "question") {
747
+ throw new Error("Question node not found or not a question type");
733
748
  }
734
- const answerQuality = args.confidence || "moderate";
735
- await ctx.db.patch(args.questionNodeId, {
736
- answerQuality,
737
- updatedAt: now
738
- });
739
- await ctx.db.insert("epistemicAudit", {
740
- entityType: "answer",
741
- entityId: nodeId,
742
- changeType: "created",
743
- changedAt: now,
744
- changedBy: args.userId,
745
- isAgent: false,
746
- projectId: topicId,
747
- newState: {
748
- answerText: args.answerText.slice(0, 200),
749
- versionNumber,
750
- questionNodeId: String(args.questionNodeId),
751
- backfilled: !!args.backfilledFromQuestionId
752
- }
749
+ return createAnswerMutationResult(ctx, {
750
+ questionNode,
751
+ questionNodeId: args.questionNodeId,
752
+ answerText: args.answerText,
753
+ confidence: args.confidence,
754
+ evidenceNodeIds: args.evidenceNodeIds,
755
+ answerSource: args.answerSource,
756
+ sprintId: args.sprintId,
757
+ userId: args.userId,
758
+ topicId,
759
+ edgeTopicId: String(args.topicId ?? ""),
760
+ backfilledFromQuestionId: args.backfilledFromQuestionId
753
761
  });
754
- return { nodeId, globalId, versionNumber };
755
762
  }
756
763
  });
757
764
  var getByQuestion = query({
@@ -761,30 +768,35 @@ var getByQuestion = query({
761
768
  },
762
769
  returns: permissiveReturn,
763
770
  handler: async (ctx, args) => {
764
- const questionNode = await ctx.db.get(args.questionNodeId);
765
- if (!questionNode || questionNode.nodeType !== "question") {
771
+ const questionNode = readAnswerNodeRow(
772
+ await ctx.db.get(args.questionNodeId)
773
+ );
774
+ if (questionNode?.nodeType !== "question") {
766
775
  return [];
767
776
  }
768
777
  const topicId = questionNode.topicId;
769
778
  if (!topicId) {
770
779
  return [];
771
780
  }
772
- const answers = await ctx.db.query("epistemicNodes").withIndex(
773
- "by_topic_type",
774
- (q) => q.eq("topicId", topicId).eq("nodeType", "answer")
775
- ).collect();
781
+ const answers = readRowList(
782
+ await ctx.db.query("epistemicNodes").withIndex(
783
+ "by_topic_type",
784
+ (q) => q.eq("topicId", topicId).eq("nodeType", "answer")
785
+ ).collect(),
786
+ readAnswerNodeRow
787
+ );
776
788
  const questionIdStr = String(args.questionNodeId);
777
789
  const filtered = answers.filter((a) => {
778
- const meta = a.metadata || {};
790
+ const meta = a.metadata ?? {};
779
791
  return meta.questionNodeId === questionIdStr;
780
792
  });
781
793
  if (!args.includeSuperseded) {
782
794
  return filtered.filter((a) => a.status === "active");
783
795
  }
784
796
  filtered.sort((a, b) => {
785
- const metaA = a.metadata || {};
786
- const metaB = b.metadata || {};
787
- return (metaB.versionNumber || 0) - (metaA.versionNumber || 0);
797
+ const metaA = a.metadata ?? {};
798
+ const metaB = b.metadata ?? {};
799
+ return (readOptionalNumber(metaB.versionNumber) ?? 0) - (readOptionalNumber(metaA.versionNumber) ?? 0);
788
800
  });
789
801
  return filtered;
790
802
  }
@@ -795,20 +807,26 @@ var getLatestForQuestion = query({
795
807
  },
796
808
  returns: permissiveReturn,
797
809
  handler: async (ctx, args) => {
798
- const questionNode = await ctx.db.get(args.questionNodeId);
799
- if (!questionNode || questionNode.nodeType !== "question") {
810
+ const questionNode = readAnswerNodeRow(
811
+ await ctx.db.get(args.questionNodeId)
812
+ );
813
+ if (questionNode?.nodeType !== "question") {
800
814
  return null;
801
815
  }
802
- if (!questionNode.topicId) {
816
+ const topicId = questionNode.topicId;
817
+ if (!topicId) {
803
818
  return null;
804
819
  }
805
- const answers = await ctx.db.query("epistemicNodes").withIndex(
806
- "by_topic_type",
807
- (q) => q.eq("topicId", questionNode.topicId).eq("nodeType", "answer")
808
- ).filter((q) => q.eq(q.field("status"), "active")).collect();
820
+ const answers = readRowList(
821
+ await ctx.db.query("epistemicNodes").withIndex(
822
+ "by_topic_type",
823
+ (q) => q.eq("topicId", topicId).eq("nodeType", "answer")
824
+ ).filter((q) => q.eq(q.field("status"), "active")).collect(),
825
+ readAnswerNodeRow
826
+ );
809
827
  const questionIdStr = String(args.questionNodeId);
810
828
  const latest = answers.find((a) => {
811
- const meta = a.metadata || {};
829
+ const meta = a.metadata ?? {};
812
830
  return meta.questionNodeId === questionIdStr && meta.isLatest === true;
813
831
  });
814
832
  return latest || null;
@@ -820,38 +838,44 @@ var getVersionHistory = query({
820
838
  },
821
839
  returns: permissiveReturn,
822
840
  handler: async (ctx, args) => {
823
- const questionNode = await ctx.db.get(args.questionNodeId);
824
- if (!questionNode || questionNode.nodeType !== "question") {
841
+ const questionNode = readAnswerNodeRow(
842
+ await ctx.db.get(args.questionNodeId)
843
+ );
844
+ if (questionNode?.nodeType !== "question") {
825
845
  return [];
826
846
  }
827
- if (!questionNode.topicId) {
847
+ const topicId = questionNode.topicId;
848
+ if (!topicId) {
828
849
  return [];
829
850
  }
830
- const answers = await ctx.db.query("epistemicNodes").withIndex(
831
- "by_topic_type",
832
- (q) => q.eq("topicId", questionNode.topicId).eq("nodeType", "answer")
833
- ).filter(
834
- (q) => q.or(
835
- q.eq(q.field("status"), "active"),
836
- q.eq(q.field("status"), "superseded")
837
- )
838
- ).collect();
851
+ const answers = readRowList(
852
+ await ctx.db.query("epistemicNodes").withIndex(
853
+ "by_topic_type",
854
+ (q) => q.eq("topicId", topicId).eq("nodeType", "answer")
855
+ ).filter(
856
+ (q) => q.or(
857
+ q.eq(q.field("status"), "active"),
858
+ q.eq(q.field("status"), "superseded")
859
+ )
860
+ ).collect(),
861
+ readAnswerNodeRow
862
+ );
839
863
  const questionIdStr = String(args.questionNodeId);
840
864
  const history = answers.filter((a) => {
841
- const meta = a.metadata || {};
865
+ const meta = a.metadata ?? {};
842
866
  return meta.questionNodeId === questionIdStr;
843
867
  }).map((a) => {
844
- const meta = a.metadata || {};
868
+ const meta = a.metadata ?? {};
845
869
  return {
846
870
  _id: a._id,
847
871
  globalId: a.globalId,
848
872
  answerText: a.canonicalText,
849
- versionNumber: meta.versionNumber || 1,
850
- isLatest: meta.isLatest || false,
873
+ versionNumber: readOptionalNumber(meta.versionNumber) ?? 1,
874
+ isLatest: meta.isLatest === true,
851
875
  status: a.status,
852
- answeredBy: meta.answeredBy || a.createdBy,
853
- answerSource: meta.answerSource,
854
- evidenceCount: meta.evidenceCount || 0,
876
+ answeredBy: readOptionalString(meta.answeredBy) ?? a.createdBy,
877
+ answerSource: readOptionalString(meta.answerSource),
878
+ evidenceCount: readOptionalNumber(meta.evidenceCount) ?? 0,
855
879
  createdAt: a.createdAt,
856
880
  supersededBy: a.supersededBy
857
881
  };
@@ -868,25 +892,39 @@ var backfillAnswerNodes = internalMutation({
868
892
  returns: permissiveReturn,
869
893
  handler: async (ctx, args) => {
870
894
  const scope = await resolveScope(ctx, args);
871
- const topicId = scope?.topicId ?? args.topicId;
895
+ const topicId = scope?.topicId ? String(scope.topicId) : args.topicId;
872
896
  const dryRun = args.dryRun ?? false;
873
- const questions = await ctx.db.query("epistemicNodes").withIndex(
874
- "by_topic_type",
875
- (q) => q.eq("topicId", topicId).eq("nodeType", "question")
876
- ).collect();
897
+ if (!topicId) {
898
+ return {
899
+ questionsWithAnswers: 0,
900
+ alreadyHaveNodes: 0,
901
+ needsBackfill: 0,
902
+ backfilled: 0
903
+ };
904
+ }
905
+ const questions = readRowList(
906
+ await ctx.db.query("epistemicNodes").withIndex(
907
+ "by_topic_type",
908
+ (q) => q.eq("topicId", topicId).eq("nodeType", "question")
909
+ ).collect(),
910
+ readAnswerNodeRow
911
+ );
877
912
  const questionsWithAnswers = questions.filter((q) => {
878
- const meta = q.metadata || {};
879
- const answer = meta.answer;
913
+ const meta = q.metadata ?? {};
914
+ const answer = readOptionalString(meta.answer);
880
915
  return answer && answer.trim().length > 0;
881
916
  });
882
- const existingAnswers = await ctx.db.query("epistemicNodes").withIndex(
883
- "by_topic_type",
884
- (q) => q.eq("topicId", topicId).eq("nodeType", "answer")
885
- ).collect();
917
+ const existingAnswers = readRowList(
918
+ await ctx.db.query("epistemicNodes").withIndex(
919
+ "by_topic_type",
920
+ (q) => q.eq("topicId", topicId).eq("nodeType", "answer")
921
+ ).collect(),
922
+ readAnswerNodeRow
923
+ );
886
924
  const questionIdsWithAnswerNodes = new Set(
887
925
  existingAnswers.map((a) => {
888
- const meta = a.metadata || {};
889
- return meta.questionNodeId;
926
+ const meta = a.metadata ?? {};
927
+ return readOptionalString(meta.questionNodeId);
890
928
  })
891
929
  );
892
930
  const needsBackfill = questionsWithAnswers.filter(
@@ -904,14 +942,18 @@ var backfillAnswerNodes = internalMutation({
904
942
  id: q._id,
905
943
  globalId: q.globalId,
906
944
  text: q.canonicalText?.slice(0, 80),
907
- answer: q.metadata?.answer?.slice(0, 80)
945
+ answer: readOptionalString(q.metadata?.answer)?.slice(0, 80)
908
946
  }))
909
947
  };
910
948
  }
911
949
  let created = 0;
912
950
  for (const question of needsBackfill) {
913
- const meta = question.metadata || {};
914
- const answerText = meta.answer;
951
+ const meta = question.metadata ?? {};
952
+ const answerText = readOptionalString(meta.answer);
953
+ if (!answerText) {
954
+ continue;
955
+ }
956
+ const answeredBy = readOptionalString(meta.answeredBy);
915
957
  await ctx.scheduler.runAfter(
916
958
  0,
917
959
  internal.epistemicAnswers.createInternal,
@@ -920,8 +962,8 @@ var backfillAnswerNodes = internalMutation({
920
962
  // TC-C: migrated from projectId
921
963
  questionNodeId: question._id,
922
964
  answerText,
923
- answerSource: meta.answeredBy ? "human" : "ai_generated",
924
- userId: meta.answeredBy || args.userId,
965
+ answerSource: answeredBy ? "human" : "ai_generated",
966
+ userId: answeredBy ?? args.userId,
925
967
  backfilledFromQuestionId: String(question._id)
926
968
  }
927
969
  );