@salesforce/afv-skills 1.14.0 → 1.16.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 (365) hide show
  1. package/package.json +1 -1
  2. package/skills/activating-datacloud/SKILL.md +0 -1
  3. package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
  4. package/skills/applying-slds/SKILL.md +322 -0
  5. package/skills/applying-slds/checklists.md +83 -0
  6. package/skills/applying-slds/examples.md +283 -0
  7. package/skills/applying-slds/guidance/README.md +83 -0
  8. package/skills/applying-slds/guidance/blueprints-index.md +213 -0
  9. package/skills/applying-slds/guidance/icons-guidance.md +186 -0
  10. package/skills/applying-slds/guidance/overviews/borders.md +236 -0
  11. package/skills/applying-slds/guidance/overviews/color.md +266 -0
  12. package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
  13. package/skills/applying-slds/guidance/overviews/icons.md +240 -0
  14. package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
  15. package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
  16. package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
  17. package/skills/applying-slds/guidance/overviews/typography.md +323 -0
  18. package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
  19. package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
  20. package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
  21. package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
  22. package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
  23. package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
  24. package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
  25. package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
  26. package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
  27. package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
  28. package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
  29. package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
  30. package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
  31. package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
  32. package/skills/applying-slds/guidance/utilities/borders.md +131 -0
  33. package/skills/applying-slds/guidance/utilities/box.md +125 -0
  34. package/skills/applying-slds/guidance/utilities/color.md +165 -0
  35. package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
  36. package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
  37. package/skills/applying-slds/guidance/utilities/floats.md +117 -0
  38. package/skills/applying-slds/guidance/utilities/grid.md +264 -0
  39. package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
  40. package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
  41. package/skills/applying-slds/guidance/utilities/index.md +205 -0
  42. package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
  43. package/skills/applying-slds/guidance/utilities/layout.md +109 -0
  44. package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
  45. package/skills/applying-slds/guidance/utilities/margin.md +155 -0
  46. package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
  47. package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
  48. package/skills/applying-slds/guidance/utilities/padding.md +155 -0
  49. package/skills/applying-slds/guidance/utilities/position.md +177 -0
  50. package/skills/applying-slds/guidance/utilities/print.md +114 -0
  51. package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
  52. package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
  53. package/skills/applying-slds/guidance/utilities/themes.md +121 -0
  54. package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
  55. package/skills/applying-slds/guidance/utilities/typography.md +166 -0
  56. package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
  57. package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
  58. package/skills/applying-slds/metadata/README.md +84 -0
  59. package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
  60. package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
  61. package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
  62. package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
  63. package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
  64. package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
  65. package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
  66. package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
  67. package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
  68. package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
  69. package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
  70. package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
  71. package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
  72. package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
  73. package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
  74. package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
  75. package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
  76. package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
  77. package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
  78. package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
  79. package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
  80. package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
  81. package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
  82. package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
  83. package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
  84. package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
  85. package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
  86. package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
  87. package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
  88. package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
  89. package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
  90. package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
  91. package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
  92. package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
  93. package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
  94. package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
  95. package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
  96. package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
  97. package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
  98. package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
  99. package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
  100. package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
  101. package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
  102. package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
  103. package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
  104. package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
  105. package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
  106. package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
  107. package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
  108. package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
  109. package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
  110. package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
  111. package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
  112. package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
  113. package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
  114. package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
  115. package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
  116. package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
  117. package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
  118. package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
  119. package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
  120. package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
  121. package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
  122. package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
  123. package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
  124. package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
  125. package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
  126. package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
  127. package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
  128. package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
  129. package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
  130. package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
  131. package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
  132. package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
  133. package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
  134. package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
  135. package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
  136. package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
  137. package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
  138. package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
  139. package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
  140. package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
  141. package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
  142. package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
  143. package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
  144. package/skills/applying-slds/metadata/hooks-index.json +6272 -0
  145. package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
  146. package/skills/applying-slds/metadata/utilities-index.json +21912 -0
  147. package/skills/applying-slds/references/component-selection.md +112 -0
  148. package/skills/applying-slds/references/icons-decision-guide.md +124 -0
  149. package/skills/applying-slds/references/styling-decision-guide.md +228 -0
  150. package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
  151. package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
  152. package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
  153. package/skills/applying-slds/scripts/search-icons.cjs +174 -0
  154. package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
  155. package/skills/building-mobile-apps/SKILL.md +0 -1
  156. package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
  157. package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
  158. package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
  159. package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
  160. package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
  161. package/skills/building-sf-integrations/SKILL.md +0 -1
  162. package/skills/configuring-connected-apps/SKILL.md +0 -1
  163. package/skills/connecting-datacloud/SKILL.md +0 -1
  164. package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
  165. package/skills/debugging-apex-logs/SKILL.md +0 -1
  166. package/skills/deploying-metadata/SKILL.md +0 -1
  167. package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
  168. package/skills/developing-agentforce/SKILL.md +0 -1
  169. package/skills/fetching-salesforce-docs/SKILL.md +0 -1
  170. package/skills/generating-custom-lightning-type/SKILL.md +17 -39
  171. package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
  172. package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
  173. package/skills/generating-lwc-components/SKILL.md +0 -1
  174. package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
  175. package/skills/generating-visual-diagrams/SKILL.md +0 -1
  176. package/skills/handling-sf-data/SKILL.md +0 -1
  177. package/skills/harmonizing-datacloud/SKILL.md +0 -1
  178. package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
  179. package/skills/investigating-agentforce-architecture/README.md +156 -0
  180. package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
  181. package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
  182. package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
  183. package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
  184. package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
  185. package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
  186. package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
  187. package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
  188. package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
  189. package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
  190. package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
  191. package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
  192. package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
  193. package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
  194. package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
  195. package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
  196. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
  197. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
  198. package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
  199. package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
  200. package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
  201. package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
  202. package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
  203. package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
  204. package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
  205. package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
  206. package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
  207. package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
  208. package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
  209. package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
  210. package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
  211. package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
  212. package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
  213. package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
  214. package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
  215. package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
  216. package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
  217. package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
  218. package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
  219. package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
  220. package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
  221. package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
  222. package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
  223. package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
  224. package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
  225. package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
  226. package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
  227. package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
  228. package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
  229. package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
  230. package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
  231. package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
  232. package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
  233. package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
  234. package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
  235. package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
  236. package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
  237. package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
  238. package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
  239. package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
  240. package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
  241. package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
  242. package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
  243. package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
  244. package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
  245. package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
  246. package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
  247. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
  248. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
  249. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
  250. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
  251. package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
  252. package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
  253. package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
  254. package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
  255. package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
  256. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
  257. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
  258. package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
  259. package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
  260. package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
  261. package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
  262. package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
  263. package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
  264. package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
  265. package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
  266. package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
  267. package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
  268. package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
  269. package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
  270. package/skills/investigating-agentforce-d360/README.md +123 -0
  271. package/skills/investigating-agentforce-d360/SKILL.md +163 -0
  272. package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
  273. package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
  274. package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
  275. package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
  276. package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
  277. package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
  278. package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
  279. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
  280. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
  281. package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
  282. package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
  283. package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
  284. package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
  285. package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
  286. package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
  287. package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
  288. package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
  289. package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
  290. package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
  291. package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
  292. package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
  293. package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
  294. package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
  295. package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
  296. package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
  297. package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
  298. package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
  299. package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
  300. package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
  301. package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
  302. package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
  303. package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
  304. package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
  305. package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
  306. package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
  307. package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
  308. package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
  309. package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
  310. package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
  311. package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
  312. package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
  313. package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
  314. package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
  315. package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
  316. package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
  317. package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
  318. package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
  319. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
  320. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
  321. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
  322. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
  323. package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
  324. package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
  325. package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
  326. package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
  327. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
  328. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
  329. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
  330. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
  331. package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
  332. package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
  333. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
  334. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
  335. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
  336. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
  337. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
  338. package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
  339. package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
  340. package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
  341. package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
  342. package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
  343. package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
  344. package/skills/managing-managed-event-subscription/SKILL.md +152 -0
  345. package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
  346. package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
  347. package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
  348. package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
  349. package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
  350. package/skills/observing-agentforce/SKILL.md +0 -1
  351. package/skills/orchestrating-datacloud/SKILL.md +0 -1
  352. package/skills/preparing-datacloud/SKILL.md +0 -1
  353. package/skills/querying-soql/SKILL.md +0 -1
  354. package/skills/retrieving-datacloud/SKILL.md +0 -1
  355. package/skills/running-apex-tests/SKILL.md +0 -1
  356. package/skills/running-code-analyzer/SKILL.md +0 -1
  357. package/skills/segmenting-datacloud/SKILL.md +0 -1
  358. package/skills/testing-agentforce/SKILL.md +0 -1
  359. package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
  360. package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
  361. package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
  362. package/skills/validating-slds/SKILL.md +262 -0
  363. package/skills/validating-slds/references/quality-checks.md +308 -0
  364. package/skills/validating-slds/references/report-format.md +302 -0
  365. package/skills/validating-slds/scripts/analyze-quality.cjs +521 -0
