@ryanstark24/sfgraph-core 1.1.8 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/dist/analyze/__tests__/audit-graph.test.js +79 -0
  2. package/dist/analyze/__tests__/audit-graph.test.js.map +1 -1
  3. package/dist/analyze/__tests__/edge-filters.test.d.ts +2 -0
  4. package/dist/analyze/__tests__/edge-filters.test.d.ts.map +1 -0
  5. package/dist/analyze/__tests__/edge-filters.test.js +66 -0
  6. package/dist/analyze/__tests__/edge-filters.test.js.map +1 -0
  7. package/dist/analyze/__tests__/find-nodes.test.d.ts +2 -0
  8. package/dist/analyze/__tests__/find-nodes.test.d.ts.map +1 -0
  9. package/dist/analyze/__tests__/find-nodes.test.js +129 -0
  10. package/dist/analyze/__tests__/find-nodes.test.js.map +1 -0
  11. package/dist/analyze/__tests__/findings.test.d.ts +2 -0
  12. package/dist/analyze/__tests__/findings.test.d.ts.map +1 -0
  13. package/dist/analyze/__tests__/findings.test.js +100 -0
  14. package/dist/analyze/__tests__/findings.test.js.map +1 -0
  15. package/dist/analyze/__tests__/governor-risks-detection.test.js +90 -0
  16. package/dist/analyze/__tests__/governor-risks-detection.test.js.map +1 -1
  17. package/dist/analyze/__tests__/security-fls-filter.test.d.ts +2 -0
  18. package/dist/analyze/__tests__/security-fls-filter.test.d.ts.map +1 -0
  19. package/dist/analyze/__tests__/security-fls-filter.test.js +23 -0
  20. package/dist/analyze/__tests__/security-fls-filter.test.js.map +1 -0
  21. package/dist/analyze/audit-graph.d.ts +26 -2
  22. package/dist/analyze/audit-graph.d.ts.map +1 -1
  23. package/dist/analyze/audit-graph.js +52 -5
  24. package/dist/analyze/audit-graph.js.map +1 -1
  25. package/dist/analyze/dependencies.d.ts +2 -2
  26. package/dist/analyze/dependencies.d.ts.map +1 -1
  27. package/dist/analyze/dependencies.js +9 -3
  28. package/dist/analyze/dependencies.js.map +1 -1
  29. package/dist/analyze/dependents.d.ts +6 -1
  30. package/dist/analyze/dependents.d.ts.map +1 -1
  31. package/dist/analyze/dependents.js +12 -2
  32. package/dist/analyze/dependents.js.map +1 -1
  33. package/dist/analyze/edge-filters.d.ts +37 -0
  34. package/dist/analyze/edge-filters.d.ts.map +1 -0
  35. package/dist/analyze/edge-filters.js +51 -0
  36. package/dist/analyze/edge-filters.js.map +1 -0
  37. package/dist/analyze/find-nodes.d.ts +38 -0
  38. package/dist/analyze/find-nodes.d.ts.map +1 -0
  39. package/dist/analyze/find-nodes.js +51 -0
  40. package/dist/analyze/find-nodes.js.map +1 -0
  41. package/dist/analyze/findings.d.ts +86 -0
  42. package/dist/analyze/findings.d.ts.map +1 -0
  43. package/dist/analyze/findings.js +141 -0
  44. package/dist/analyze/findings.js.map +1 -0
  45. package/dist/analyze/index.d.ts +2 -0
  46. package/dist/analyze/index.d.ts.map +1 -1
  47. package/dist/analyze/index.js +2 -0
  48. package/dist/analyze/index.js.map +1 -1
  49. package/dist/analyze/populate.d.ts.map +1 -1
  50. package/dist/analyze/populate.js +55 -10
  51. package/dist/analyze/populate.js.map +1 -1
  52. package/dist/analyze/security.d.ts +15 -0
  53. package/dist/analyze/security.d.ts.map +1 -1
  54. package/dist/analyze/security.js +59 -3
  55. package/dist/analyze/security.js.map +1 -1
  56. package/dist/analyze/wip-impact.d.ts.map +1 -1
  57. package/dist/analyze/wip-impact.js +12 -2
  58. package/dist/analyze/wip-impact.js.map +1 -1
  59. package/dist/domain/edge-fact.d.ts +25 -0
  60. package/dist/domain/edge-fact.d.ts.map +1 -1
  61. package/dist/domain/metadata-category.d.ts +1 -0
  62. package/dist/domain/metadata-category.d.ts.map +1 -1
  63. package/dist/domain/metadata-category.js +1 -0
  64. package/dist/domain/metadata-category.js.map +1 -1
  65. package/dist/domain/rel-types.d.ts +4 -0
  66. package/dist/domain/rel-types.d.ts.map +1 -1
  67. package/dist/domain/rel-types.js +7 -0
  68. package/dist/domain/rel-types.js.map +1 -1
  69. package/dist/embedding/__tests__/embed.test.d.ts +2 -0
  70. package/dist/embedding/__tests__/embed.test.d.ts.map +1 -0
  71. package/dist/embedding/__tests__/embed.test.js +72 -0
  72. package/dist/embedding/__tests__/embed.test.js.map +1 -0
  73. package/dist/embedding/embed.d.ts.map +1 -1
  74. package/dist/embedding/embed.js +14 -1
  75. package/dist/embedding/embed.js.map +1 -1
  76. package/dist/extractors/interfaces/metadata-source.d.ts +11 -0
  77. package/dist/extractors/interfaces/metadata-source.d.ts.map +1 -1
  78. package/dist/extractors/live-org/__tests__/bulk-retrieve.test.js.map +1 -1
  79. package/dist/extractors/live-org/__tests__/custom-settings.test.d.ts +2 -0
  80. package/dist/extractors/live-org/__tests__/custom-settings.test.d.ts.map +1 -0
  81. package/dist/extractors/live-org/__tests__/custom-settings.test.js +79 -0
  82. package/dist/extractors/live-org/__tests__/custom-settings.test.js.map +1 -0
  83. package/dist/extractors/live-org/__tests__/extractor-enrichment.test.js +70 -8
  84. package/dist/extractors/live-org/__tests__/extractor-enrichment.test.js.map +1 -1
  85. package/dist/extractors/live-org/__tests__/liveness.test.d.ts +2 -0
  86. package/dist/extractors/live-org/__tests__/liveness.test.d.ts.map +1 -0
  87. package/dist/extractors/live-org/__tests__/liveness.test.js +148 -0
  88. package/dist/extractors/live-org/__tests__/liveness.test.js.map +1 -0
  89. package/dist/extractors/live-org/__tests__/mcd-baseline.test.d.ts +2 -0
  90. package/dist/extractors/live-org/__tests__/mcd-baseline.test.d.ts.map +1 -0
  91. package/dist/extractors/live-org/__tests__/mcd-baseline.test.js +177 -0
  92. package/dist/extractors/live-org/__tests__/mcd-baseline.test.js.map +1 -0
  93. package/dist/extractors/live-org/__tests__/metadata-read-batch.test.d.ts +2 -0
  94. package/dist/extractors/live-org/__tests__/metadata-read-batch.test.d.ts.map +1 -0
  95. package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js +75 -0
  96. package/dist/extractors/live-org/__tests__/metadata-read-batch.test.js.map +1 -0
  97. package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.d.ts +2 -0
  98. package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.d.ts.map +1 -0
  99. package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js +135 -0
  100. package/dist/extractors/live-org/__tests__/omnistudio-retrieve.test.js.map +1 -0
  101. package/dist/extractors/live-org/__tests__/pools.test.js +4 -1
  102. package/dist/extractors/live-org/__tests__/pools.test.js.map +1 -1
  103. package/dist/extractors/live-org/__tests__/rate-limit.test.js +1 -6
  104. package/dist/extractors/live-org/__tests__/rate-limit.test.js.map +1 -1
  105. package/dist/extractors/live-org/__tests__/soql-rebatch.test.d.ts +2 -0
  106. package/dist/extractors/live-org/__tests__/soql-rebatch.test.d.ts.map +1 -0
  107. package/dist/extractors/live-org/__tests__/soql-rebatch.test.js +118 -0
  108. package/dist/extractors/live-org/__tests__/soql-rebatch.test.js.map +1 -0
  109. package/dist/extractors/live-org/__tests__/stop-waiting.test.d.ts +2 -0
  110. package/dist/extractors/live-org/__tests__/stop-waiting.test.d.ts.map +1 -0
  111. package/dist/extractors/live-org/__tests__/stop-waiting.test.js +243 -0
  112. package/dist/extractors/live-org/__tests__/stop-waiting.test.js.map +1 -0
  113. package/dist/extractors/live-org/__tests__/vlocity-runner.test.js +27 -0
  114. package/dist/extractors/live-org/__tests__/vlocity-runner.test.js.map +1 -1
  115. package/dist/extractors/live-org/__tests__/watchdog-budgets.test.d.ts +2 -0
  116. package/dist/extractors/live-org/__tests__/watchdog-budgets.test.d.ts.map +1 -0
  117. package/dist/extractors/live-org/__tests__/watchdog-budgets.test.js +40 -0
  118. package/dist/extractors/live-org/__tests__/watchdog-budgets.test.js.map +1 -0
  119. package/dist/extractors/live-org/__tests__/wedge-isolation.test.d.ts +2 -0
  120. package/dist/extractors/live-org/__tests__/wedge-isolation.test.d.ts.map +1 -0
  121. package/dist/extractors/live-org/__tests__/wedge-isolation.test.js +270 -0
  122. package/dist/extractors/live-org/__tests__/wedge-isolation.test.js.map +1 -0
  123. package/dist/extractors/live-org/bulk-retrieve.d.ts +176 -0
  124. package/dist/extractors/live-org/bulk-retrieve.d.ts.map +1 -1
  125. package/dist/extractors/live-org/bulk-retrieve.js +509 -71
  126. package/dist/extractors/live-org/bulk-retrieve.js.map +1 -1
  127. package/dist/extractors/live-org/capabilities.d.ts.map +1 -1
  128. package/dist/extractors/live-org/capabilities.js.map +1 -1
  129. package/dist/extractors/live-org/extractors/__tests__/apex.test.d.ts +2 -0
  130. package/dist/extractors/live-org/extractors/__tests__/apex.test.d.ts.map +1 -0
  131. package/dist/extractors/live-org/extractors/__tests__/apex.test.js +73 -0
  132. package/dist/extractors/live-org/extractors/__tests__/apex.test.js.map +1 -0
  133. package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.d.ts +2 -0
  134. package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.d.ts.map +1 -0
  135. package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.js +183 -0
  136. package/dist/extractors/live-org/extractors/__tests__/lwc.parallel.test.js.map +1 -0
  137. package/dist/extractors/live-org/extractors/apex.d.ts.map +1 -1
  138. package/dist/extractors/live-org/extractors/apex.js +76 -51
  139. package/dist/extractors/live-org/extractors/apex.js.map +1 -1
  140. package/dist/extractors/live-org/extractors/custom-settings.d.ts +3 -0
  141. package/dist/extractors/live-org/extractors/custom-settings.d.ts.map +1 -0
  142. package/dist/extractors/live-org/extractors/custom-settings.js +66 -0
  143. package/dist/extractors/live-org/extractors/custom-settings.js.map +1 -0
  144. package/dist/extractors/live-org/extractors/flow.d.ts.map +1 -1
  145. package/dist/extractors/live-org/extractors/flow.js.map +1 -1
  146. package/dist/extractors/live-org/extractors/generic-metadata.d.ts.map +1 -1
  147. package/dist/extractors/live-org/extractors/generic-metadata.js.map +1 -1
  148. package/dist/extractors/live-org/extractors/generic-sobject.js.map +1 -1
  149. package/dist/extractors/live-org/extractors/lwc.d.ts.map +1 -1
  150. package/dist/extractors/live-org/extractors/lwc.js +81 -41
  151. package/dist/extractors/live-org/extractors/lwc.js.map +1 -1
  152. package/dist/extractors/live-org/extractors/mcd-baseline.d.ts +60 -0
  153. package/dist/extractors/live-org/extractors/mcd-baseline.d.ts.map +1 -0
  154. package/dist/extractors/live-org/extractors/mcd-baseline.js +118 -0
  155. package/dist/extractors/live-org/extractors/mcd-baseline.js.map +1 -0
  156. package/dist/extractors/live-org/extractors/object.d.ts.map +1 -1
  157. package/dist/extractors/live-org/extractors/object.js.map +1 -1
  158. package/dist/extractors/live-org/extractors/omnistudio-retrieve.d.ts +23 -0
  159. package/dist/extractors/live-org/extractors/omnistudio-retrieve.d.ts.map +1 -0
  160. package/dist/extractors/live-org/extractors/omnistudio-retrieve.js +172 -0
  161. package/dist/extractors/live-org/extractors/omnistudio-retrieve.js.map +1 -0
  162. package/dist/extractors/live-org/extractors/omnistudio.d.ts +8 -1
  163. package/dist/extractors/live-org/extractors/omnistudio.d.ts.map +1 -1
  164. package/dist/extractors/live-org/extractors/omnistudio.js +59 -6
  165. package/dist/extractors/live-org/extractors/omnistudio.js.map +1 -1
  166. package/dist/extractors/live-org/rate-limit.d.ts +98 -15
  167. package/dist/extractors/live-org/rate-limit.d.ts.map +1 -1
  168. package/dist/extractors/live-org/rate-limit.js +278 -41
  169. package/dist/extractors/live-org/rate-limit.js.map +1 -1
  170. package/dist/extractors/live-org/vlocity/runner.d.ts +1 -1
  171. package/dist/extractors/live-org/vlocity/runner.d.ts.map +1 -1
  172. package/dist/extractors/live-org/vlocity/runner.js +49 -14
  173. package/dist/extractors/live-org/vlocity/runner.js.map +1 -1
  174. package/dist/index.d.ts +1 -0
  175. package/dist/index.d.ts.map +1 -1
  176. package/dist/index.js +1 -0
  177. package/dist/index.js.map +1 -1
  178. package/dist/ingest/__tests__/config-values.test.d.ts +2 -0
  179. package/dist/ingest/__tests__/config-values.test.d.ts.map +1 -0
  180. package/dist/ingest/__tests__/config-values.test.js +65 -0
  181. package/dist/ingest/__tests__/config-values.test.js.map +1 -0
  182. package/dist/ingest/__tests__/full-sync-deletion.test.js +11 -4
  183. package/dist/ingest/__tests__/full-sync-deletion.test.js.map +1 -1
  184. package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.d.ts +10 -0
  185. package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.d.ts.map +1 -0
  186. package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.js +251 -0
  187. package/dist/ingest/__tests__/live-ingest.detect-deletions-guard.test.js.map +1 -0
  188. package/dist/ingest/__tests__/live-ingest.test.js.map +1 -1
  189. package/dist/ingest/__tests__/multi-org-orchestrator.test.js +3 -0
  190. package/dist/ingest/__tests__/multi-org-orchestrator.test.js.map +1 -1
  191. package/dist/ingest/__tests__/parser-dispatch.test.d.ts +2 -0
  192. package/dist/ingest/__tests__/parser-dispatch.test.d.ts.map +1 -0
  193. package/dist/ingest/__tests__/parser-dispatch.test.js +55 -0
  194. package/dist/ingest/__tests__/parser-dispatch.test.js.map +1 -0
  195. package/dist/ingest/__tests__/vlocity-dispatch-integration.test.d.ts +2 -0
  196. package/dist/ingest/__tests__/vlocity-dispatch-integration.test.d.ts.map +1 -0
  197. package/dist/ingest/__tests__/vlocity-dispatch-integration.test.js +134 -0
  198. package/dist/ingest/__tests__/vlocity-dispatch-integration.test.js.map +1 -0
  199. package/dist/ingest/detect-deletions-guard.d.ts +45 -0
  200. package/dist/ingest/detect-deletions-guard.d.ts.map +1 -0
  201. package/dist/ingest/detect-deletions-guard.js +63 -0
  202. package/dist/ingest/detect-deletions-guard.js.map +1 -0
  203. package/dist/ingest/live-ingest.d.ts +86 -0
  204. package/dist/ingest/live-ingest.d.ts.map +1 -1
  205. package/dist/ingest/live-ingest.js +783 -449
  206. package/dist/ingest/live-ingest.js.map +1 -1
  207. package/dist/parsers/__tests__/datatransform-structured.test.d.ts +2 -0
  208. package/dist/parsers/__tests__/datatransform-structured.test.d.ts.map +1 -0
  209. package/dist/parsers/__tests__/datatransform-structured.test.js +147 -0
  210. package/dist/parsers/__tests__/datatransform-structured.test.js.map +1 -0
  211. package/dist/parsers/__tests__/edge-provenance.test.d.ts +2 -0
  212. package/dist/parsers/__tests__/edge-provenance.test.d.ts.map +1 -0
  213. package/dist/parsers/__tests__/edge-provenance.test.js +47 -0
  214. package/dist/parsers/__tests__/edge-provenance.test.js.map +1 -0
  215. package/dist/parsers/__tests__/lwc-bindings.test.js +78 -3
  216. package/dist/parsers/__tests__/lwc-bindings.test.js.map +1 -1
  217. package/dist/parsers/__tests__/mcd-gap-fills.test.d.ts +2 -0
  218. package/dist/parsers/__tests__/mcd-gap-fills.test.d.ts.map +1 -0
  219. package/dist/parsers/__tests__/mcd-gap-fills.test.js +75 -0
  220. package/dist/parsers/__tests__/mcd-gap-fills.test.js.map +1 -0
  221. package/dist/parsers/__tests__/object-inline-fields.test.js +2 -2
  222. package/dist/parsers/__tests__/object-inline-fields.test.js.map +1 -1
  223. package/dist/parsers/__tests__/omnistudio-overlap.test.d.ts +2 -0
  224. package/dist/parsers/__tests__/omnistudio-overlap.test.d.ts.map +1 -0
  225. package/dist/parsers/__tests__/omnistudio-overlap.test.js +163 -0
  226. package/dist/parsers/__tests__/omnistudio-overlap.test.js.map +1 -0
  227. package/dist/parsers/__tests__/omnistudio.golden.test.js +5 -6
  228. package/dist/parsers/__tests__/omnistudio.golden.test.js.map +1 -1
  229. package/dist/parsers/__tests__/reflection-walker.test.d.ts +2 -0
  230. package/dist/parsers/__tests__/reflection-walker.test.d.ts.map +1 -0
  231. package/dist/parsers/__tests__/reflection-walker.test.js +162 -0
  232. package/dist/parsers/__tests__/reflection-walker.test.js.map +1 -0
  233. package/dist/parsers/__tests__/vlocity.golden.test.js +9 -13
  234. package/dist/parsers/__tests__/vlocity.golden.test.js.map +1 -1
  235. package/dist/parsers/apex/__tests__/ast-extractor.test.js +64 -4
  236. package/dist/parsers/apex/__tests__/ast-extractor.test.js.map +1 -1
  237. package/dist/parsers/apex/__tests__/empty-body.test.d.ts +2 -0
  238. package/dist/parsers/apex/__tests__/empty-body.test.d.ts.map +1 -0
  239. package/dist/parsers/apex/__tests__/empty-body.test.js +40 -0
  240. package/dist/parsers/apex/__tests__/empty-body.test.js.map +1 -0
  241. package/dist/parsers/apex/__tests__/is-test-attribute.test.d.ts +2 -0
  242. package/dist/parsers/apex/__tests__/is-test-attribute.test.d.ts.map +1 -0
  243. package/dist/parsers/apex/__tests__/is-test-attribute.test.js +56 -0
  244. package/dist/parsers/apex/__tests__/is-test-attribute.test.js.map +1 -0
  245. package/dist/parsers/apex/__tests__/regex-arg-count.test.d.ts +2 -0
  246. package/dist/parsers/apex/__tests__/regex-arg-count.test.d.ts.map +1 -0
  247. package/dist/parsers/apex/__tests__/regex-arg-count.test.js +82 -0
  248. package/dist/parsers/apex/__tests__/regex-arg-count.test.js.map +1 -0
  249. package/dist/parsers/apex/__tests__/regex-inloop.test.d.ts +2 -0
  250. package/dist/parsers/apex/__tests__/regex-inloop.test.d.ts.map +1 -0
  251. package/dist/parsers/apex/__tests__/regex-inloop.test.js +70 -0
  252. package/dist/parsers/apex/__tests__/regex-inloop.test.js.map +1 -0
  253. package/dist/parsers/apex/arity-resolver.d.ts.map +1 -1
  254. package/dist/parsers/apex/arity-resolver.js.map +1 -1
  255. package/dist/parsers/apex/ast-extractor.d.ts.map +1 -1
  256. package/dist/parsers/apex/ast-extractor.js +33 -1
  257. package/dist/parsers/apex/ast-extractor.js.map +1 -1
  258. package/dist/parsers/apex/class.d.ts.map +1 -1
  259. package/dist/parsers/apex/class.js +162 -5
  260. package/dist/parsers/apex/class.js.map +1 -1
  261. package/dist/parsers/apex/trigger.d.ts.map +1 -1
  262. package/dist/parsers/apex/trigger.js +10 -0
  263. package/dist/parsers/apex/trigger.js.map +1 -1
  264. package/dist/parsers/common.d.ts +4 -1
  265. package/dist/parsers/common.d.ts.map +1 -1
  266. package/dist/parsers/common.js +15 -2
  267. package/dist/parsers/common.js.map +1 -1
  268. package/dist/parsers/flow/__tests__/invocable-resolver.test.js.map +1 -1
  269. package/dist/parsers/flow/invocable-resolver.d.ts.map +1 -1
  270. package/dist/parsers/flow/invocable-resolver.js.map +1 -1
  271. package/dist/parsers/generic/reflection-walker.d.ts +90 -0
  272. package/dist/parsers/generic/reflection-walker.d.ts.map +1 -0
  273. package/dist/parsers/generic/reflection-walker.js +193 -0
  274. package/dist/parsers/generic/reflection-walker.js.map +1 -0
  275. package/dist/parsers/lwc/html-visitor.d.ts.map +1 -1
  276. package/dist/parsers/lwc/html-visitor.js +82 -3
  277. package/dist/parsers/lwc/html-visitor.js.map +1 -1
  278. package/dist/parsers/lwc/js-visitor.d.ts.map +1 -1
  279. package/dist/parsers/lwc/js-visitor.js +6 -2
  280. package/dist/parsers/lwc/js-visitor.js.map +1 -1
  281. package/dist/parsers/object/object.d.ts.map +1 -1
  282. package/dist/parsers/object/object.js +35 -5
  283. package/dist/parsers/object/object.js.map +1 -1
  284. package/dist/parsers/omnistudio/data-transform.d.ts.map +1 -1
  285. package/dist/parsers/omnistudio/data-transform.js +39 -6
  286. package/dist/parsers/omnistudio/data-transform.js.map +1 -1
  287. package/dist/parsers/omnistudio/integration-procedure.d.ts.map +1 -1
  288. package/dist/parsers/omnistudio/integration-procedure.js +3 -2
  289. package/dist/parsers/omnistudio/integration-procedure.js.map +1 -1
  290. package/dist/parsers/omnistudio/overlap-detector.d.ts +50 -0
  291. package/dist/parsers/omnistudio/overlap-detector.d.ts.map +1 -0
  292. package/dist/parsers/omnistudio/overlap-detector.js +122 -0
  293. package/dist/parsers/omnistudio/overlap-detector.js.map +1 -0
  294. package/dist/parsers/omnistudio/process.d.ts.map +1 -1
  295. package/dist/parsers/omnistudio/process.js +3 -2
  296. package/dist/parsers/omnistudio/process.js.map +1 -1
  297. package/dist/parsers/omnistudio/ui-card.d.ts.map +1 -1
  298. package/dist/parsers/omnistudio/ui-card.js +3 -2
  299. package/dist/parsers/omnistudio/ui-card.js.map +1 -1
  300. package/dist/parsers/rules/custom-metadata-type.yml +7 -0
  301. package/dist/parsers/rules/custom-setting.yml +24 -0
  302. package/dist/parsers/vlocity/common.d.ts +16 -0
  303. package/dist/parsers/vlocity/common.d.ts.map +1 -1
  304. package/dist/parsers/vlocity/common.js +62 -0
  305. package/dist/parsers/vlocity/common.js.map +1 -1
  306. package/dist/parsers/vlocity/data-raptor.d.ts.map +1 -1
  307. package/dist/parsers/vlocity/data-raptor.js +43 -14
  308. package/dist/parsers/vlocity/data-raptor.js.map +1 -1
  309. package/dist/render/__tests__/sarif.test.d.ts +2 -0
  310. package/dist/render/__tests__/sarif.test.d.ts.map +1 -0
  311. package/dist/render/__tests__/sarif.test.js +126 -0
  312. package/dist/render/__tests__/sarif.test.js.map +1 -0
  313. package/dist/render/sarif.d.ts +94 -0
  314. package/dist/render/sarif.d.ts.map +1 -0
  315. package/dist/render/sarif.js +131 -0
  316. package/dist/render/sarif.js.map +1 -0
  317. package/dist/storage/__tests__/analysis-tables-migration.test.js +1 -1
  318. package/dist/storage/__tests__/graph-store.test.js +13 -0
  319. package/dist/storage/__tests__/graph-store.test.js.map +1 -1
  320. package/dist/storage/__tests__/migrations.test.js +4 -4
  321. package/dist/storage/__tests__/rename-stability.test.d.ts +2 -0
  322. package/dist/storage/__tests__/rename-stability.test.d.ts.map +1 -0
  323. package/dist/storage/__tests__/rename-stability.test.js +135 -0
  324. package/dist/storage/__tests__/rename-stability.test.js.map +1 -0
  325. package/dist/storage/__tests__/sync-generation.test.d.ts +2 -0
  326. package/dist/storage/__tests__/sync-generation.test.d.ts.map +1 -0
  327. package/dist/storage/__tests__/sync-generation.test.js +159 -0
  328. package/dist/storage/__tests__/sync-generation.test.js.map +1 -0
  329. package/dist/storage/index.d.ts +1 -0
  330. package/dist/storage/index.d.ts.map +1 -1
  331. package/dist/storage/index.js +1 -0
  332. package/dist/storage/index.js.map +1 -1
  333. package/dist/storage/interfaces.d.ts +60 -0
  334. package/dist/storage/interfaces.d.ts.map +1 -1
  335. package/dist/storage/sqlite/graph-store.d.ts +21 -1
  336. package/dist/storage/sqlite/graph-store.d.ts.map +1 -1
  337. package/dist/storage/sqlite/graph-store.js +86 -0
  338. package/dist/storage/sqlite/graph-store.js.map +1 -1
  339. package/dist/storage/sqlite/migrations.d.ts.map +1 -1
  340. package/dist/storage/sqlite/migrations.js +38 -0
  341. package/dist/storage/sqlite/migrations.js.map +1 -1
  342. package/dist/storage/sqlite/rename-stability.d.ts +94 -0
  343. package/dist/storage/sqlite/rename-stability.d.ts.map +1 -0
  344. package/dist/storage/sqlite/rename-stability.js +92 -0
  345. package/dist/storage/sqlite/rename-stability.js.map +1 -0
  346. package/package.json +4 -1