@@ -0,0 +1,1045 @@
1
+ """Fetch all 24 Data Cloud artifacts for one Agentforce session.
2
+
3
+ Given a session UUID and an `sf` org alias, this CLI drives the waterfall
4
+ of DC queries defined in `assets/dc/*.sql`, lands each result under the
5
+ nested layout:
6
+
7
+ DATA_ROOT/<org_id_15>/<agent_api_name>__<agent_version>/<sid>/dc.<name>.json
8
+
9
+ …via `storage.save`, and emits a `dc._session_manifest.json` summarizing
10
+ counts, timings, and empty-by-design reasons.
11
+
12
+ Design contract:
13
+ - SQL loaded via `dc.load_sql(name, **params)` only — no inline concat.
14
+ - Responses parsed via `dc.parse(response)` — no inline dict digging.
15
+ - Persistence via `storage.save(data, org, agent, ver, sid, "dc", name)`
16
+ — no direct writes. Every on-disk path is validated by
17
+ ``paths.session_dir`` before it's touched.
18
+ - Identity is resolved BEFORE the first write. The sessions + participants
19
+ queries run first so (org_id_15, agent_api_name, agent_version) can be
20
+ derived; those three segments name the session dir.
21
+ - Every output file is written exactly once; empty artifacts go through
22
+ `_fetch_empty(name, reason)` so the manifest records why.
23
+ - Rerunning the same session overwrites prior artifacts.
24
+
25
+ Invocation:
26
+ python3 scripts/fetch_dc.py --session <uuid> --org <alias> [--verbose]
27
+
28
+ After the waterfall finishes, the fetcher chains two downstream steps:
29
+ 1. assemble_dc.main_for_session → dc._session_tree.json
30
+ (skip with --no-assemble).
31
+ 2. render_dc.main_for_session → dc._session_summary.md
32
+ (skip with --no-render). Runs against the on-disk tree from step 1
33
+ or a prior run.
34
+
35
+ ## DC-access preflight
36
+
37
+ Before the waterfall starts, we run a single cheap probe:
38
+
39
+ SELECT Id FROM ssot__AIAgentSession__dlm WHERE ssot__Id__c = '<esc_sid>' LIMIT 1
40
+
41
+ If the probe fails (401/403, no org alias, resolve_org failure), we raise
42
+ ``DcAccessDenied`` and the top-level ``main()`` branches on
43
+ ``sys.stdin.isatty()``:
44
+
45
+ - Interactive (tty): print a 2-option menu, read one line from stdin.
46
+ (1) retry with a different --org (2) cancel.
47
+ - Headless (no tty): emit a single-line JSON preamble to stdout and
48
+ ``exit(10)`` immediately. Orchestrators (subagents, CI, ``claude -p``)
49
+ see ``exit(10)`` as the stable contract for "DC access denied".
50
+
51
+ ``exit(10)`` is the stable contract between this skill and any orchestrator
52
+ wrapper — a non-zero exit code that is distinct from generic errors.
53
+ """
54
+ from __future__ import annotations
55
+
56
+ import argparse
57
+ import html
58
+ import json
59
+ import re
60
+ import subprocess
61
+ import sys
62
+ import time
63
+ from datetime import datetime, timezone
64
+ from pathlib import Path
65
+
66
+ sys.path.insert(0, str(Path(__file__).parent))
67
+
68
+ from config import DATA_ROOT, paths, sql as _sql_mod
69
+ from dc import SQL_DIR, DCQueryError, load_sql, post, resolve_org
70
+ from storage import save
71
+
72
+
73
+ # ---- preflight / exit contracts -------------------------------------------
74
+
75
+ # Exit code reserved for "DC access denied".
76
+ # Orchestrators grep for this specifically. See module docstring for the full
77
+ # contract; do not reuse for other failure modes.
78
+ EXIT_DC_ACCESS_DENIED = 10
79
+
80
+
81
+ class DcAccessDenied(RuntimeError):
82
+ """DC probe failed in a way that tells us DC is unusable for this session.
83
+
84
+ Carries both a short machine-readable ``reason`` (for the JSON preamble
85
+ payload — e.g. ``"401"``, ``"no_org"``, ``"resolve_org_failed"``) and a
86
+ longer human-readable ``detail`` (surfaced in the interactive prompt).
87
+ """
88
+
89
+ def __init__(self, reason: str, detail: str) -> None:
90
+ self.reason = reason
91
+ self.detail = detail
92
+ super().__init__(f"{reason}: {detail}")
93
+
94
+
95
+ def preflight_dc_access(
96
+ session_id: str,
97
+ org_alias: str,
98
+ ) -> tuple[str, str]:
99
+ """Probe DC for this session. Returns (instance_url, token) on success.
100
+
101
+ Runs the cheapest possible DC query — a single ``LIMIT 1`` against
102
+ ``ssot__AIAgentSession__dlm`` — to establish that:
103
+ - ``sf`` CLI can resolve the org alias to an instance URL + token.
104
+ - DC accepts the token (no 401/403).
105
+ - The session id is well-formed SOQL (no literal injection).
106
+
107
+ Raises ``DcAccessDenied`` on failure. The ``session_id`` is escaped
108
+ via ``sql._escape_sql_literal`` before being interpolated, matching
109
+ the Batch-C defense for ``_session_row_live``. The probe does NOT
110
+ require the session to exist — a zero-row response is success
111
+ (proves DC is reachable and auth'd).
112
+ """
113
+ # Stage 1: resolve org alias → instance_url + token. Any failure here
114
+ # is classified as "no_org" / "resolve_org_failed" — we never got far
115
+ # enough to hit DC.
116
+ try:
117
+ instance_url, token = resolve_org(org_alias)
118
+ except SystemExit as e:
119
+ raise DcAccessDenied("no_org", str(e)) from e
120
+
121
+ # Stage 2: issue the probe. Reuse ``sessions`` template with a narrow
122
+ # WHERE clause + LIMIT 1. The escape hardens against injection at the
123
+ # literal interpolation point.
124
+ esc_sid = _sql_mod._escape_sql_literal(session_id)
125
+ probe_sql = (
126
+ f"SELECT ssot__Id__c FROM ssot__AIAgentSession__dlm "
127
+ f"WHERE ssot__Id__c = '{esc_sid}' LIMIT 1"
128
+ )
129
+ try:
130
+ post(probe_sql, instance_url, token, "preflight")
131
+ except DCQueryError as e:
132
+ msg = str(e)
133
+ # Classify based on the HTTP code in the error message. The error
134
+ # body carries ``http=<code>`` right at the start per dc.post().
135
+ if "http=401" in msg:
136
+ raise DcAccessDenied("401", msg) from e
137
+ if "http=403" in msg:
138
+ raise DcAccessDenied("403", msg) from e
139
+ raise DcAccessDenied("dc_probe_failed", msg) from e
140
+ return instance_url, token
141
+
142
+
143
+ def _emit_dc_access_denied_preamble(
144
+ reason: str,
145
+ detail: str,
146
+ ) -> None:
147
+ """Headless fallback: emit the machine-readable preamble to stdout.
148
+
149
+ Single-line JSON per the Batch-D plan. Followed by ``exit(EXIT_DC_ACCESS_DENIED)``
150
+ by the caller. Kept separate so tests can drive it without spawning a
151
+ subprocess.
152
+ """
153
+ payload = {
154
+ "status": "DC_ACCESS_DENIED",
155
+ "reason": reason,
156
+ "detail": detail,
157
+ "options": [
158
+ {"code": "1", "action": "retry", "arg": "--org <alias>"},
159
+ {"code": "2", "action": "cancel"},
160
+ ],
161
+ }
162
+ print(json.dumps(payload))
163
+
164
+
165
+ def _handle_dc_access_denied(
166
+ exc: DcAccessDenied,
167
+ *,
168
+ session_id: str,
169
+ is_tty: bool,
170
+ ) -> int:
171
+ """Branch on tty presence and return an exit code.
172
+
173
+ Interactive: print the 2-option menu; read one line from stdin.
174
+ - ``1`` → signal caller to retry (returns ``EXIT_DC_ACCESS_DENIED``;
175
+ outer ``main()`` may re-prompt for alias in a bounded loop — for
176
+ now we surface the exit code and let the user re-invoke).
177
+ - ``2`` → return 0 (user cancelled; graceful exit).
178
+
179
+ Headless: emit JSON preamble + ``EXIT_DC_ACCESS_DENIED`` immediately.
180
+ """
181
+ if not is_tty:
182
+ _emit_dc_access_denied_preamble(exc.reason, exc.detail)
183
+ return EXIT_DC_ACCESS_DENIED
184
+
185
+ _log(
186
+ f"\nThis skill requires Data Cloud access, which failed: "
187
+ f"{exc.detail}\nYour options:\n"
188
+ f" (1) Retry with a different org alias (pass --org <alias>)\n"
189
+ f" (2) Cancel\nPick one: "
190
+ )
191
+ try:
192
+ choice = sys.stdin.readline().strip()
193
+ except (KeyboardInterrupt, EOFError):
194
+ return 0
195
+ if choice == "1":
196
+ _log("Re-invoke with: python3 fetch_dc.py --session "
197
+ f"{session_id} --org <new-alias>")
198
+ return EXIT_DC_ACCESS_DENIED
199
+ # Any other response is treated as cancel.
200
+ return 0
201
+
202
+
203
+ # ---- constants -------------------------------------------------------------
204
+
205
+ _INTERNAL_TRACE_RE = re.compile(r'"internalTraceId":"([a-f0-9]+)"') # @rule-suppress starter-sec-002 — re.compile, not exec/eval
206
+ _NOT_SET = {"", "NOT_SET", None}
207
+ # Mirrors _shared/fs_guard.API_NAME_RE. Duplicated locally as a pre-flight
208
+ # gate so the cross-role fallback's adopted api_name string satisfies the
209
+ # path-builder's regex before we hand it to paths.session_dir(). Without
210
+ # this, paths.session_dir would reject the path with an opaque
211
+ # ValidationError after the fallback claimed success — operator confusion.
212
+ # Anchored \A...\Z + `.fullmatch()` (not ^...$ + .match) so trailing
213
+ # newlines don't slip through; matches the security pattern in fs_guard.
214
+ _API_NAME_RE = re.compile(r"\A[A-Za-z0-9_]+\Z") # @rule-suppress starter-sec-002 — re.compile, not exec/eval
215
+ # Mirrors _shared/fs_guard.AGENT_VERSION_RE. Used by the cross-role
216
+ # fallback so we never adopt a malformed version string into a session-dir
217
+ # path.
218
+ _VERSION_RE = re.compile(r"\Av[0-9]+\Z") # @rule-suppress starter-sec-002 — re.compile, not exec/eval
219
+
220
+ # All 24 .sql templates — checked for existence at startup.
221
+ _TEMPLATES = (
222
+ "sessions", "interactions", "messages", "moments", "participants",
223
+ "tag_associations", "gateway_requests",
224
+ "steps", "moment_interactions",
225
+ "telemetry_spans", "generations", "app_generation",
226
+ "gateway_request_tags", "gateway_responses", "gateway_records",
227
+ "gateway_request_metadata", "gateway_request_llm",
228
+ "feedback",
229
+ "content_quality", "content_category", "feedback_details",
230
+ "tag_definitions", "tag_definition_associations", "tags",
231
+ )
232
+
233
+
234
+ # ---- small helpers ---------------------------------------------------------
235
+
236
+ def _in_list(ids: list[str]) -> str:
237
+ """SQL `IN (…)` fragment. Dedupes, drops NOT_SET/empty, preserves order."""
238
+ seen: dict[str, None] = {}
239
+ for i in ids:
240
+ if i and i not in _NOT_SET and i not in seen:
241
+ seen[i] = None
242
+ return "(" + ",".join(f"'{i}'" for i in seen) + ")"
243
+
244
+
245
+ def _extract_trace_ids(interactions: list[dict]) -> list[str]:
246
+ """Pull runtime trace_ids from interaction rows.
247
+
248
+ `ssot__TelemetryTraceId__c` is often empty — the real trace_id lives
249
+ HTML-escaped inside `ssot__AttributeText__c` as `"internalTraceId":"…"`.
250
+ See references/dc_dmo_fields.md for the full gotcha.
251
+ """
252
+ out: list[str] = []
253
+ for r in interactions:
254
+ tid = r.get("ssot__TelemetryTraceId__c") or ""
255
+ if tid and tid not in _NOT_SET:
256
+ out.append(tid)
257
+ continue
258
+ attr = html.unescape(r.get("ssot__AttributeText__c") or "")
259
+ m = _INTERNAL_TRACE_RE.search(attr)
260
+ if m:
261
+ out.append(m.group(1))
262
+ # dedupe, preserve order
263
+ return list(dict.fromkeys(out))
264
+
265
+
266
+ def _preflight_templates() -> None:
267
+ """Refuse to start if any .sql template is missing."""
268
+ missing = [n for n in _TEMPLATES if not (SQL_DIR / f"{n}.sql").is_file()]
269
+ if missing:
270
+ raise SystemExit(
271
+ f"missing SQL templates in {SQL_DIR}: {', '.join(missing)}"
272
+ )
273
+
274
+
275
+ # ---- fetch helpers (single entry point for disk writes + manifest) --------
276
+
277
+ def _log(msg: str) -> None:
278
+ print(msg, file=sys.stderr, flush=True)
279
+
280
+
281
+ def _fetch(
282
+ ctx: dict,
283
+ wave: int,
284
+ name: str,
285
+ where: str,
286
+ order_by: str = "",
287
+ *,
288
+ join_path: str | None = None,
289
+ ) -> list[dict]:
290
+ """Run a live query, save result, append manifest entry, return rows."""
291
+ sql = load_sql(name, WHERE_CLAUSE=where, ORDER_BY=order_by)
292
+ if ctx["verbose"]:
293
+ _log(f" SQL[{name}]:\n{sql}\n")
294
+ t0 = time.monotonic()
295
+ try:
296
+ rows = post(sql, ctx["instance_url"], ctx["token"], name)
297
+ except DCQueryError as e:
298
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
299
+ _log(f" dc.{name:<30} ERROR ({elapsed_ms}ms)")
300
+ entry = {
301
+ "name": name, "wave": wave, "rows": 0,
302
+ "elapsed_ms": elapsed_ms, "status": "error",
303
+ "_unavailable_reason": str(e).splitlines()[0],
304
+ }
305
+ ctx["queries"].append(entry)
306
+ return []
307
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
308
+ # Only persist when there's data. Zero-row / error / skipped outcomes
309
+ # are recorded in the manifest (status + _unavailable_reason); a
310
+ # matching dc.<name>.json would be an empty `[]` file adding noise to
311
+ # the per-session directory. assemble_dc._load tolerates missing
312
+ # files by returning [].
313
+ if rows:
314
+ save(
315
+ rows,
316
+ ctx["org_id_15"],
317
+ ctx["agent_api_name"],
318
+ ctx["agent_version"],
319
+ ctx["session_id"],
320
+ "dc",
321
+ name,
322
+ )
323
+ entry = {
324
+ "name": name, "wave": wave, "rows": len(rows),
325
+ "elapsed_ms": elapsed_ms,
326
+ "status": "ok" if rows else "empty",
327
+ }
328
+ if join_path:
329
+ entry["join_path"] = join_path
330
+ if not rows:
331
+ entry["_unavailable_reason"] = (
332
+ f"query returned zero rows for where={where[:120]}"
333
+ )
334
+ ctx["queries"].append(entry)
335
+ _log(f" dc.{name:<30} rows={len(rows):<5} ({elapsed_ms}ms)")
336
+ return rows
337
+
338
+
339
+ def _fetch_empty(ctx: dict, wave: int, name: str, reason: str) -> None:
340
+ """Record a skipped query in the manifest — no HTTP call, no artifact file."""
341
+ ctx["queries"].append({
342
+ "name": name, "wave": wave, "rows": 0,
343
+ "elapsed_ms": 0, "status": "skipped",
344
+ "_unavailable_reason": reason,
345
+ })
346
+ _log(f" dc.{name:<30} SKIPPED ({reason})")
347
+
348
+
349
+ # ---- main entry point -----------------------------------------------------
350
+
351
+ def main() -> int:
352
+ ap = argparse.ArgumentParser(
353
+ prog="fetch_dc.py",
354
+ description="Fetch all 24 DC artifacts for one Agentforce session.",
355
+ )
356
+ ap.add_argument("--session", required=True,
357
+ help="AI-agent session UUID, OR a Salesforce MessagingSession id "
358
+ "(0Mw... prefix). Messaging ids are resolved to the UUID "
359
+ "via a one-row DC lookup before the waterfall starts.")
360
+ ap.add_argument("--org", required=True, help="sf org alias")
361
+ ap.add_argument("--verbose", action="store_true", help="dump each SQL before POST")
362
+ ap.add_argument("--no-assemble", action="store_true",
363
+ help="skip tree assembly after fetch")
364
+ ap.add_argument("--no-render", action="store_true",
365
+ help="skip summary markdown rendering")
366
+ # Runtime-agnostic path overrides; default to ~/.vibe/...
367
+ from _shared.cli_override import add_cli_flags, apply_overrides
368
+ add_cli_flags(ap)
369
+ args = ap.parse_args()
370
+ apply_overrides(args, caller_globals=globals())
371
+
372
+ _preflight_templates()
373
+
374
+ # Accept either a UUID or a MessagingSession id (0Mw...). The resolver
375
+ # passes UUIDs through unchanged; on messaging ids it tries disk first
376
+ # (any prior fetch left dc.sessions.json behind under DATA_ROOT), and
377
+ # falls back to a live one-row DC lookup only if disk misses. This keeps
378
+ # fetch_dc consistent with every other entry point and avoids a
379
+ # round-trip when re-invoking against an already-fetched session.
380
+ # On multi-match or zero-match the resolver exits with a diagnostic.
381
+ from resolve_session import is_messaging_id, resolve_disk_or_live
382
+ input_id = args.session
383
+ session_id = resolve_disk_or_live(input_id, org=args.org)
384
+ if is_messaging_id(input_id):
385
+ _log(f"resolved messaging id {input_id} → AiAgentSession {session_id}")
386
+
387
+ # Preflight — one cheap probe before the waterfall starts.
388
+ # Fails fast with DcAccessDenied on 401/403/no-org. The outer try
389
+ # block routes the exception through _handle_dc_access_denied, which
390
+ # emits either an interactive prompt or a JSON preamble depending on
391
+ # tty presence, then exits with EXIT_DC_ACCESS_DENIED (10).
392
+ try:
393
+ instance_url, token = preflight_dc_access(session_id, args.org)
394
+ except DcAccessDenied as exc:
395
+ return _handle_dc_access_denied(
396
+ exc,
397
+ session_id=session_id,
398
+ is_tty=sys.stdin.isatty(),
399
+ )
400
+ _log(f"org: {instance_url}")
401
+ _log(f"session: {session_id}\n")
402
+
403
+ ctx = {
404
+ "session_id": session_id,
405
+ "org_alias": args.org,
406
+ "instance_url": instance_url,
407
+ "token": token,
408
+ "verbose": args.verbose,
409
+ "queries": [],
410
+ "started_at": datetime.now(timezone.utc),
411
+ # Populated by _resolve_identity() before the first storage.save call.
412
+ "org_id_15": None,
413
+ "agent_api_name": None,
414
+ "agent_version": None,
415
+ }
416
+
417
+ _run_waterfall(ctx)
418
+
419
+ _write_manifest(ctx)
420
+
421
+ if not args.no_assemble:
422
+ from assemble_dc import main_for_session as _assemble
423
+ _assemble(args.session)
424
+
425
+ if not args.no_render:
426
+ from render_dc import main_for_session as _render
427
+ _render(args.session) # SystemExits if tree is missing
428
+
429
+ return 0
430
+
431
+
432
+ def _fetch_no_save(
433
+ ctx: dict, wave: int, name: str, where: str, order_by: str = "",
434
+ ) -> list[dict]:
435
+ """Variant of ``_fetch`` that skips the on-disk write.
436
+
437
+ Used for the identity-resolution phase: ``sessions`` and ``participants``
438
+ run before we know ``(org_id_15, agent_api_name, agent_version)``, which
439
+ are required to build the target session dir. We stash the rows and
440
+ write them via ``storage.save`` once identity is resolved.
441
+
442
+ Manifest entry is still appended so the final ``dc._session_manifest.json``
443
+ mirrors every query run.
444
+ """
445
+ sql = load_sql(name, WHERE_CLAUSE=where, ORDER_BY=order_by)
446
+ if ctx["verbose"]:
447
+ _log(f" SQL[{name}]:\n{sql}\n")
448
+ t0 = time.monotonic()
449
+ try:
450
+ rows = post(sql, ctx["instance_url"], ctx["token"], name)
451
+ except DCQueryError as e:
452
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
453
+ _log(f" dc.{name:<30} ERROR ({elapsed_ms}ms)")
454
+ ctx["queries"].append({
455
+ "name": name, "wave": wave, "rows": 0,
456
+ "elapsed_ms": elapsed_ms, "status": "error",
457
+ "_unavailable_reason": str(e).splitlines()[0],
458
+ })
459
+ return []
460
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
461
+ entry = {
462
+ "name": name, "wave": wave, "rows": len(rows),
463
+ "elapsed_ms": elapsed_ms,
464
+ "status": "ok" if rows else "empty",
465
+ }
466
+ if not rows:
467
+ entry["_unavailable_reason"] = (
468
+ f"query returned zero rows for where={where[:120]}"
469
+ )
470
+ ctx["queries"].append(entry)
471
+ _log(f" dc.{name:<30} rows={len(rows):<5} ({elapsed_ms}ms) [deferred save]")
472
+ return rows
473
+
474
+
475
+ def _lookup_org_id_via_sf_cli(org_alias: str) -> str:
476
+ """Return the 15-char org id from ``sf org display --target-org <alias>``.
477
+
478
+ Fallback path for ``_resolve_identity``: the DMO field
479
+ ``ssot__InternalOrganizationId__c`` is occasionally null on
480
+ otherwise-well-formed sessions (materialization gap in the SSOT
481
+ pipeline). Since the authenticated ``sf`` alias already knows the
482
+ org id, we ask the CLI directly rather than failing the whole run.
483
+
484
+ Matches the shell pattern used by ``dc.resolve_org`` — same argv, same
485
+ JSON shape (``.result.id``). Any non-zero exit, missing binary, or
486
+ malformed payload propagates to the caller as an exception so the
487
+ outer ``_resolve_identity`` can raise a unified "both sources failed"
488
+ diagnostic.
489
+ """
490
+ proc = subprocess.run(
491
+ ["sf", "org", "display", "--target-org", org_alias, "--json"],
492
+ capture_output=True, text=True, check=True,
493
+ )
494
+ payload = json.loads(proc.stdout)
495
+ org_id = (payload.get("result") or {}).get("id") or ""
496
+ if len(org_id) < 15:
497
+ raise ValueError(
498
+ f"sf org display returned result.id={org_id!r} (len<15)"
499
+ )
500
+ return org_id[:15]
501
+
502
+
503
+ def _resolve_identity(
504
+ sessions: list[dict],
505
+ participants: list[dict],
506
+ org_alias: str,
507
+ ) -> tuple[str, str, str]:
508
+ """Derive (org_id_15, agent_api_name, agent_version) from wave-1 rows.
509
+
510
+ - ``org_id_15`` — ``sessions[0].ssot__InternalOrganizationId__c[:15]``,
511
+ falling back to ``sf org display --target-org <org_alias>`` when the
512
+ DMO field is null/empty/shorter-than-15 (a known SSOT materialization
513
+ gap; the authenticated CLI alias knows the org id regardless).
514
+ - ``agent_api_name`` / ``agent_version`` — first AGENT participant row
515
+ (sorted by participant id for determinism) with both fields populated.
516
+
517
+ **Dominant-agent policy:** if multiple AGENT participants are present
518
+ (handoff sessions), the one with the lexicographically first
519
+ ``agent_api_name`` wins. Matches the ``sorted(agents_observed)[0]``
520
+ rule used by ``_session_row_live`` so every writer agrees on the
521
+ session's home dir.
522
+
523
+ Raises ``SystemExit`` with a clear diagnostic when any segment cannot
524
+ be derived. We deliberately do NOT fall back to a catch-all "unknown"
525
+ agent dir — writing under a synthetic identity would let every
526
+ ambiguous session pile into the same folder and corrupt the layout
527
+ invariant downstream readers depend on.
528
+ """
529
+ if not sessions:
530
+ raise SystemExit(
531
+ "fetch_dc: sessions query returned 0 rows; "
532
+ "cannot resolve org identity — is the session id correct?"
533
+ )
534
+ org_id_18 = sessions[0].get("ssot__InternalOrganizationId__c") or ""
535
+ if org_id_18 and len(org_id_18) >= 15:
536
+ org_id_15 = org_id_18[:15]
537
+ else:
538
+ # DMO field is null/empty/short. The session is otherwise well-formed
539
+ # — fall back to the authenticated sf alias for the org id.
540
+ try:
541
+ org_id_15 = _lookup_org_id_via_sf_cli(org_alias)
542
+ except (subprocess.CalledProcessError, FileNotFoundError,
543
+ json.JSONDecodeError, ValueError) as e:
544
+ raise SystemExit(
545
+ f"fetch_dc: both DMO field and sf org display failed — "
546
+ f"sessions[0].ssot__InternalOrganizationId__c={org_id_18!r}, "
547
+ f"sf org display --target-org {org_alias!r} error: "
548
+ f"{type(e).__name__}: {e}"
549
+ )
550
+ _log(
551
+ f" org_id_15 fallback to sf org display: {org_id_15} "
552
+ f"(DMO field was null)"
553
+ )
554
+
555
+ # Collect every AGENT participant with a non-empty agent_api_name +
556
+ # agent_version. Sort by (api_name, version) for a stable dominant-
557
+ # agent pick on handoffs.
558
+ candidates = sorted(
559
+ {
560
+ (
561
+ p.get("ssot__AiAgentApiName__c") or "",
562
+ p.get("ssot__AiAgentVersionApiName__c") or "",
563
+ )
564
+ for p in participants
565
+ if p.get("ssot__AiAgentSessionParticipantRole__c") == "AGENT"
566
+ }
567
+ )
568
+ # Drop entries missing either field.
569
+ candidates = [(n, v) for n, v in candidates if n and v and n not in _NOT_SET and v not in _NOT_SET]
570
+ if not candidates:
571
+ # Fallback: some agent shapes (e.g. MyAgent on Messaging)
572
+ # leave AGENT rows with api_name/version=NOT_SET while USER (or
573
+ # other-role) participant rows carry both fields. Harvest from
574
+ # ANY role first so we land on the real (api_name, version) pair
575
+ # — that's what assemble_dc later reconciles into the tree, so
576
+ # the dir name and tree.identity will agree out of the gate
577
+ # rather than dir=__v0/ + tree=__v24/.
578
+ cross_role = sorted({
579
+ (
580
+ p.get("ssot__AiAgentApiName__c") or "",
581
+ p.get("ssot__AiAgentVersionApiName__c") or "",
582
+ )
583
+ for p in participants
584
+ })
585
+ cross_role = [
586
+ (n, v) for n, v in cross_role
587
+ if n and n not in _NOT_SET
588
+ and v and v not in _NOT_SET
589
+ and _API_NAME_RE.fullmatch(n)
590
+ and _VERSION_RE.fullmatch(v)
591
+ ]
592
+ if cross_role:
593
+ api_name, version = cross_role[0]
594
+ _log(
595
+ f" identity fallback: strict AGENT-row resolution failed; "
596
+ f"using api_name={api_name!r} version={version!r} from a "
597
+ f"non-AGENT participant row"
598
+ )
599
+ return org_id_15, api_name, version
600
+ # Last-ditch fallback: api_name from any role, but no version
601
+ # available anywhere. Stamp version='v0' (placeholder satisfying
602
+ # ^v[0-9]+$). Without this, the entire DC pipeline is unrunnable
603
+ # for those sessions even though all the downstream rows exist.
604
+ # The Builder Previewer shape legitimately lands here — every
605
+ # version_api_name in DC for that session truly is empty/v0.
606
+ any_names = sorted({
607
+ (p.get("ssot__AiAgentApiName__c") or "")
608
+ for p in participants
609
+ })
610
+ any_names = [n for n in any_names if n and n not in _NOT_SET]
611
+ # Only promote names that satisfy fs_guard's API_NAME_RE — otherwise
612
+ # paths.session_dir would reject the dir later with an opaque
613
+ # ValidationError. Cleaner UX is to fall through to the original
614
+ # SystemExit, which carries actionable diagnostic text.
615
+ any_names = [n for n in any_names if _API_NAME_RE.fullmatch(n)]
616
+ if any_names:
617
+ _log(
618
+ f" identity fallback: strict AGENT-row resolution failed; "
619
+ f"no participant row carries (api_name, version) together; "
620
+ f"using api_name={any_names[0]!r} from any participant row, "
621
+ f"version='v0' (placeholder)"
622
+ )
623
+ return org_id_15, any_names[0], "v0"
624
+ raise SystemExit(
625
+ "fetch_dc: no AGENT participants with agent_api_name + "
626
+ "agent_version on this session; cannot resolve identity. "
627
+ "If the session was created recently, STDM materialization "
628
+ "may not have completed yet — retry in a few minutes. "
629
+ "Otherwise the session may be malformed."
630
+ )
631
+ agent_api_name, agent_version = candidates[0]
632
+ return org_id_15, agent_api_name, agent_version
633
+
634
+
635
+ def _run_waterfall(ctx: dict) -> None:
636
+ """5-wave forward-only orchestration.
637
+
638
+ Wave 1 fetches session-scoped rows directly keyed on the session id.
639
+ Wave 2 fetches interaction-scoped rows (Steps, MomentInteractions).
640
+ Wave 3 fans out to Generation (via Step.GenerationId) and to the full
641
+ Gateway audit chain (via GatewayRequest.sessionId__c harvested in
642
+ wave 1 — GatewayResponse, Tags, ObjRecord, Metadata, LLM).
643
+ Wave 4 fetches Generation-scoped child rows (Quality, Category,
644
+ Feedback children).
645
+ Wave 5 fetches the agent/tag catalog (not session-keyed).
646
+
647
+ Every edge is forward from Session. See `references/dc_dmo_fields.md`
648
+ for the full join map.
649
+
650
+ Wave-1 ordering: ``sessions`` + ``participants`` run first with
651
+ deferred saves so ``_resolve_identity`` can compute the session's
652
+ target dir BEFORE any on-disk write. Without this ordering, the
653
+ first ``storage.save`` call would fail validation (``paths.session_dir``
654
+ requires the 3 identity segments). Once identity is stamped on
655
+ ``ctx``, remaining wave-1 queries save through the normal ``_fetch``
656
+ path.
657
+ """
658
+ sid = ctx["session_id"]
659
+ sid_q = f"'{sid}'"
660
+
661
+ # ---- wave 1a: identity resolution (deferred save) --------------------
662
+ _log("== wave 1a: identity resolution ==")
663
+ sessions = _fetch_no_save(ctx, 1, "sessions",
664
+ f"ssot__Id__c = {sid_q}")
665
+ participants = _fetch_no_save(ctx, 1, "participants",
666
+ f"ssot__AiAgentSessionId__c = {sid_q}")
667
+ (
668
+ ctx["org_id_15"],
669
+ ctx["agent_api_name"],
670
+ ctx["agent_version"],
671
+ ) = _resolve_identity(sessions, participants, ctx["org_alias"])
672
+ _log(
673
+ f" identity: org={ctx['org_id_15']} "
674
+ f"agent={ctx['agent_api_name']} version={ctx['agent_version']}"
675
+ )
676
+
677
+ # Flush deferred writes now that identity is known. Empty results
678
+ # stay unwritten (same contract as ``_fetch``).
679
+ if sessions:
680
+ save(
681
+ sessions, ctx["org_id_15"], ctx["agent_api_name"],
682
+ ctx["agent_version"], sid, "dc", "sessions",
683
+ )
684
+ if participants:
685
+ save(
686
+ participants, ctx["org_id_15"], ctx["agent_api_name"],
687
+ ctx["agent_version"], sid, "dc", "participants",
688
+ )
689
+
690
+ # ---- wave 1b: remaining session-scoped (5 queries) -------------------
691
+ _log("\n== wave 1b: session-scoped ==")
692
+ interactions = _fetch(ctx, 1, "interactions",
693
+ f"ssot__AiAgentSessionId__c = {sid_q}",
694
+ "ORDER BY ssot__StartTimestamp__c")
695
+ messages = _fetch(ctx, 1, "messages",
696
+ f"ssot__AiAgentSessionId__c = {sid_q}",
697
+ "ORDER BY ssot__MessageSentTimestamp__c")
698
+ moments = _fetch(ctx, 1, "moments",
699
+ f"ssot__AiAgentSessionId__c = {sid_q}",
700
+ "ORDER BY ssot__StartTimestamp__c")
701
+ _fetch(ctx, 1, "tag_associations",
702
+ f"ssot__AiAgentSessionId__c = {sid_q}")
703
+ # sessionId__c is stored as a literal quoted string ('"<uuid>"'); raw-UUID
704
+ # exact match returns 0 rows. LIKE handles both '"<uuid>"' and any future
705
+ # variant cleanly. See references/dc_dmo_fields.md.
706
+ gw_requests = _fetch(ctx, 1, "gateway_requests",
707
+ f"sessionId__c LIKE '%{sid}%'",
708
+ "ORDER BY timestamp__c")
709
+
710
+ # Harvest IDs from wave 1 for downstream waves
711
+ interaction_ids = [r.get("ssot__Id__c") for r in interactions]
712
+ # Agent name harvest: moments DMO is sparse — many short or
713
+ # abandoned-before-tagging sessions never write a moment row, leaving
714
+ # agent_api_names empty even though the agent identity was already
715
+ # resolved upstream. Fall through to participants (always populated
716
+ # for any session with ≥1 turn) and finally to ctx.agent_api_name
717
+ # (resolved during identity wave 1a from BotDefinition).
718
+ _harvested = {
719
+ r["ssot__AiAgentApiName__c"]
720
+ for r in moments
721
+ if r.get("ssot__AiAgentApiName__c") and r["ssot__AiAgentApiName__c"] not in _NOT_SET
722
+ }
723
+ _harvested |= {
724
+ r["ssot__AiAgentApiName__c"]
725
+ for r in participants
726
+ if r.get("ssot__AiAgentApiName__c") and r["ssot__AiAgentApiName__c"] not in _NOT_SET
727
+ }
728
+ if not _harvested and ctx.get("agent_api_name"):
729
+ _harvested = {ctx["agent_api_name"]}
730
+ agent_api_names = sorted(_harvested)
731
+ gw_req_ids = [r.get("gatewayRequestId__c") for r in gw_requests]
732
+ trace_ids = _extract_trace_ids(interactions)
733
+ _log(f" harvested: {len(interaction_ids)} interactions, "
734
+ f"{len(moments)} moments, trace_ids={len(trace_ids)}, "
735
+ f"gw_req_ids={len(gw_req_ids)}, agents={agent_api_names}")
736
+
737
+ # ---- wave 2: interaction/moment-scoped (2 queries) --------------------
738
+ _log("\n== wave 2: interaction/moment-scoped ==")
739
+ interaction_in = _in_list(interaction_ids)
740
+ if interaction_in != "()":
741
+ steps = _fetch(ctx, 2, "steps",
742
+ f"ssot__AiAgentInteractionId__c IN {interaction_in}",
743
+ "ORDER BY ssot__StartTimestamp__c")
744
+ _fetch(ctx, 2, "moment_interactions",
745
+ f"ssot__AiAgentInteractionId__c IN {interaction_in}")
746
+ else:
747
+ steps = []
748
+ _fetch_empty(ctx, 2, "steps",
749
+ "no interactions for session")
750
+ _fetch_empty(ctx, 2, "moment_interactions",
751
+ "no interactions for session")
752
+
753
+ # Harvest generation IDs from steps for forward fetch of GenAIGeneration.
754
+ # (Step.ssot__GenAiGatewayResponseId__c / ssot__GenAiGatewayRequestId__c are
755
+ # NOT harvested — those would be backward chains. Gateway audit rows are
756
+ # fetched forward from the session via GatewayRequest.sessionId__c in wave 1.)
757
+ step_gen_ids = [s.get("ssot__GenerationId__c") for s in steps]
758
+ _log(f" harvested: generation_ids={len([x for x in step_gen_ids if x and x not in _NOT_SET])}")
759
+
760
+ # ---- wave 3: trace/generation/gateway fanout (6 queries) --------------
761
+ _log("\n== wave 3: trace/generation/gateway fanout ==")
762
+
763
+ # telemetry_spans — on trace_ids from interactions
764
+ trace_in = _in_list(trace_ids)
765
+ if trace_in != "()":
766
+ _fetch(ctx, 3, "telemetry_spans",
767
+ f"ssot__TelemetryTrace__c IN {trace_in}",
768
+ "ORDER BY ssot__StartDateTime__c")
769
+ else:
770
+ _fetch_empty(ctx, 3, "telemetry_spans",
771
+ "no trace_ids extractable from interactions "
772
+ "(TelemetryTraceId__c empty and no internalTraceId in AttributeText__c)")
773
+
774
+ # generations — canonical path: steps.ssot__GenerationId__c
775
+ gen_in = _in_list(step_gen_ids)
776
+ if gen_in != "()":
777
+ generations = _fetch(ctx, 3, "generations",
778
+ f"generationId__c IN {gen_in}",
779
+ "ORDER BY timestamp__c",
780
+ join_path="steps.ssot__GenerationId__c")
781
+ # app_generation — same join key as generations (sibling DMO)
782
+ _fetch(ctx, 3, "app_generation",
783
+ f"generationId__c IN {gen_in}",
784
+ "ORDER BY timestamp__c",
785
+ join_path="steps.ssot__GenerationId__c")
786
+ else:
787
+ generations = []
788
+ _fetch_empty(ctx, 3, "generations",
789
+ "no step.ssot__GenerationId__c values on this session "
790
+ "(all steps had NOT_SET or steps table was empty)")
791
+ _fetch_empty(ctx, 3, "app_generation",
792
+ "no step.ssot__GenerationId__c values on this session "
793
+ "(all steps had NOT_SET or steps table was empty)")
794
+
795
+ # --- Gateway audit chain (forward-only from session) ------------------
796
+ # All audit rows flow forward from Session via GatewayRequest.sessionId__c
797
+ # (harvested in wave 1). Every child table is keyed on the authoritative
798
+ # gw_req_ids from that wave-1 fetch:
799
+ #
800
+ # GenAIGatewayResponse generationRequestId__c IN {gw_req_ids}
801
+ # GenAIGatewayRequestTag parent__c IN {gw_req_ids}
802
+ # GenAIGtwyObjRecord parent__c IN {gw_req_ids}
803
+ # GenAIGtwyRequestMetadata parent__c IN {gw_req_ids}
804
+ # GenAIGtwyRequestLLM parent__c IN {gw_req_ids}
805
+ #
806
+ # No Step->Response->Request chain. Step's GenAiGatewayRequestId__c and
807
+ # GenAiGatewayResponseId__c FKs exist but we don't harvest them: they
808
+ # only cover LLM_STEP-owned calls and miss nested features like
809
+ # PromptTemplateGenerationsInvocable. The session-direct fetch in wave 1
810
+ # is the authoritative set.
811
+ gw_req_in = _in_list(gw_req_ids)
812
+ if gw_req_in != "()":
813
+ _fetch(ctx, 3, "gateway_responses",
814
+ f"generationRequestId__c IN {gw_req_in}",
815
+ "ORDER BY timestamp__c",
816
+ join_path="gateway_requests.gatewayRequestId__c")
817
+ _fetch(ctx, 3, "gateway_request_tags",
818
+ f"parent__c IN {gw_req_in}")
819
+ _fetch(ctx, 3, "gateway_records",
820
+ f"parent__c IN {gw_req_in}")
821
+ _fetch(ctx, 3, "gateway_request_metadata",
822
+ f"parent__c IN {gw_req_in}")
823
+ _fetch(ctx, 3, "gateway_request_llm",
824
+ f"parent__c IN {gw_req_in}")
825
+ else:
826
+ gw_empty_reason = (
827
+ "no gateway_requests matched GatewayRequest.sessionId__c LIKE "
828
+ "'%<sid>%' in wave 1 — Trust Layer gateway logging may be "
829
+ "disabled for this org, or the session produced no LLM calls"
830
+ )
831
+ for n in ("gateway_responses", "gateway_request_tags", "gateway_records",
832
+ "gateway_request_metadata", "gateway_request_llm"):
833
+ _fetch_empty(ctx, 3, n, gw_empty_reason)
834
+
835
+ # feedback — keyed by generationId (no session col on GenAIFeedback)
836
+ gen_ids_for_feedback = [r.get("generationId__c") for r in generations]
837
+ feedback_in = _in_list(gen_ids_for_feedback)
838
+ if feedback_in != "()":
839
+ feedback = _fetch(ctx, 3, "feedback",
840
+ f"generationId__c IN {feedback_in}")
841
+ else:
842
+ feedback = []
843
+ _fetch_empty(ctx, 3, "feedback",
844
+ "no generation_ids available (generations wave was empty)")
845
+
846
+ # ---- wave 4: generation/feedback-dependent (3 queries) ----------------
847
+ _log("\n== wave 4: generation/feedback-dependent ==")
848
+ gen_only_in = _in_list(gen_ids_for_feedback)
849
+ if gen_only_in != "()":
850
+ quality = _fetch(ctx, 4, "content_quality",
851
+ f"parent__c IN {gen_only_in}")
852
+ else:
853
+ quality = []
854
+ _fetch_empty(ctx, 4, "content_quality",
855
+ "no generation_ids (parent__c IN cannot be empty)")
856
+
857
+ # content_category — parent is (generation_ids ∪ quality.id__c)
858
+ quality_ids = [r.get("id__c") or r.get("ssot__Id__c") for r in quality]
859
+ category_parent_ids = gen_ids_for_feedback + quality_ids
860
+ category_in = _in_list(category_parent_ids)
861
+ if category_in != "()":
862
+ _fetch(ctx, 4, "content_category",
863
+ f"parent__c IN {category_in}")
864
+ else:
865
+ _fetch_empty(ctx, 4, "content_category",
866
+ "no generation_ids or quality_ids to parent to")
867
+
868
+ # feedback_details — parent is feedback row id.
869
+ # GenAIFeedback__dlm PK is `feedbackId__c` (no ssot__ prefix, verified via describe);
870
+ # the old harvest used ssot__Id__c which doesn't exist on this DMO.
871
+ feedback_ids = [r.get("feedbackId__c") for r in feedback]
872
+ feedback_ids_in = _in_list(feedback_ids)
873
+ if feedback_ids_in != "()":
874
+ _fetch(ctx, 4, "feedback_details",
875
+ f"parent__c IN {feedback_ids_in}")
876
+ else:
877
+ _fetch_empty(ctx, 4, "feedback_details",
878
+ "no feedback rows for this session "
879
+ "(no user thumbs-up/down on any generation)")
880
+
881
+ # ---- wave 5: tag catalog (agent-scoped) -------------------------------
882
+ _log("\n== wave 5: tag catalog ==")
883
+ # Live orgs use 'Available' (not Help-doc's 'Active'). If that returns
884
+ # zero rows, retry unfiltered — some orgs have a different enum value or
885
+ # the definitions predate the Status field. See references/dc_dmo_fields.md.
886
+ tag_defs = _fetch(ctx, 5, "tag_definitions",
887
+ "ssot__Status__c = 'Available'")
888
+ if not tag_defs:
889
+ _log(" (tag_definitions Status='Available' empty — retrying unfiltered)")
890
+ # Pop the empty entry so _fetch can append a fresh one for the retry.
891
+ # The retry overwrites the same on-disk artifact (last run wins).
892
+ ctx["queries"].pop()
893
+ tag_defs = _fetch(ctx, 5, "tag_definitions",
894
+ "ssot__Id__c IS NOT NULL")
895
+ # Tag the retry in the manifest for traceability.
896
+ ctx["queries"][-1]["notes"] = (
897
+ "retried unfiltered after Status='Available' returned 0 rows"
898
+ )
899
+ if not tag_defs:
900
+ ctx["queries"][-1]["_unavailable_reason"] = (
901
+ "tag_definitions truly empty on this org"
902
+ )
903
+
904
+ agent_in = _in_list(agent_api_names)
905
+ if agent_in != "()":
906
+ _fetch(ctx, 5, "tag_definition_associations",
907
+ f"ssot__AiAgentApiName__c IN {agent_in}")
908
+ else:
909
+ _fetch_empty(ctx, 5, "tag_definition_associations",
910
+ "no agent api names observed in moments")
911
+
912
+ def_ids = [r.get("ssot__Id__c") for r in tag_defs]
913
+ def_in = _in_list(def_ids)
914
+ if def_in != "()":
915
+ _fetch(ctx, 5, "tags",
916
+ f"ssot__AiAgentTagDefinitionId__c IN {def_in}")
917
+ else:
918
+ _fetch_empty(ctx, 5, "tags",
919
+ "no tag_definition ids (tag_definitions was empty)")
920
+
921
+ # Tally steps by type for the session_shape classifier.
922
+ steps_by_type = {
923
+ "LLM_STEP": 0, "ACTION_STEP": 0, "TOPIC_STEP": 0,
924
+ "TRUST_GUARDRAILS_STEP": 0, "SESSION_END": 0,
925
+ }
926
+ for s in steps:
927
+ t = s.get("ssot__AiAgentInteractionStepType__c")
928
+ if t in steps_by_type:
929
+ steps_by_type[t] += 1
930
+
931
+ # Stash harvested-id counts for the manifest
932
+ steps_with_gen = sum(1 for g in step_gen_ids if g and g not in _NOT_SET)
933
+ ctx["harvested_ids"] = {
934
+ "sessions": len(sessions),
935
+ "messages": len(messages),
936
+ "interactions": len(interaction_ids),
937
+ "moments": len(moments),
938
+ "steps_total": len(steps),
939
+ "steps_by_type": steps_by_type,
940
+ "steps_with_generation_id": steps_with_gen,
941
+ "trace_ids_from_interactions": len(trace_ids),
942
+ "gateway_request_ids": len(gw_req_ids),
943
+ "generation_ids": sum(
944
+ 1 for g in gen_ids_for_feedback if g and g not in _NOT_SET
945
+ ),
946
+ "agents_observed": agent_api_names,
947
+ }
948
+
949
+ # Classify session shape. 5-value enum; rules evaluated top-to-bottom, first-match-wins.
950
+ # See references/dc_dmo_fields.md for rationale.
951
+ ctx["session_shape"] = _classify_session_shape(
952
+ sessions_count=len(sessions),
953
+ steps_total=len(steps),
954
+ llm_step_count=steps_by_type["LLM_STEP"],
955
+ steps_with_generation_id=steps_with_gen,
956
+ gw_req_count=len(gw_req_ids),
957
+ )
958
+
959
+
960
+ def _classify_session_shape(*, sessions_count, steps_total, llm_step_count,
961
+ steps_with_generation_id, gw_req_count):
962
+ """6-value session-shape diagnostic. First match wins.
963
+
964
+ Rules (top-to-bottom):
965
+ - session_not_found — sessions.json returned 0 rows
966
+ - interactions_not_materialized_yet — gw_reqs > 0 AND steps == 0. Gateway DMOs
967
+ materialize within minutes; STDM Interaction
968
+ / Step / Message DMOs can take hours to days.
969
+ Detected BEFORE abandoned_before_llm because
970
+ gw_req_count > 0 is a stronger positive signal
971
+ than steps_total > 0 (and the two rules'
972
+ inputs are disjoint on this path — steps == 0
973
+ here, gw_reqs == 0 for abandoned).
974
+ - abandoned_before_llm — steps > 0, LLM_STEP == 0, gw_reqs == 0
975
+ - gateway_requests_dropped_by_stdm — LLM_STEP > 0
976
+ AND gw_reqs == 0 AND steps_with_generation_id == 0.
977
+ STDM dropped not just gateway_requests but also
978
+ generations (the join chain Step→Generation
979
+ is broken). Frequently observed on Atlas-routed
980
+ sessions; visible in Splunk LLMGatewayUsageEventWriter
981
+ even when DC has zero rows.
982
+ - planner_ran_no_gateway_logs — LLM_STEP > 0 AND steps_with_generation_id > 0
983
+ AND gw_reqs == 0 (the extra guard prevents a
984
+ broken generations-IN-clause from being
985
+ misclassified here). Generations wrote to DC,
986
+ gateway_requests did not — narrower defect than
987
+ gateway_requests_dropped_by_stdm.
988
+ - complete — everything else (the "normal" bucket,
989
+ including partial chain-orphan sessions)
990
+
991
+ See references/dc_dmo_fields.md for rationale.
992
+ """
993
+ if sessions_count == 0:
994
+ return "session_not_found"
995
+ if gw_req_count > 0 and steps_total == 0:
996
+ return "interactions_not_materialized_yet"
997
+ if steps_total > 0 and llm_step_count == 0 and gw_req_count == 0:
998
+ return "abandoned_before_llm"
999
+ # When LLM_STEPs ran but neither gateway_requests nor generations
1000
+ # wrote, the STDM exporter dropped both. Distinct from
1001
+ # planner_ran_no_gateway_logs (which still has generation rows).
1002
+ if (llm_step_count > 0 and steps_with_generation_id == 0
1003
+ and gw_req_count == 0):
1004
+ return "gateway_requests_dropped_by_stdm"
1005
+ if (llm_step_count > 0 and steps_with_generation_id > 0
1006
+ and gw_req_count == 0):
1007
+ return "planner_ran_no_gateway_logs"
1008
+ return "complete"
1009
+
1010
+
1011
+ def _write_manifest(ctx: dict) -> None:
1012
+ """Emit dc._session_manifest.json next to the 24 artifacts."""
1013
+ finished = datetime.now(timezone.utc)
1014
+ elapsed_ms = int((finished - ctx["started_at"]).total_seconds() * 1000)
1015
+ manifest = {
1016
+ "_doc": "Per-query summary of this DC fetch run.",
1017
+ "session_id": ctx["session_id"],
1018
+ "org_alias": ctx["org_alias"],
1019
+ "instance_url": ctx["instance_url"],
1020
+ # Identity resolved in wave 1a, before any on-disk write. Stamped
1021
+ # here so downstream readers can recover (org, agent, version)
1022
+ # without re-parsing the tree.
1023
+ "org_id_15": ctx.get("org_id_15"),
1024
+ "agent_api_name": ctx.get("agent_api_name"),
1025
+ "agent_version": ctx.get("agent_version"),
1026
+ "session_shape": ctx.get("session_shape", "unknown"),
1027
+ "started_at_utc": ctx["started_at"].isoformat().replace("+00:00", "Z"),
1028
+ "finished_at_utc": finished.isoformat().replace("+00:00", "Z"),
1029
+ "elapsed_ms": elapsed_ms,
1030
+ "queries": ctx["queries"],
1031
+ "harvested_ids": ctx.get("harvested_ids", {}),
1032
+ }
1033
+ target = paths.session_dir(
1034
+ ctx["org_id_15"],
1035
+ ctx["agent_api_name"],
1036
+ ctx["agent_version"],
1037
+ ctx["session_id"],
1038
+ ) / "dc._session_manifest.json"
1039
+ target.parent.mkdir(parents=True, exist_ok=True)
1040
+ target.write_text(json.dumps(manifest, indent=2, default=str) + "\n")
1041
+ _log(f"\nmanifest: {target} ({elapsed_ms}ms total)")
1042
+
1043
+
1044
+ if __name__ == "__main__":
1045
+ sys.exit(main())