@@ -11,10 +11,11 @@ import { EmbeddingQueue } from "../embedding/index.js";
11
11
  import { resolveOrg } from "../extractors/live-org/auth.js";
12
12
  import { bulkRetrieve } from "../extractors/live-org/bulk-retrieve.js";
13
13
  import { probeCapabilities } from "../extractors/live-org/capabilities.js";
14
- import { iterChanges } from "../extractors/live-org/source-member.js";
15
- import { startLivenessProbe, } from "../extractors/live-org/liveness.js";
14
+ import { startLivenessProbe } from "../extractors/live-org/liveness.js";
16
15
  import { dataPool, metadataPool, toolingPool } from "../extractors/live-org/rate-limit.js";
16
+ import { iterChanges } from "../extractors/live-org/source-member.js";
17
17
  import { loadAllRules } from "../parsers/rules/_loader.js";
18
+ import { evaluateDeletionGuard, resolveMaxDropRatio } from "./detect-deletions-guard.js";
18
19
  const NOOP_LOG = {
19
20
  debug: () => { },
20
21
  info: () => { },
@@ -112,7 +113,83 @@ function unwrapApexEnvelope(content) {
112
113
  }
113
114
  return { body: content };
114
115
  }
115
- function adaptParserInput(ref, content) {
116
+ /** Maps the Vlocity runner's emitted memberType (the bare VlocityDataPackType)
117
+ * to the parser-registry key the four rich Vlocity parsers register under.
118
+ * Only these four DataPack types have dedicated parsers; the other ~44 fall
119
+ * through to the opaque node parser (node-only, by design). */
120
+ const VLOCITY_MEMBER_TO_PARSER = {
121
+ DataRaptor: "VlocityDataRaptor",
122
+ VlocityDataRaptor: "VlocityDataRaptor",
123
+ IntegrationProcedure: "VlocityIntegrationProcedure",
124
+ VlocityIntegrationProcedure: "VlocityIntegrationProcedure",
125
+ OmniScript: "VlocityOmniScript",
126
+ VlocityOmniScript: "VlocityOmniScript",
127
+ VlocityCard: "VlocityCard",
128
+ };
129
+ /**
130
+ * Build the text embedded for a node's semantic vector. Beyond the
131
+ * label/qname/description baseline, this folds in CONFIG VALUES so
132
+ * `find_similar` matches on what a config artifact actually resolves to —
133
+ * a Custom Metadata record's field values, a Custom Label's text, a Custom
134
+ * Setting's rows. Without this, searching "endpoint for product push" can't
135
+ * reach `End_Point_Urls.ProductPush_PLDT` because only its name was embedded.
136
+ * Exported for tests.
137
+ */
138
+ export function buildEmbedText(label, qname, attributes) {
139
+ const a = attributes ?? {};
140
+ const parts = [`${label}: ${qname}`];
141
+ if (typeof a.description === "string" && a.description)
142
+ parts.push(a.description);
143
+ // Custom Metadata record field values: [{field,value}] | {field:value} | object.
144
+ const values = a.values;
145
+ if (Array.isArray(values)) {
146
+ for (const v of values) {
147
+ const f = v.field;
148
+ const val = v.value;
149
+ if (f != null && val != null)
150
+ parts.push(`${String(f)} = ${stringifyConfigVal(val)}`);
151
+ }
152
+ }
153
+ else if (values && typeof values === "object") {
154
+ for (const [k, v] of Object.entries(values)) {
155
+ if (v != null && v !== "")
156
+ parts.push(`${k} = ${stringifyConfigVal(v)}`);
157
+ }
158
+ }
159
+ // Custom Label text value.
160
+ if (typeof a.value === "string" && a.value)
161
+ parts.push(a.value);
162
+ // Custom Setting rows: array of row objects (field=value per row).
163
+ const rows = a.customSettingRows;
164
+ if (Array.isArray(rows)) {
165
+ for (const row of rows) {
166
+ if (row && typeof row === "object") {
167
+ const kv = Object.entries(row)
168
+ .filter(([, v]) => v != null && v !== "")
169
+ .map(([k, v]) => `${k}=${stringifyConfigVal(v)}`)
170
+ .join(", ");
171
+ if (kv)
172
+ parts.push(kv);
173
+ }
174
+ }
175
+ }
176
+ return parts.join("\n");
177
+ }
178
+ function stringifyConfigVal(v) {
179
+ if (v == null)
180
+ return "";
181
+ if (typeof v === "object") {
182
+ try {
183
+ return JSON.stringify(v);
184
+ }
185
+ catch {
186
+ return String(v);
187
+ }
188
+ }
189
+ return String(v);
190
+ }
191
+ /** Exported for tests — maps a RawMember to the registered parser's input shape. */
192
+ export function adaptParserInput(ref, content) {
116
193
  switch (ref.memberType) {
117
194
  case "ApexClass": {
118
195
  const { body, metaXml } = unwrapApexEnvelope(content);
@@ -167,10 +244,24 @@ function adaptParserInput(ref, content) {
167
244
  }
168
245
  return { type: ref.memberType, input: { name: ref.memberName, metadata } };
169
246
  }
247
+ // The Vlocity runner emits the bare VlocityDataPackType as memberType
248
+ // ("DataRaptor", "IntegrationProcedure", "OmniScript", "VlocityCard"),
249
+ // but the four rich parsers register under "Vlocity"-prefixed type names
250
+ // ("VlocityDataRaptor", …). Without this mapping the runner's members
251
+ // never matched a case here and fell through to the opaque fallback —
252
+ // shallow nodes with zero DR_/IP_/OS_ edges. Only "VlocityCard" happened
253
+ // to align on both sides, which is why a real ingest produced VC edges
254
+ // but no DataRaptor/IP/OmniScript lineage. The golden tests call the
255
+ // parsers directly, so this runner→dispatch seam was never covered.
256
+ // The legacy "Vlocity*"-prefixed keys are kept too in case any caller
257
+ // (or a future filesystem source) emits them.
258
+ case "DataRaptor":
170
259
  case "VlocityDataRaptor":
260
+ case "IntegrationProcedure":
171
261
  case "VlocityIntegrationProcedure":
172
- case "VlocityCard":
173
- case "VlocityOmniScript": {
262
+ case "OmniScript":
263
+ case "VlocityOmniScript":
264
+ case "VlocityCard": {
174
265
  let datapack = content;
175
266
  try {
176
267
  datapack = JSON.parse(content);
@@ -178,11 +269,52 @@ function adaptParserInput(ref, content) {
178
269
  catch {
179
270
  /* keep raw */
180
271
  }
181
- return { type: ref.memberType, input: { name: ref.memberName, datapack } };
272
+ return {
273
+ type: VLOCITY_MEMBER_TO_PARSER[ref.memberType] ?? ref.memberType,
274
+ input: { name: ref.memberName, datapack },
275
+ };
182
276
  }
183
- default:
184
- return null;
277
+ default: {
278
+ // Long-tail metadata types: route to a registered parser when one
279
+ // exists (the 21 rule-based YAML parsers register under their raw
280
+ // metadata type name, e.g. "Layout", "Dashboard"), otherwise fall
281
+ // back to the opaque node parser so the member still becomes a
282
+ // graph node. Before this fallback every generic-extractor member
283
+ // was silently dropped here — extracted, logged as ✓, and never
284
+ // merged — which left grant/reference edges dangling (e.g. 59k
285
+ // GRANTS_TAB_ACCESS edges pointing at CustomTab nodes that were
286
+ // never created).
287
+ if (parserRegistry.for(ref.memberType)) {
288
+ return { type: ref.memberType, input: genericRuleInput(ref.memberName, content) };
289
+ }
290
+ return {
291
+ type: "OpaqueMetadata",
292
+ input: { metadataType: ref.memberType, name: ref.memberName, raw: content },
293
+ };
294
+ }
295
+ }
296
+ }
297
+ /**
298
+ * Build the `{ name, xml, ...siblings }` input shape the rule-based parsers'
299
+ * `xml-string` coercion expects. Filesystem sources hand us raw XML; the
300
+ * live generic extractor hands us the JSON-serialized `metadata.read()`
301
+ * object — whose keys mirror the parsed XML root, so they can be promoted
302
+ * to sibling keys directly (coerceInput merges siblings into the record).
303
+ */
304
+ function genericRuleInput(name, content) {
305
+ const s = (content ?? "").trimStart();
306
+ if (s.startsWith("<"))
307
+ return { name, xml: content };
308
+ let parsed = {};
309
+ try {
310
+ const j = JSON.parse(s);
311
+ if (j && typeof j === "object" && !Array.isArray(j))
312
+ parsed = j;
313
+ }
314
+ catch {
315
+ /* not JSON — leave record empty; the node still gets name + label */
185
316
  }
317
+ return { xml: "", ...parsed, name };
186
318
  }
187
319
  export async function liveIngest(opts) {
188
320
  const logger = opts.logger ?? NOOP_LOG;
@@ -206,510 +338,712 @@ export async function liveIngest(opts) {
206
338
  apiVersion,
207
339
  createdAt: now,
208
340
  });
209
- const caps = await probeCapabilities(resolved.conn);
210
- logger.info("live-ingest: probed capabilities", { caps });
211
- // Discovery is also invoked implicitly inside bulkRetrieve, but logging the
212
- // type-count up-front gives operators a quick coverage signal.
341
+ // W1.5-08: flag the org row as having an in-flight ingest before any
342
+ // extractor work. Done after upsertOrg so the row definitely exists.
343
+ // The matching markSyncComplete / markSyncFailed calls below are wired
344
+ // into a try/catch around the whole ingest body so concurrent MCP
345
+ // readers always see in_progress flip back to 0 even when ingest throws.
346
+ // Failure to record sync-start is non-fatal — log and continue rather
347
+ // than aborting an ingest because the bookkeeping column is unhappy.
348
+ const ingestStartedIso = new Date(now).toISOString();
213
349
  try {
214
- const { discoverMetadataTypes } = await import("../extractors/live-org/discovery.js");
215
- const discovered = await discoverMetadataTypes(resolved.conn, apiVersion);
216
- logger.info("live-ingest: discovered metadata types", { count: discovered.length });
350
+ graph.markSyncStarted(resolved.orgId, ingestStartedIso);
217
351
  }
218
352
  catch (e) {
219
- logger.warn("live-ingest: metadata.describe failed", { err: e.message });
220
- }
221
- const existing = graph.getOrg(resolved.orgId);
222
- const requestedMode = opts.mode ?? "auto";
223
- let mode;
224
- if (requestedMode === "auto") {
225
- mode = existing?.lastSyncedAt && caps.sourceTracking ? "incremental" : "full";
226
- }
227
- else {
228
- mode = requestedMode;
353
+ logger.warn("live-ingest: markSyncStarted failed", { err: e.message });
229
354
  }
230
- if (!opts.skipSnapshot && opts.snapshotStore) {
355
+ // W1.5-08: the rest of the ingest runs inside this try so a thrown
356
+ // exception still flips `sync_in_progress` back to 0 via markSyncFailed.
357
+ // Per-resolver try/catch isolation inside bulkRetrieve / live-ingest is
358
+ // preserved — this wrapper only catches exceptions that escape *all* of
359
+ // those inner handlers (i.e. genuinely fatal failures).
360
+ try {
361
+ const caps = await probeCapabilities(resolved.conn);
362
+ logger.info("live-ingest: probed capabilities", { caps });
363
+ // Discovery is also invoked implicitly inside bulkRetrieve, but logging the
364
+ // type-count up-front gives operators a quick coverage signal.
231
365
  try {
232
- opts.snapshotStore.createSnapshot(resolved.orgId, `pre-sync-${new Date(now).toISOString()}`, true);
366
+ const { discoverMetadataTypes } = await import("../extractors/live-org/discovery.js");
367
+ const discovered = await discoverMetadataTypes(resolved.conn, apiVersion);
368
+ logger.info("live-ingest: discovered metadata types", { count: discovered.length });
233
369
  }
234
370
  catch (e) {
235
- logger.warn("live-ingest: snapshot failed", { err: e.message });
236
- }
237
- }
238
- let membersProcessed = 0;
239
- let parseErrors = 0;
240
- let deletions = 0;
241
- // Set in the full-sync branch if the bulkRetrieve stream itself throws.
242
- // Detect-deletions reads it post-fan-out to bail out instead of mass-
243
- // wiping nodes that were never visited due to the abort.
244
- let streamAborted = false;
245
- const touchedQnames = new Set();
246
- const parseCtxBase = {
247
- orgId: resolved.orgId,
248
- parseTimestamp: new Date(now).toISOString(),
249
- namespace: null,
250
- logger,
251
- };
252
- const embedQueue = opts.vectorStore
253
- ? new EmbeddingQueue({
254
- vectorStore: opts.vectorStore,
255
- onError: (err) => logger.warn("live-ingest: embedding batch failed", { err: err.message }),
256
- })
257
- : null;
258
- const handleParsed = (parsed) => {
259
- if (parsed.nodes.length) {
260
- graph.mergeNodes(parsed.nodes);
261
- for (const n of parsed.nodes)
262
- touchedQnames.add(String(n.qualifiedName));
263
- }
264
- if (parsed.edges.length)
265
- graph.mergeEdges(parsed.edges);
266
- if (parsed.snippets?.length) {
267
- graph.transaction(() => {
268
- for (const s of parsed.snippets ?? []) {
269
- graph.upsertSnippet(s);
270
- }
271
- });
371
+ logger.warn("live-ingest: metadata.describe failed", { err: e.message });
272
372
  }
273
- if (embedQueue) {
274
- for (const n of parsed.nodes) {
275
- const desc = n.attributes?.description;
276
- const text = `${n.label}: ${n.qualifiedName}\n${typeof desc === "string" ? desc : ""}`;
277
- embedQueue.push({
278
- qname: String(n.qualifiedName),
279
- text,
280
- orgId: String(n.orgId),
281
- label: n.label,
282
- });
283
- }
373
+ const existing = graph.getOrg(resolved.orgId);
374
+ const requestedMode = opts.mode ?? "auto";
375
+ let mode;
376
+ if (requestedMode === "auto") {
377
+ mode = existing?.lastSyncedAt && caps.sourceTracking ? "incremental" : "full";
284
378
  }
285
- };
286
- const debugProcess = process.env.SFGRAPH_DEBUG_INGEST === "1";
287
- const processOne = async (ref, content) => {
288
- const qnameForLog = `${ref.memberType}:${ref.memberName}`;
289
- if (ref.obsolete) {
290
- // Build the qualified name same way parsers would: best-effort by member name.
291
- const qname = asQualifiedName(`${ref.memberType}:${ref.memberName}`);
292
- if (debugProcess)
293
- console.log(`ingest: [trace] delete ← ${qnameForLog}`);
294
- graph.deleteEdgesFor(resolved.orgId, qname);
295
- graph.deleteNode(resolved.orgId, qname);
296
- if (debugProcess)
297
- console.log(`ingest: [trace] delete ✓ ${qnameForLog}`);
298
- deletions += 1;
299
- return;
300
- }
301
- const adapted = adaptParserInput(ref, content);
302
- if (!adapted)
303
- return;
304
- const parser = parserRegistry.for(adapted.type);
305
- if (!parser)
306
- return;
307
- try {
308
- const ctx = {
309
- ...parseCtxBase,
310
- sourceUri: ref.sourceUri,
311
- namespace: ref.namespace ?? null,
312
- };
313
- if (debugProcess)
314
- console.log(`ingest: [trace] parse ← ${qnameForLog}`);
315
- const result = await parser.parse(adapted.input, ctx);
316
- if (debugProcess)
317
- console.log(`ingest: [trace] parse ✓ ${qnameForLog} (nodes=${result.nodes.length} edges=${result.edges.length})`);
318
- if (debugProcess)
319
- console.log(`ingest: [trace] graph-merge ← ${qnameForLog}`);
320
- handleParsed(result);
321
- if (debugProcess)
322
- console.log(`ingest: [trace] graph-merge ✓ ${qnameForLog}`);
323
- membersProcessed += 1;
379
+ else {
380
+ mode = requestedMode;
324
381
  }
325
- catch (e) {
326
- parseErrors += 1;
327
- logger.warn("live-ingest: parser failure", {
328
- type: ref.memberType,
329
- name: ref.memberName,
330
- err: e.message,
331
- });
332
- if (debugProcess) {
333
- console.error(`ingest: [trace] FAILURE ${qnameForLog}: ${e.message}\n${e.stack}`);
382
+ if (!opts.skipSnapshot && opts.snapshotStore) {
383
+ try {
384
+ opts.snapshotStore.createSnapshot(resolved.orgId, `pre-sync-${new Date(now).toISOString()}`, true);
385
+ }
386
+ catch (e) {
387
+ logger.warn("live-ingest: snapshot failed", { err: e.message });
334
388
  }
335
389
  }
336
- };
337
- if (mode === "incremental") {
338
- const sinceIso = existing?.lastSyncedAt
339
- ? new Date(existing.lastSyncedAt).toISOString()
340
- : new Date(now - 24 * 3600 * 1000).toISOString();
341
- for await (const ref of iterChanges(resolved.conn, resolved.orgId, sinceIso)) {
342
- // For incremental, content is best fetched on-demand; for deletions we skip content.
343
- await processOne(ref, "");
344
- }
345
- }
346
- else {
347
- // The outer iteration is also wrapped: bulkRetrieve uses fail-soft per
348
- // source, but a top-level catch protects against any iterator-protocol
349
- // surprises (e.g. generator throws during `next()` before yielding).
350
- // Read live pool caps so the log reflects --tooling-pool/--metadata-pool/
351
- // --data-pool overrides or SFGRAPH_*_POOL env vars rather than hardcoded
352
- // defaults.
353
- const { toolingPool, metadataPool, dataPool } = await import("../extractors/live-org/rate-limit.js");
354
- const tCap = toolingPool._store
355
- .storeOptions.maxConcurrent;
356
- const mCap = metadataPool._store
357
- .storeOptions.maxConcurrent;
358
- const dCap = dataPool._store
359
- .storeOptions.maxConcurrent;
360
- console.log(`ingest: starting full sync (Tooling pool ${tCap} / Metadata pool ${mCap} / Data pool ${dCap} concurrent)`);
361
- const fanOutStart = Date.now();
362
- let progressCount = 0;
363
- let lastTickAt = Date.now();
364
- let lastActivityAt = Date.now();
365
- let lastMemberLabel = "(none)";
366
- const PROGRESS_TICK_MS = 5000; // emit a heartbeat at least every 5s
390
+ let membersProcessed = 0;
391
+ let parseErrors = 0;
392
+ let deletions = 0;
393
+ // Set in the full-sync branch if the bulkRetrieve stream itself throws.
394
+ // Detect-deletions reads it post-fan-out to bail out instead of mass-
395
+ // wiping nodes that were never visited due to the abort.
396
+ let streamAborted = false;
397
+ const touchedQnames = new Set();
398
+ // W1.5-07: maintain per-label touched-qnames sets in parallel with the
399
+ // flat `touchedQnames` set so the deletion-sweep guard can compute
400
+ // priorCount/touchedCount per label without re-deriving labels later.
401
+ const touchedQnamesByLabel = new Map();
402
+ // Skip/warning report collected across all branches. Populated by the
403
+ // full-sync fan-out via bulkRetrieve's onError callback; incremental mode
404
+ // currently emits no entries (single-stream iterChanges has no per-source
405
+ // failure surface). Returned on LiveIngestResult.warnings so MCP consumers
406
+ // can read what was skipped without parsing stdout.
367
407
  const skipReport = { skips: [] };
368
- if (opts.onlyLabels && opts.onlyLabels.size > 0) {
369
- console.log(`ingest: --only filter active (${opts.onlyLabels.size} source${opts.onlyLabels.size === 1 ? "" : "s"}): ${[...opts.onlyLabels].join(", ")}`);
370
- }
371
- // Debug mode: SFGRAPH_DEBUG_INGEST=1 enables a heartbeat timer that
372
- // prints heap usage + seconds-since-last-activity every 10s. Critical
373
- // for diagnosing silent ingest deaths (OOM, native segfault, hung SF
374
- // call) where the normal progress log just stops without an error.
375
- const debug = process.env.SFGRAPH_DEBUG_INGEST === "1";
376
- let heartbeatTimer = null;
377
- let keepAlive = null;
378
- let livenessProbe = null;
379
- // Track signal handlers we register so we can remove them in finally.
380
- // Without this, every invocation of liveIngest in debug mode leaked one
381
- // listener per signal (SIGTERM/SIGINT/SIGUSR2), eventually triggering
382
- // MaxListenersExceededWarning and N-fold handler firing on first ^C in
383
- // multi-org / programmatic flows. (Audit finding C2.)
384
- const installedSignalHandlers = [];
385
- // streamAborted (hoisted at function scope) gates the post-fan-out
386
- // detect-deletions block. If the bulkRetrieve stream itself throws
387
- // (vs individual sources fail-softing), we've only seen a fraction of
388
- // the org and the `touchedQnames` set is not safe to use for stale-
389
- // qname computation — without this guard a transient SF error would
390
- // mass-delete the untouched 70% of the graph. (Audit finding C1.)
391
- try {
392
- // Keep-alive sentinel: a ref'd timer that exists purely to prevent the
393
- // event loop from draining while we're awaiting Bottleneck-scheduled
394
- // work. Bottleneck's local-datastore reservoir-refresh timer is unref'd
395
- // internally, jsforce's HTTP keep-alive sockets idle between requests,
396
- // and Node will happily exit(0) on a perfectly healthy pending Promise
397
- // if no ref'd handle is left.
398
- keepAlive = setInterval(() => { }, 60_000);
399
- // Background liveness probe. Polls conn.identity() every 30s with a
400
- // 10s deadline; after two consecutive failures it logs a prominent
401
- // "CONNECTION LOST" warning so the user knows why surviving
402
- // extractors are about to time out one by one. We intentionally do
403
- // NOT abort the ingest from here — per-call 60s timeouts (added in
404
- // 1.1.3) already cap exposure, and aborting mid-flight would need
405
- // an AbortSignal threaded through every extractor for marginal
406
- // benefit. The probe's job is observability, not control flow.
407
- // Disable via SFGRAPH_NO_LIVENESS_PROBE=1 for hermetic test runs
408
- // where the mock connection has no identity() implementation.
409
- if (process.env.SFGRAPH_NO_LIVENESS_PROBE !== "1") {
410
- livenessProbe = startLivenessProbe(resolved.conn);
411
- }
412
- // Wall-clock heartbeat — always on. Without this, silent phases (where
413
- // all in-window sources are awaiting Bottleneck-queued metadata reads
414
- // without yielding) look indistinguishable from a hung process. The
415
- // per-record progress tick at the bottom of the fan-out loop only
416
- // fires when a record arrives; this fires on a real timer regardless.
417
- // Debug mode adds heap/rss; baseline shows processed/lastSource/idle.
418
- const showPoolCounters = debug || process.env.SFGRAPH_DEBUG_POOLS === "1";
419
- heartbeatTimer = setInterval(() => {
420
- const idleSec = Math.round((Date.now() - lastActivityAt) / 1000);
421
- if (idleSec < 10)
422
- return; // suppress chatter when records are flowing
423
- if (debug) {
424
- const mem = process.memoryUsage();
425
- const heapMb = Math.round(mem.heapUsed / 1024 / 1024);
426
- const rssMb = Math.round(mem.rss / 1024 / 1024);
427
- const externalMb = Math.round(mem.external / 1024 / 1024);
428
- console.log(`ingest: [heartbeat] processed=${progressCount} lastSource=${lastMemberLabel} idle=${idleSec}s heap=${heapMb}MB rss=${rssMb}MB ext=${externalMb}MB`);
429
- }
430
- else {
431
- console.log(`ingest: …still working — processed=${progressCount} idle=${idleSec}s lastSource=${lastMemberLabel}`);
408
+ // Phase 1.5 W1.5-02: namespaced wedge warnings populated by bulkRetrieve's
409
+ // soft-isolate path (`wedge:<source>:<stage>:<detail>`). Concatenated with
410
+ // the legacy `${label}: ${reason}` strings from skipReport.skips into the
411
+ // final LiveIngestResult.warnings array. See bulk-retrieve.ts WedgeCoordinator.
412
+ const wedgeWarnings = [];
413
+ const parseCtxBase = {
414
+ orgId: resolved.orgId,
415
+ parseTimestamp: new Date(now).toISOString(),
416
+ namespace: null,
417
+ logger,
418
+ };
419
+ const embedQueue = opts.vectorStore
420
+ ? new EmbeddingQueue({
421
+ vectorStore: opts.vectorStore,
422
+ onError: (err) => logger.warn("live-ingest: embedding batch failed", { err: err.message }),
423
+ })
424
+ : null;
425
+ const handleParsed = (parsed) => {
426
+ if (parsed.nodes.length) {
427
+ graph.mergeNodes(parsed.nodes);
428
+ for (const n of parsed.nodes) {
429
+ const qn = String(n.qualifiedName);
430
+ touchedQnames.add(qn);
431
+ // W1.5-07: also bucket by label for the per-label deletion guard.
432
+ let bucket = touchedQnamesByLabel.get(n.label);
433
+ if (!bucket) {
434
+ bucket = new Set();
435
+ touchedQnamesByLabel.set(n.label, bucket);
436
+ }
437
+ bucket.add(qn);
432
438
  }
433
- // Pool diagnostics: snapshot Bottleneck state so a wedge can be
434
- // attributed concretely to reservoir starvation vs. stuck-in-flight
435
- // jobs vs. pre-yield iterator hangs. Counts are synchronous; the
436
- // reservoir read is async but fire-and-forget — by the time it
437
- // resolves we just log it.
438
- if (showPoolCounters) {
439
- const snapshot = [
440
- ["tool", toolingPool],
441
- ["meta", metadataPool],
442
- ["data", dataPool],
443
- ];
444
- for (const [name, p] of snapshot) {
445
- const c = p.counts();
446
- p.currentReservoir().then((reservoir) => {
447
- console.log(`ingest: [pool ${name}] running=${c.RUNNING} executing=${c.EXECUTING} queued=${c.QUEUED} received=${c.RECEIVED} reservoir=${reservoir}`);
448
- }, () => {
449
- /* swallow — never let a diag failure kill ingest */
450
- });
439
+ }
440
+ if (parsed.edges.length)
441
+ graph.mergeEdges(parsed.edges);
442
+ if (parsed.snippets?.length) {
443
+ graph.transaction(() => {
444
+ for (const s of parsed.snippets ?? []) {
445
+ graph.upsertSnippet(s);
451
446
  }
447
+ });
448
+ }
449
+ if (embedQueue) {
450
+ for (const n of parsed.nodes) {
451
+ embedQueue.push({
452
+ qname: String(n.qualifiedName),
453
+ text: buildEmbedText(n.label, String(n.qualifiedName), n.attributes),
454
+ orgId: String(n.orgId),
455
+ label: n.label,
456
+ });
452
457
  }
453
- }, 10_000);
454
- if (debug) {
455
- console.log("ingest: [debug] SFGRAPH_DEBUG_INGEST=1 active — heartbeat every 10s");
456
- // Signal handlers — surface what was running when the user (or OS)
457
- // sends a kill so silent terminations have at least one breadcrumb.
458
- const onSig = (sig) => () => {
459
- console.error(`ingest: [debug] received ${sig} after ${Math.round((Date.now() - fanOutStart) / 1000)}s — processed=${progressCount} lastSource=${lastMemberLabel}`);
460
- console.error(new Error(`signal:${sig}`).stack);
461
- process.exitCode = 130;
462
- };
463
- const onUsr2 = () => {
464
- const mem = process.memoryUsage();
465
- console.error(`ingest: [debug] SIGUSR2 — heap=${Math.round(mem.heapUsed / 1024 / 1024)}MB rss=${Math.round(mem.rss / 1024 / 1024)}MB lastSource=${lastMemberLabel}`);
458
+ }
459
+ };
460
+ const debugProcess = process.env.SFGRAPH_DEBUG_INGEST === "1";
461
+ const processOne = async (ref, content) => {
462
+ const qnameForLog = `${ref.memberType}:${ref.memberName}`;
463
+ if (ref.obsolete) {
464
+ // Build the qualified name same way parsers would: best-effort by member name.
465
+ const qname = asQualifiedName(`${ref.memberType}:${ref.memberName}`);
466
+ if (debugProcess)
467
+ console.log(`ingest: [trace] delete ← ${qnameForLog}`);
468
+ graph.deleteEdgesFor(resolved.orgId, qname);
469
+ graph.deleteNode(resolved.orgId, qname);
470
+ if (debugProcess)
471
+ console.log(`ingest: [trace] delete ✓ ${qnameForLog}`);
472
+ deletions += 1;
473
+ return;
474
+ }
475
+ const adapted = adaptParserInput(ref, content);
476
+ if (!adapted)
477
+ return;
478
+ const parser = parserRegistry.for(adapted.type);
479
+ if (!parser)
480
+ return;
481
+ try {
482
+ const ctx = {
483
+ ...parseCtxBase,
484
+ sourceUri: ref.sourceUri,
485
+ namespace: ref.namespace ?? null,
466
486
  };
467
- installedSignalHandlers.push({ sig: "SIGTERM", fn: onSig("SIGTERM") });
468
- installedSignalHandlers.push({ sig: "SIGINT", fn: onSig("SIGINT") });
469
- installedSignalHandlers.push({ sig: "SIGUSR2", fn: onUsr2 });
470
- for (const h of installedSignalHandlers)
471
- process.on(h.sig, h.fn);
487
+ if (debugProcess)
488
+ console.log(`ingest: [trace] parse ← ${qnameForLog}`);
489
+ const result = await parser.parse(adapted.input, ctx);
490
+ if (debugProcess)
491
+ console.log(`ingest: [trace] parse ✓ ${qnameForLog} (nodes=${result.nodes.length} edges=${result.edges.length})`);
492
+ if (debugProcess)
493
+ console.log(`ingest: [trace] graph-merge ← ${qnameForLog}`);
494
+ handleParsed(result);
495
+ if (debugProcess)
496
+ console.log(`ingest: [trace] graph-merge ✓ ${qnameForLog}`);
497
+ membersProcessed += 1;
472
498
  }
499
+ catch (e) {
500
+ parseErrors += 1;
501
+ logger.warn("live-ingest: parser failure", {
502
+ type: ref.memberType,
503
+ name: ref.memberName,
504
+ err: e.message,
505
+ });
506
+ if (debugProcess) {
507
+ console.error(`ingest: [trace] FAILURE ${qnameForLog}: ${e.message}\n${e.stack}`);
508
+ }
509
+ }
510
+ };
511
+ if (mode === "incremental") {
512
+ const sinceIso = existing?.lastSyncedAt
513
+ ? new Date(existing.lastSyncedAt).toISOString()
514
+ : new Date(now - 24 * 3600 * 1000).toISOString();
515
+ for await (const ref of iterChanges(resolved.conn, resolved.orgId, sinceIso)) {
516
+ // For incremental, content is best fetched on-demand; for deletions we skip content.
517
+ await processOne(ref, "");
518
+ }
519
+ }
520
+ else {
521
+ // The outer iteration is also wrapped: bulkRetrieve uses fail-soft per
522
+ // source, but a top-level catch protects against any iterator-protocol
523
+ // surprises (e.g. generator throws during `next()` before yielding).
524
+ // Read live pool caps so the log reflects --tooling-pool/--metadata-pool/
525
+ // --data-pool overrides or SFGRAPH_*_POOL env vars rather than hardcoded
526
+ // defaults.
527
+ const { toolingPool, metadataPool, dataPool } = await import("../extractors/live-org/rate-limit.js");
528
+ const tCap = toolingPool._store.storeOptions.maxConcurrent;
529
+ const mCap = metadataPool._store.storeOptions.maxConcurrent;
530
+ const dCap = dataPool
531
+ ._store.storeOptions.maxConcurrent;
532
+ console.log(`ingest: starting full sync (Tooling pool ${tCap} / Metadata pool ${mCap} / Data pool ${dCap} concurrent)`);
533
+ // MCD baseline runs BEFORE the parser fan-out so any (src, dst, REFERENCES)
534
+ // edges it writes can be overwritten if a real parser produces the same
535
+ // shape later. The MCD path uses generic REFERENCES while parsers use
536
+ // specific rel-types — so in practice MCD and parsed coexist in
537
+ // different edge tables rather than competing for the same row, but
538
+ // the pre-fan-out ordering preserves the "parsed wins" guarantee for
539
+ // the (rare) case where a parser does emit REFERENCES.
540
+ if (!opts.disableMcdBaseline) {
541
+ try {
542
+ const { runMcdBaseline } = await import("../extractors/live-org/extractors/mcd-baseline.js");
543
+ const mcdCtx = {
544
+ ...parseCtxBase,
545
+ sourceUri: "mcd-baseline://tooling-soql",
546
+ };
547
+ const mcd = await runMcdBaseline(resolved.conn, {
548
+ orgId: resolved.orgId,
549
+ ctx: mcdCtx,
550
+ onError: (label, err) => {
551
+ skipReport.skips.push({
552
+ label,
553
+ reason: err.message,
554
+ category: "unknown",
555
+ });
556
+ },
557
+ });
558
+ if (mcd.edges.length > 0) {
559
+ graph.mergeEdges(mcd.edges);
560
+ }
561
+ logger.info("live-ingest: MCD baseline complete", {
562
+ edges: mcd.edges.length,
563
+ byType: mcd.byType,
564
+ });
565
+ }
566
+ catch (e) {
567
+ logger.warn("live-ingest: MCD baseline failed", { err: e.message });
568
+ }
569
+ }
570
+ const fanOutStart = Date.now();
571
+ let progressCount = 0;
572
+ let lastTickAt = Date.now();
573
+ let lastActivityAt = Date.now();
574
+ let lastMemberLabel = "(none)";
575
+ const PROGRESS_TICK_MS = 5000; // emit a heartbeat at least every 5s
576
+ if (opts.onlyLabels && opts.onlyLabels.size > 0) {
577
+ console.log(`ingest: --only filter active (${opts.onlyLabels.size} source${opts.onlyLabels.size === 1 ? "" : "s"}): ${[...opts.onlyLabels].join(", ")}`);
578
+ }
579
+ // Debug mode: SFGRAPH_DEBUG_INGEST=1 enables a heartbeat timer that
580
+ // prints heap usage + seconds-since-last-activity every 10s. Critical
581
+ // for diagnosing silent ingest deaths (OOM, native segfault, hung SF
582
+ // call) where the normal progress log just stops without an error.
583
+ const debug = process.env.SFGRAPH_DEBUG_INGEST === "1";
584
+ let heartbeatTimer = null;
585
+ let keepAlive = null;
586
+ let livenessProbe = null;
587
+ // Track signal handlers we register so we can remove them in finally.
588
+ // Without this, every invocation of liveIngest in debug mode leaked one
589
+ // listener per signal (SIGTERM/SIGINT/SIGUSR2), eventually triggering
590
+ // MaxListenersExceededWarning and N-fold handler firing on first ^C in
591
+ // multi-org / programmatic flows. (Audit finding C2.)
592
+ const installedSignalHandlers = [];
593
+ // streamAborted (hoisted at function scope) gates the post-fan-out
594
+ // detect-deletions block. If the bulkRetrieve stream itself throws
595
+ // (vs individual sources fail-softing), we've only seen a fraction of
596
+ // the org and the `touchedQnames` set is not safe to use for stale-
597
+ // qname computation — without this guard a transient SF error would
598
+ // mass-delete the untouched 70% of the graph. (Audit finding C1.)
473
599
  try {
474
- for await (const member of bulkRetrieve(resolved.conn, caps, resolved.orgId, {
475
- skipReport,
476
- ...(opts.onlyLabels ? { onlyLabels: opts.onlyLabels } : {}),
477
- })) {
478
- try {
479
- await processOne(member.ref, member.content);
600
+ // Keep-alive sentinel: a ref'd timer that exists purely to prevent the
601
+ // event loop from draining while we're awaiting Bottleneck-scheduled
602
+ // work. Bottleneck's local-datastore reservoir-refresh timer is unref'd
603
+ // internally, jsforce's HTTP keep-alive sockets idle between requests,
604
+ // and Node will happily exit(0) on a perfectly healthy pending Promise
605
+ // if no ref'd handle is left.
606
+ keepAlive = setInterval(() => { }, 60_000);
607
+ // Background liveness probe. Polls conn.identity() every 30s with a
608
+ // 10s deadline; after two consecutive failures it logs a prominent
609
+ // "CONNECTION LOST" warning so the user knows why surviving
610
+ // extractors are about to time out one by one. We intentionally do
611
+ // NOT abort the ingest from here — per-call 60s timeouts (added in
612
+ // 1.1.3) already cap exposure, and aborting mid-flight would need
613
+ // an AbortSignal threaded through every extractor for marginal
614
+ // benefit. The probe's job is observability, not control flow.
615
+ // Disable via SFGRAPH_NO_LIVENESS_PROBE=1 for hermetic test runs
616
+ // where the mock connection has no identity() implementation.
617
+ if (process.env.SFGRAPH_NO_LIVENESS_PROBE !== "1") {
618
+ livenessProbe = startLivenessProbe(resolved.conn);
619
+ }
620
+ // Wall-clock heartbeat — always on. Without this, silent phases (where
621
+ // all in-window sources are awaiting Bottleneck-queued metadata reads
622
+ // without yielding) look indistinguishable from a hung process. The
623
+ // per-record progress tick at the bottom of the fan-out loop only
624
+ // fires when a record arrives; this fires on a real timer regardless.
625
+ // Debug mode adds heap/rss; baseline shows processed/lastSource/idle.
626
+ const showPoolCounters = debug || process.env.SFGRAPH_DEBUG_POOLS === "1";
627
+ heartbeatTimer = setInterval(() => {
628
+ const idleSec = Math.round((Date.now() - lastActivityAt) / 1000);
629
+ if (idleSec < 10)
630
+ return; // suppress chatter when records are flowing
631
+ if (debug) {
632
+ const mem = process.memoryUsage();
633
+ const heapMb = Math.round(mem.heapUsed / 1024 / 1024);
634
+ const rssMb = Math.round(mem.rss / 1024 / 1024);
635
+ const externalMb = Math.round(mem.external / 1024 / 1024);
636
+ console.log(`ingest: [heartbeat] processed=${progressCount} lastSource=${lastMemberLabel} idle=${idleSec}s heap=${heapMb}MB rss=${rssMb}MB ext=${externalMb}MB`);
480
637
  }
481
- catch (e) {
482
- parseErrors += 1;
483
- logger.warn("live-ingest: processOne failed", {
484
- qname: `${member.ref.memberType}:${member.ref.memberName}`,
485
- error: e.message,
486
- });
638
+ else {
639
+ console.log(`ingest: …still working — processed=${progressCount} idle=${idleSec}s lastSource=${lastMemberLabel}`);
487
640
  }
488
- progressCount += 1;
489
- lastActivityAt = Date.now();
490
- lastMemberLabel = `${member.ref.memberType}:${member.ref.memberName}`;
491
- if (progressCount % 200 === 0 || Date.now() - lastTickAt > PROGRESS_TICK_MS) {
492
- const elapsedSec = Math.round((Date.now() - fanOutStart) / 1000);
493
- const memSuffix = debug
494
- ? ` heap=${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`
495
- : "";
496
- console.log(`ingest: …${progressCount} members processed so far (${elapsedSec}s elapsed)${memSuffix}`);
497
- lastTickAt = Date.now();
641
+ // Pool diagnostics: snapshot Bottleneck state so a wedge can be
642
+ // attributed concretely to reservoir starvation vs. stuck-in-flight
643
+ // jobs vs. pre-yield iterator hangs. Counts are synchronous; the
644
+ // reservoir read is async but fire-and-forget — by the time it
645
+ // resolves we just log it.
646
+ if (showPoolCounters) {
647
+ const snapshot = [
648
+ ["tool", toolingPool],
649
+ ["meta", metadataPool],
650
+ ["data", dataPool],
651
+ ];
652
+ for (const [name, p] of snapshot) {
653
+ const c = p.counts();
654
+ p.currentReservoir().then((reservoir) => {
655
+ console.log(`ingest: [pool ${name}] running=${c.RUNNING} executing=${c.EXECUTING} queued=${c.QUEUED} received=${c.RECEIVED} reservoir=${reservoir}`);
656
+ }, () => {
657
+ /* swallow — never let a diag failure kill ingest */
658
+ });
659
+ }
498
660
  }
499
- if (progressCount % 500 === 0) {
500
- const gs = graph;
501
- if (typeof gs.checkpoint === "function")
502
- gs.checkpoint();
661
+ }, 10_000);
662
+ if (debug) {
663
+ console.log("ingest: [debug] SFGRAPH_DEBUG_INGEST=1 active — heartbeat every 10s");
664
+ // Signal handlers — surface what was running when the user (or OS)
665
+ // sends a kill so silent terminations have at least one breadcrumb.
666
+ const onSig = (sig) => () => {
667
+ console.error(`ingest: [debug] received ${sig} after ${Math.round((Date.now() - fanOutStart) / 1000)}s — processed=${progressCount} lastSource=${lastMemberLabel}`);
668
+ console.error(new Error(`signal:${sig}`).stack);
669
+ process.exitCode = 130;
670
+ };
671
+ const onUsr2 = () => {
672
+ const mem = process.memoryUsage();
673
+ console.error(`ingest: [debug] SIGUSR2 — heap=${Math.round(mem.heapUsed / 1024 / 1024)}MB rss=${Math.round(mem.rss / 1024 / 1024)}MB lastSource=${lastMemberLabel}`);
674
+ };
675
+ installedSignalHandlers.push({ sig: "SIGTERM", fn: onSig("SIGTERM") });
676
+ installedSignalHandlers.push({ sig: "SIGINT", fn: onSig("SIGINT") });
677
+ installedSignalHandlers.push({ sig: "SIGUSR2", fn: onUsr2 });
678
+ for (const h of installedSignalHandlers)
679
+ process.on(h.sig, h.fn);
680
+ }
681
+ try {
682
+ for await (const member of bulkRetrieve(resolved.conn, caps, resolved.orgId, {
683
+ skipReport,
684
+ warnings: wedgeWarnings,
685
+ ...(opts.onlyLabels ? { onlyLabels: opts.onlyLabels } : {}),
686
+ // The bulk-retrieve flag name follows the same convention as
687
+ // the bulk-retrieve's other opts; semantically it's "do the
688
+ // retrieve()" which is now on by default. Caller-side disable
689
+ // routes through this same boolean inverted.
690
+ ...(opts.disableOmnistudioRetrieve ? {} : { enableOmnistudioRetrieve: true }),
691
+ // jsforce attaches the version it negotiated on conn.version; fall
692
+ // back to a sensible recent release if absent (mocks, etc.)
693
+ apiVersion: resolved.conn.version ?? "60.0",
694
+ })) {
695
+ try {
696
+ await processOne(member.ref, member.content);
697
+ }
698
+ catch (e) {
699
+ parseErrors += 1;
700
+ logger.warn("live-ingest: processOne failed", {
701
+ qname: `${member.ref.memberType}:${member.ref.memberName}`,
702
+ error: e.message,
703
+ });
704
+ }
705
+ progressCount += 1;
706
+ lastActivityAt = Date.now();
707
+ lastMemberLabel = `${member.ref.memberType}:${member.ref.memberName}`;
708
+ if (progressCount % 200 === 0 || Date.now() - lastTickAt > PROGRESS_TICK_MS) {
709
+ const elapsedSec = Math.round((Date.now() - fanOutStart) / 1000);
710
+ const memSuffix = debug
711
+ ? ` heap=${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`
712
+ : "";
713
+ console.log(`ingest: …${progressCount} members processed so far (${elapsedSec}s elapsed)${memSuffix}`);
714
+ lastTickAt = Date.now();
715
+ }
716
+ if (progressCount % 500 === 0) {
717
+ const gs = graph;
718
+ if (typeof gs.checkpoint === "function")
719
+ gs.checkpoint();
720
+ }
503
721
  }
722
+ console.log(`ingest: fan-out complete (${progressCount} members in ${Math.round((Date.now() - fanOutStart) / 1000)}s)`);
723
+ const gs = graph;
724
+ if (typeof gs.checkpoint === "function")
725
+ gs.checkpoint();
726
+ }
727
+ catch (e) {
728
+ // The stream itself threw (not an individual source — those are
729
+ // captured by failSoft). Mark so detect-deletions stays its hand.
730
+ streamAborted = true;
731
+ logger.warn("live-ingest: bulkRetrieve stream aborted", {
732
+ error: e.message,
733
+ });
504
734
  }
505
- console.log(`ingest: fan-out complete (${progressCount} members in ${Math.round((Date.now() - fanOutStart) / 1000)}s)`);
506
- const gs = graph;
507
- if (typeof gs.checkpoint === "function")
508
- gs.checkpoint();
509
735
  }
510
- catch (e) {
511
- // The stream itself threw (not an individual source — those are
512
- // captured by failSoft). Mark so detect-deletions stays its hand.
513
- streamAborted = true;
514
- logger.warn("live-ingest: bulkRetrieve stream aborted", {
515
- error: e.message,
516
- });
736
+ finally {
737
+ if (heartbeatTimer)
738
+ clearInterval(heartbeatTimer);
739
+ if (keepAlive)
740
+ clearInterval(keepAlive);
741
+ livenessProbe?.stop();
742
+ for (const h of installedSignalHandlers)
743
+ process.off(h.sig, h.fn);
744
+ }
745
+ // End-of-run skip summary. Grouped by category so the remediation is
746
+ // targeted to the specific class of failure.
747
+ if (skipReport.skips.length > 0) {
748
+ printSkipSummary(skipReport, resolved.alias);
749
+ }
750
+ // Persist the skip report so --retry-skipped can read it next run.
751
+ // Always written (even when empty) so users can tell whether the last
752
+ // run had skips or just hasn't recorded any yet.
753
+ if (opts.skipReportPath) {
754
+ try {
755
+ const { writeFileSync, mkdirSync } = await import("node:fs");
756
+ const { dirname } = await import("node:path");
757
+ mkdirSync(dirname(opts.skipReportPath), { recursive: true });
758
+ writeFileSync(opts.skipReportPath, JSON.stringify({ recordedAt: Date.now(), orgId: resolved.orgId, ...skipReport }, null, 2), "utf8");
759
+ }
760
+ catch (e) {
761
+ logger.warn("live-ingest: skip report write failed", { err: e.message });
762
+ }
517
763
  }
518
764
  }
519
- finally {
520
- if (heartbeatTimer)
521
- clearInterval(heartbeatTimer);
522
- if (keepAlive)
523
- clearInterval(keepAlive);
524
- livenessProbe?.stop();
525
- for (const h of installedSignalHandlers)
526
- process.off(h.sig, h.fn);
527
- }
528
- // End-of-run skip summary. Grouped by category so the remediation is
529
- // targeted to the specific class of failure.
530
- if (skipReport.skips.length > 0) {
531
- printSkipSummary(skipReport, resolved.alias);
532
- }
533
- // Persist the skip report so --retry-skipped can read it next run.
534
- // Always written (even when empty) so users can tell whether the last
535
- // run had skips or just hasn't recorded any yet.
536
- if (opts.skipReportPath) {
765
+ if (embedQueue) {
537
766
  try {
538
- const { writeFileSync, mkdirSync } = await import("node:fs");
539
- const { dirname } = await import("node:path");
540
- mkdirSync(dirname(opts.skipReportPath), { recursive: true });
541
- writeFileSync(opts.skipReportPath, JSON.stringify({ recordedAt: Date.now(), orgId: resolved.orgId, ...skipReport }, null, 2), "utf8");
767
+ await embedQueue.drain();
542
768
  }
543
769
  catch (e) {
544
- logger.warn("live-ingest: skip report write failed", { err: e.message });
770
+ logger.warn("live-ingest: embed drain failed", { err: e.message });
545
771
  }
546
772
  }
547
- }
548
- if (embedQueue) {
549
- try {
550
- await embedQueue.drain();
551
- }
552
- catch (e) {
553
- logger.warn("live-ingest: embed drain failed", { err: e.message });
554
- }
555
- }
556
- // Full-sync deletion detection: compute previously-known minus touched and
557
- // delete the difference. Skipped on incremental (SourceMember handles it),
558
- // skipped if any parse error occurred this run (avoid mass-wipe on
559
- // transient SF errors).
560
- if (opts.detectDeletions && mode === "full") {
561
- if (streamAborted) {
562
- logger.warn("live-ingest: detect-deletions skipped because bulkRetrieve stream aborted — touchedQnames is incomplete and a full deletion sweep would wrongly wipe untouched-but-still-present nodes");
563
- }
564
- else if (parseErrors > 0) {
565
- logger.warn("live-ingest: detect-deletions skipped due to parseErrors", { parseErrors });
773
+ // Full-sync deletion detection: compute previously-known minus touched and
774
+ // delete the difference. Skipped on incremental (SourceMember handles it),
775
+ // skipped if any parse error occurred this run (avoid mass-wipe on
776
+ // transient SF errors).
777
+ if (opts.detectDeletions && mode === "full") {
778
+ if (streamAborted) {
779
+ logger.warn("live-ingest: detect-deletions skipped because bulkRetrieve stream aborted — touchedQnames is incomplete and a full deletion sweep would wrongly wipe untouched-but-still-present nodes");
780
+ }
781
+ else if (parseErrors > 0) {
782
+ logger.warn("live-ingest: detect-deletions skipped due to parseErrors", { parseErrors });
783
+ }
784
+ else {
785
+ try {
786
+ // W1.5-07: per-label drop-ratio guard. The sweep used to run as a
787
+ // single set-difference across ALL labels; a wedged source with
788
+ // streamAborted=false + touchedCount=0 for its label would silently
789
+ // wipe every node of that label (graph-extinction risk that
790
+ // compounds with the W1.5-01/02 wedge cascade). Now we evaluate the
791
+ // guard per label and refuse when the implied drop is suspicious.
792
+ const maxDropRatio = resolveMaxDropRatio(process.env.SFGRAPH_DETECT_DELETIONS_MAX_DROP_RATIO);
793
+ const labels = graph.listAllLabels();
794
+ for (const label of labels) {
795
+ const priorCount = graph.countNodesByLabel(resolved.orgId, label);
796
+ const touchedCount = touchedQnamesByLabel.get(label)?.size ?? 0;
797
+ const verdict = evaluateDeletionGuard(label, priorCount, touchedCount, maxDropRatio);
798
+ if (!verdict.proceed) {
799
+ if (verdict.warning)
800
+ wedgeWarnings.push(verdict.warning);
801
+ logger.warn("live-ingest: detect-deletions refused", {
802
+ label,
803
+ priorCount,
804
+ touchedCount,
805
+ maxDropRatio,
806
+ warning: verdict.warning,
807
+ });
808
+ continue;
809
+ }
810
+ // Proceed with deletion for this label only. Scoped to this label's
811
+ // table via listNodesByLabel rather than the global listAllQnames
812
+ // walk, so a refusal for one label cannot accidentally affect
813
+ // sweeps for other labels.
814
+ const touchedSet = touchedQnamesByLabel.get(label);
815
+ const persisted = graph.listNodesByLabel(resolved.orgId, label);
816
+ let labelDeletions = 0;
817
+ for (const node of persisted) {
818
+ const q = String(node.qualifiedName);
819
+ if (touchedSet?.has(q))
820
+ continue;
821
+ graph.deleteEdgesFor(resolved.orgId, asQualifiedName(q));
822
+ graph.deleteNode(resolved.orgId, asQualifiedName(q));
823
+ deletions += 1;
824
+ labelDeletions += 1;
825
+ }
826
+ if (labelDeletions > 0) {
827
+ logger.info("live-ingest: detect-deletions removed stale qnames", {
828
+ label,
829
+ count: labelDeletions,
830
+ });
831
+ }
832
+ }
833
+ }
834
+ catch (e) {
835
+ logger.warn("live-ingest: detect-deletions failed", { err: e.message });
836
+ }
837
+ }
566
838
  }
567
- else {
839
+ // -----------------------------------------------------------------------
840
+ // Post-merge resolver passes. Each step is isolated in try/catch so a
841
+ // single resolver bug never breaks ingest — it just reports zero work done.
842
+ // -----------------------------------------------------------------------
843
+ let crossFlavorEdges = 0;
844
+ let arityResolved = 0;
845
+ let flowMethodsResolved = 0;
846
+ let danglingEdges = 0;
847
+ let overlapResult = { matched: 0, diverged: 0, empty: 0, annotated: 0 };
848
+ let reflectionEdges = 0;
849
+ // Synth a ParseContext for post-passes that need to mint edges/nodes.
850
+ const postCtx = {
851
+ ...parseCtxBase,
852
+ sourceUri: "post-merge://resolver",
853
+ };
854
+ if (!opts.disableCrossFlavor) {
568
855
  try {
569
- const persisted = graph.listAllQnames(resolved.orgId);
570
- const stale = [];
571
- for (const q of persisted) {
572
- if (!touchedQnames.has(String(q)))
573
- stale.push(String(q));
856
+ crossFlavorEdges = resolveCrossFlavor(graph, {
857
+ orgId: resolved.orgId,
858
+ namespace: null,
859
+ ctx: postCtx,
860
+ });
861
+ if (crossFlavorEdges > 0) {
862
+ logger.info("live-ingest: cross-flavor resolver linked Vlocity↔OmniStudio", {
863
+ edges: crossFlavorEdges,
864
+ });
574
865
  }
575
- for (const q of stale) {
576
- graph.deleteEdgesFor(resolved.orgId, asQualifiedName(q));
577
- graph.deleteNode(resolved.orgId, asQualifiedName(q));
578
- deletions += 1;
866
+ }
867
+ catch (e) {
868
+ logger.warn("live-ingest: cross-flavor resolver failed", { err: e.message });
869
+ }
870
+ }
871
+ // Overlap detector: opt-in (off by default) — annotates the CANONICAL_OF
872
+ // edges resolveCrossFlavor just emitted with signaturesMatch + divergence
873
+ // detail. Must run *after* cross-flavor and *before* the audit so the
874
+ // audit sees the annotated edges as the same edges (idempotent merge).
875
+ if (!opts.disableOverlapDetect) {
876
+ try {
877
+ const { detectOmnistudioOverlap } = await import("../parsers/omnistudio/overlap-detector.js");
878
+ overlapResult = detectOmnistudioOverlap(graph, {
879
+ orgId: resolved.orgId,
880
+ ctx: postCtx,
881
+ });
882
+ if (overlapResult.annotated > 0) {
883
+ logger.info("live-ingest: omnistudio overlap detector annotated CANONICAL_OF pairs", {
884
+ matched: overlapResult.matched,
885
+ diverged: overlapResult.diverged,
886
+ empty: overlapResult.empty,
887
+ annotated: overlapResult.annotated,
888
+ });
579
889
  }
580
- if (stale.length > 0) {
581
- logger.info("live-ingest: detect-deletions removed stale qnames", {
582
- count: stale.length,
890
+ }
891
+ catch (e) {
892
+ logger.warn("live-ingest: omnistudio overlap detector failed", {
893
+ err: e.message,
894
+ });
895
+ }
896
+ }
897
+ if (!opts.disableFlowInvocableResolve) {
898
+ try {
899
+ const flowResult = resolveFlowApexMethods(graph, resolved.orgId, postCtx);
900
+ flowMethodsResolved = flowResult.resolved;
901
+ if (flowResult.scanned > 0) {
902
+ logger.info("live-ingest: flow→apex invocable resolver", {
903
+ scanned: flowResult.scanned,
904
+ resolved: flowResult.resolved,
905
+ missing: flowResult.missing,
906
+ ambiguous: flowResult.ambiguous,
583
907
  });
584
908
  }
585
909
  }
586
910
  catch (e) {
587
- logger.warn("live-ingest: detect-deletions failed", { err: e.message });
911
+ logger.warn("live-ingest: flow→apex resolver failed", { err: e.message });
588
912
  }
589
913
  }
590
- }
591
- // -----------------------------------------------------------------------
592
- // Post-merge resolver passes. Each step is isolated in try/catch so a
593
- // single resolver bug never breaks ingest — it just reports zero work done.
594
- // -----------------------------------------------------------------------
595
- let crossFlavorEdges = 0;
596
- let arityResolved = 0;
597
- let flowMethodsResolved = 0;
598
- let danglingEdges = 0;
599
- // Synth a ParseContext for post-passes that need to mint edges/nodes.
600
- const postCtx = {
601
- ...parseCtxBase,
602
- sourceUri: "post-merge://resolver",
603
- };
604
- if (!opts.disableCrossFlavor) {
605
- try {
606
- crossFlavorEdges = resolveCrossFlavor(graph, {
607
- orgId: resolved.orgId,
608
- namespace: null,
609
- ctx: postCtx,
610
- });
611
- if (crossFlavorEdges > 0) {
612
- logger.info("live-ingest: cross-flavor resolver linked Vlocity↔OmniStudio", {
613
- edges: crossFlavorEdges,
914
+ if (!opts.disableArityResolve) {
915
+ try {
916
+ const arityResult = resolveApexMethodArity(graph, {
917
+ orgId: resolved.orgId,
918
+ ctx: postCtx,
614
919
  });
920
+ arityResolved = arityResult.resolved;
921
+ if (arityResult.scanned > 0) {
922
+ logger.info("live-ingest: apex method arity resolver", {
923
+ scanned: arityResult.scanned,
924
+ resolved: arityResult.resolved,
925
+ ambiguous: arityResult.ambiguous,
926
+ unresolved: arityResult.unresolved,
927
+ edgesEmitted: arityResult.edgesEmitted,
928
+ });
929
+ }
930
+ }
931
+ catch (e) {
932
+ logger.warn("live-ingest: apex arity resolver failed", { err: e.message });
615
933
  }
616
934
  }
617
- catch (e) {
618
- logger.warn("live-ingest: cross-flavor resolver failed", { err: e.message });
619
- }
620
- }
621
- if (!opts.disableFlowInvocableResolve) {
622
- try {
623
- const flowResult = resolveFlowApexMethods(graph, resolved.orgId, postCtx);
624
- flowMethodsResolved = flowResult.resolved;
625
- if (flowResult.scanned > 0) {
626
- logger.info("live-ingest: flow→apex invocable resolver", {
627
- scanned: flowResult.scanned,
628
- resolved: flowResult.resolved,
629
- missing: flowResult.missing,
630
- ambiguous: flowResult.ambiguous,
935
+ // Reflection walker: runs BEFORE the dangling-edge audit so any
936
+ // REFERENCES edges it emits are validated by the audit alongside
937
+ // parser-emitted edges. Default on; disable via disableReflectionWalker.
938
+ if (!opts.disableReflectionWalker) {
939
+ try {
940
+ const { walkBlobsForReferences } = await import("../parsers/generic/reflection-walker.js");
941
+ const reflectionResult = walkBlobsForReferences(graph, {
942
+ orgId: resolved.orgId,
943
+ ctx: postCtx,
944
+ });
945
+ reflectionEdges = reflectionResult.edgesEmitted;
946
+ if (reflectionEdges > 0) {
947
+ logger.info("live-ingest: reflection walker emitted REFERENCES edges", {
948
+ scanned: reflectionResult.scanned,
949
+ edges: reflectionEdges,
950
+ truncated: reflectionResult.truncatedSources,
951
+ ambiguous: reflectionResult.ambiguousMatches,
952
+ });
953
+ }
954
+ }
955
+ catch (e) {
956
+ logger.warn("live-ingest: reflection walker failed", {
957
+ err: e.message,
631
958
  });
632
959
  }
633
960
  }
634
- catch (e) {
635
- logger.warn("live-ingest: flow→apex resolver failed", { err: e.message });
961
+ if (!opts.disableAudit) {
962
+ try {
963
+ const auditResult = auditDanglingEdges(graph, resolved.orgId, { sampleSize: 25 });
964
+ danglingEdges = auditResult.danglingCount;
965
+ if (auditResult.danglingCount > 0) {
966
+ logger.info("live-ingest: graph audit found dangling edges", {
967
+ totalEdges: auditResult.totalEdges,
968
+ danglingCount: auditResult.danglingCount,
969
+ platformRefs: auditResult.platformRefCount,
970
+ unexpected: auditResult.unexpectedCount,
971
+ byRel: auditResult.byRel,
972
+ byDstPrefix: auditResult.byDstPrefix,
973
+ });
974
+ }
975
+ }
976
+ catch (e) {
977
+ logger.warn("live-ingest: graph audit failed", { err: e.message });
978
+ }
636
979
  }
637
- }
638
- if (!opts.disableArityResolve) {
980
+ const completedIso = new Date().toISOString();
639
981
  try {
640
- const arityResult = resolveApexMethodArity(graph, {
641
- orgId: resolved.orgId,
642
- ctx: postCtx,
643
- });
644
- arityResolved = arityResult.resolved;
645
- if (arityResult.scanned > 0) {
646
- logger.info("live-ingest: apex method arity resolver", {
647
- scanned: arityResult.scanned,
648
- resolved: arityResult.resolved,
649
- ambiguous: arityResult.ambiguous,
650
- unresolved: arityResult.unresolved,
651
- edgesEmitted: arityResult.edgesEmitted,
652
- });
653
- }
982
+ // W1.5-08: markSyncComplete subsumes the old touchSync — it updates
983
+ // last_synced_at AND increments sync_generation AND clears the
984
+ // in-progress flag, atomically in one transaction.
985
+ graph.markSyncComplete(resolved.orgId, completedIso);
654
986
  }
655
987
  catch (e) {
656
- logger.warn("live-ingest: apex arity resolver failed", { err: e.message });
988
+ logger.warn("live-ingest: markSyncComplete failed", { err: e.message });
657
989
  }
658
- }
659
- if (!opts.disableAudit) {
660
- try {
661
- const auditResult = auditDanglingEdges(graph, resolved.orgId, { sampleSize: 25 });
662
- danglingEdges = auditResult.danglingCount;
663
- if (auditResult.danglingCount > 0) {
664
- logger.info("live-ingest: graph audit found dangling edges", {
665
- totalEdges: auditResult.totalEdges,
666
- danglingCount: auditResult.danglingCount,
667
- byRel: auditResult.byRel,
668
- byDstPrefix: auditResult.byDstPrefix,
990
+ if (opts.analysisDb) {
991
+ try {
992
+ await populateAnalysisTables(graph, resolved.orgId, opts.analysisDb);
993
+ }
994
+ catch (e) {
995
+ logger.warn("live-ingest: populate analysis tables failed", {
996
+ err: e.message,
669
997
  });
670
998
  }
671
999
  }
672
- catch (e) {
673
- logger.warn("live-ingest: graph audit failed", { err: e.message });
1000
+ if (opts.snapshotStore) {
1001
+ try {
1002
+ opts.snapshotStore.prune(resolved.orgId, opts.snapshotRetentionDays ?? 30);
1003
+ }
1004
+ catch (e) {
1005
+ logger.warn("live-ingest: snapshot prune failed", { err: e.message });
1006
+ }
674
1007
  }
675
- }
676
- const completedIso = new Date().toISOString();
677
- try {
678
- graph.touchSync(resolved.orgId, completedIso);
1008
+ return {
1009
+ orgId: resolved.orgId,
1010
+ capabilities: caps,
1011
+ mode,
1012
+ membersProcessed,
1013
+ parseErrors,
1014
+ deletions,
1015
+ durationMs: Date.now() - startedAt,
1016
+ crossFlavorEdges,
1017
+ arityResolved,
1018
+ flowMethodsResolved,
1019
+ danglingEdges,
1020
+ reflectionEdges,
1021
+ overlap: overlapResult,
1022
+ warnings: [
1023
+ ...skipReport.skips.map((s) => `${s.label}: ${s.reason}`),
1024
+ // Phase 1.5 W1.5-02 namespaced wedge strings. Format documented in
1025
+ // PLAN.md: `wedge:<source>:<stage>:<detail>`. Forward-compatible
1026
+ // with Phase 1's structured-warnings refactor (W1-01).
1027
+ ...wedgeWarnings,
1028
+ ],
1029
+ };
679
1030
  }
680
1031
  catch (e) {
681
- logger.warn("live-ingest: touchSync failed", { err: e.message });
682
- }
683
- if (opts.analysisDb) {
1032
+ // W1.5-08: ingest threw before reaching markSyncComplete. Flip
1033
+ // sync_in_progress back to 0 (and clear sync_started_at) so concurrent
1034
+ // MCP readers don't see an orphaned in_progress flag forever. Generation
1035
+ // is intentionally NOT incremented — failed ingests don't count as
1036
+ // fresh graph data. Re-throw so callers (CLI / MCP job runner) see the
1037
+ // original error unchanged.
684
1038
  try {
685
- await populateAnalysisTables(graph, resolved.orgId, opts.analysisDb);
1039
+ graph.markSyncFailed(resolved.orgId);
686
1040
  }
687
- catch (e) {
688
- logger.warn("live-ingest: populate analysis tables failed", {
689
- err: e.message,
1041
+ catch (markErr) {
1042
+ logger.warn("live-ingest: markSyncFailed failed", {
1043
+ err: markErr.message,
690
1044
  });
691
1045
  }
1046
+ throw e;
692
1047
  }
693
- if (opts.snapshotStore) {
694
- try {
695
- opts.snapshotStore.prune(resolved.orgId, opts.snapshotRetentionDays ?? 30);
696
- }
697
- catch (e) {
698
- logger.warn("live-ingest: snapshot prune failed", { err: e.message });
699
- }
700
- }
701
- return {
702
- orgId: resolved.orgId,
703
- capabilities: caps,
704
- mode,
705
- membersProcessed,
706
- parseErrors,
707
- deletions,
708
- durationMs: Date.now() - startedAt,
709
- crossFlavorEdges,
710
- arityResolved,
711
- flowMethodsResolved,
712
- danglingEdges,
713
- };
714
1048
  }
715
1049
  //# sourceMappingURL=live-ingest.js.map