@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,845 @@
1
+ #!/usr/bin/env python3
2
+ """Idempotent wave parser — builds `$WORK_DIR/declared_action_tree.json`.
3
+
4
+ Replaces old agent Phase 4 (wave-2 init) + Phase 5/6 re-parse + MAX_WAVE cap.
5
+ One script, one code path. The "init vs re-parse" distinction is keyed on the
6
+ presence of `declared_action_tree.json` at call time.
7
+
8
+ Flow:
9
+ 1. Load bundle JSON, bot_definition JSON, existing tree (if any).
10
+ 2. Walk every $WORK_DIR/sf_meta/*/unpackaged/ dir:
11
+ - flows/*.flow: parse actionCalls → APEX/PROMPT_TEMPLATE/STANDARD_ACTION/
12
+ UNKNOWN children; subflows → FLOW children. Record flow_children[flow_name].
13
+ Mark ("FLOW", flow_name) visited.
14
+ - classes/*.cls-meta.xml: mark ("APEX", name) visited.
15
+ - genAiPromptTemplates/*.genAiPromptTemplate: mark ("PROMPT_TEMPLATE", name).
16
+ 3. Build tree if missing (Phase-4 init):
17
+ - agent metadata from bot_definition + bundle
18
+ - root BOT_DEFINITION node with children = topics[] only
19
+ (plannerActions[] retained in the bundle shape for back-compat but
20
+ always empty — a planner never has direct functions, 2026-05-05)
21
+ - each topic → TOPIC node with GEN_AI_FUNCTION children (from inline actions)
22
+ - each GEN_AI_FUNCTION → unwraps_to + leaf child (Flow/Apex/Prompt/Std/Unk)
23
+ 4. Inflate every FLOW leaf recursively (depth ≤ 6) with flow_children data.
24
+ 5. Recompute `_pending_fetches` (names not yet in visited set).
25
+ 6. Walk tree for `node_count`, `depth`, `_kind_counts`.
26
+ 7. Atomic write.
27
+
28
+ With --finalize-cap: after loading tree, move all remaining _pending_fetches
29
+ items to _unresolved[] with reason "max-wave-depth exceeded".
30
+
31
+ Usage:
32
+ python3 parse_wave.py # init or re-parse
33
+ python3 parse_wave.py --finalize-cap # drain _pending_fetches → _unresolved
34
+
35
+ Inputs (env):
36
+ WORK_DIR, AGENT_API_NAME, AGENT_VERSION, BOT_ID, BOT_MASTER_LABEL,
37
+ VERSION_AUTO_PICKED
38
+
39
+ Outputs:
40
+ $WORK_DIR/declared_action_tree.json (atomic write)
41
+ stderr: log lines (node counts, pending counts, etc.)
42
+ exit 0 on success, 1 on write failure
43
+ """
44
+ import json
45
+ import os
46
+ import pathlib
47
+ import sys
48
+ import xml.etree.ElementTree as ET
49
+ from collections import Counter
50
+
51
+
52
+ NS = {"sf": "http://soap.sforce.com/2006/04/metadata"}
53
+
54
+
55
+ def _t(el, p):
56
+ if el is None:
57
+ return None
58
+ x = el.find(p, NS)
59
+ return x.text if x is not None else None
60
+
61
+
62
+ def classify_bundle_action(action: dict) -> dict:
63
+ """Bundle-action classifier — duplicated in plan_wave.py per 'no
64
+ intra-skill imports' convention. Matches old agent's Phase 3/4 logic.
65
+
66
+ Returns (unwraps_dict, leaf_dict) as in the old agent Phase 4's
67
+ action_node(). leaf_dict is the child appended under the GEN_AI_FUNCTION
68
+ node; unwraps_dict is the compact form stored on the function itself.
69
+ """
70
+ tgt = action.get("invocationTarget")
71
+ ttype = (action.get("invocationTargetType") or "").lower()
72
+ if not tgt:
73
+ return None, None
74
+ if ttype == "flow":
75
+ return ({"kind": "FLOW", "api_name": tgt},
76
+ {"kind": "FLOW", "api_name": tgt, "children": []})
77
+ if ttype == "apex":
78
+ return ({"kind": "APEX", "api_name": tgt},
79
+ {"kind": "APEX", "api_name": tgt})
80
+ if ttype == "generatepromptresponse" or ttype.startswith("prompt") or ttype.startswith("genai"):
81
+ return ({"kind": "PROMPT_TEMPLATE", "api_name": tgt},
82
+ {"kind": "PROMPT_TEMPLATE", "api_name": tgt})
83
+ # Canonical field name is `invocation_type` (schema 3.1). Prior
84
+ # versions wrote `raw_invocation_type` on bundle-sourced nodes and
85
+ # `raw_action_type` on flow-actionCall-sourced nodes — downstream
86
+ # readers (render_architecture._display_name, summarize_tree) fall
87
+ # back to both legacy keys for one release.
88
+ if ttype == "standardinvocableaction":
89
+ return ({"kind": "STANDARD_ACTION", "api_name": tgt, "invocation_type": ttype},
90
+ {"kind": "STANDARD_ACTION", "api_name": tgt, "invocation_type": ttype})
91
+ return ({"kind": "UNKNOWN", "api_name": tgt, "invocation_type": ttype},
92
+ {"kind": "UNKNOWN", "api_name": tgt, "invocation_type": ttype})
93
+
94
+
95
+ def classify_action_call(at: str, an: str, element_name: str) -> dict:
96
+ """Flow actionCall classifier (old Phase 4 logic).
97
+
98
+ Canonical field name is `invocation_type` (schema 3.1). Legacy readers
99
+ still fall back to `raw_action_type` for one release.
100
+ """
101
+ at = at or ""
102
+ if at == "apex" and an:
103
+ return {"kind": "APEX", "element_name": element_name, "api_name": an}
104
+ if at == "generatePromptResponse" and an:
105
+ return {"kind": "PROMPT_TEMPLATE", "element_name": element_name, "api_name": an}
106
+ if at:
107
+ # Any other non-empty actionType → STANDARD_ACTION; preserve raw.
108
+ return {"kind": "STANDARD_ACTION", "element_name": element_name,
109
+ "api_name": an or at, "invocation_type": at}
110
+ return {"kind": "UNKNOWN", "element_name": element_name,
111
+ "api_name": an or "?", "invocation_type": at or ""}
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # pure BFS step helper. Visited sets are tuple-keyed on
116
+ # (kind, canonical_name). Pending is a dict-by-kind so cross-kind collisions
117
+ # (Flow Foo vs Apex Foo) stay distinct.
118
+ #
119
+ # this is the single source of truth for wave-level BFS. Both
120
+ # `harvest_waves` and `main()` route through here so the tuple-keyed
121
+ # semantics are active on the production path — not just under unit tests.
122
+ #
123
+ # kind tokens match the runtime tree's `kind` field
124
+ # (FLOW/APEX/PROMPT_TEMPLATE/STANDARD_ACTION). The persisted
125
+ # `_pending_fetches` dict uses the SAME tokens so internal + on-disk
126
+ # conventions don't diverge.
127
+ #
128
+ # promoted from `_BFS_KINDS` → `BFS_KINDS` so
129
+ # cross-module callers (main.py's in-process orchestrator) import a stable
130
+ # public symbol instead of reaching across a leading-underscore boundary.
131
+ # Same pattern as rest_client.redact_text's promotion. The underscore
132
+ # alias below is retained for backwards compatibility and will be removed
133
+ # in the next minor version; new code MUST use `BFS_KINDS`.
134
+ # unified node-truncation annotation. When a node is not fully
135
+ # expanded (either because of a cycle back to an ancestor, or because the
136
+ # MAX_BFS_DEPTH cap tripped), we annotate it with a `_truncated` sub-
137
+ # object of shape `{"reason": <str>, "target": <str>}`. `reason` is one
138
+ # of the values below; `target` is a `"KIND:name"` path pointing at the
139
+ # first encounter (cycle) or the unreached leaf (depth-cap). Downstream
140
+ # consumers check `_truncated["reason"]` once; no per-reason pattern
141
+ # matching.
142
+ #
143
+ # Backcompat: `_cycle_back_to` is emitted alongside `_truncated` for any
144
+ # consumer that hasn't migrated (render_architecture, summarize_tree,
145
+ # third-party tooling). Will be removed in the next minor version.
146
+ TRUNCATION_CYCLE = "cycle"
147
+ TRUNCATION_MAX_DEPTH = "max-depth"
148
+ TRUNCATION_REASONS = frozenset({TRUNCATION_CYCLE, TRUNCATION_MAX_DEPTH})
149
+
150
+
151
+ BFS_KINDS = ("FLOW", "APEX", "PROMPT_TEMPLATE", "STANDARD_ACTION")
152
+
153
+ # STANDARD_ACTION is a Salesforce-owned builtin (e.g.
154
+ # `streamKnowledgeSearch`, `createRecord`, `sendEmail`). These are never
155
+ # fetched — they're declared-only leaves, with the action name carrying
156
+ # all the identity. Keeping STANDARD_ACTION in BFS_KINDS lets the tree's
157
+ # _kind_counts tally include them and visited-bookkeeping dedup against
158
+ # them, but they must never accumulate into `_pending_fetches`. A name
159
+ # like `streamKnowledgeSearch` landing in _pending_fetches.STANDARD_ACTION
160
+ # is pollution — the pipeline isn't "missing" anything; the action is
161
+ # simply declared and never materialized.
162
+ #
163
+ # Callers: when collecting refs into `new_refs`, gate on FETCHABLE_KINDS
164
+ # rather than BFS_KINDS to avoid polluting pending buckets with leaves.
165
+ FETCHABLE_KINDS = ("FLOW", "APEX", "PROMPT_TEMPLATE")
166
+
167
+ # deprecated alias. Retained so existing tests and any lingering
168
+ # `from parse_wave import _BFS_KINDS` imports keep working. New code MUST
169
+ # use `BFS_KINDS` (public). Planned removal in the next minor version.
170
+ _BFS_KINDS = BFS_KINDS
171
+
172
+
173
+ def bfs_step(
174
+ pending_by_kind: dict[str, set[str]],
175
+ visited_by_kind: dict[str, set[str]],
176
+ new_refs_by_kind: dict[str, set[str]],
177
+ ) -> tuple[dict[str, set[str]], list[tuple[str, str]]]:
178
+ """Advance one BFS wave. Returns (new_pending_by_kind, cycles).
179
+
180
+ `cycles` is a list of (kind, name) tuples for refs that were already
181
+ visited on this wave — callers use these to annotate `_cycle_back_to`.
182
+
183
+ Self-cycles (a ref pointing at something already visited) are filtered
184
+ out of pending. Cross-type tuples (Flow Foo + Apex Foo) are distinct —
185
+ both land in their respective pending buckets.
186
+
187
+ unknown kinds RAISE — this is an internal API; a typo in a caller
188
+ is a programming error, not a recoverable runtime condition. Silent drop
189
+ produced false confidence (the dropped ref never got fetched and no log
190
+ ever mentioned it). Callers must filter to `BFS_KINDS` upstream.
191
+ """
192
+ new_pending: dict[str, set[str]] = {k: set() for k in BFS_KINDS}
193
+ cycles: list[tuple[str, str]] = []
194
+ for kind, refs in new_refs_by_kind.items():
195
+ if kind not in BFS_KINDS:
196
+ raise ValueError(
197
+ f"unknown BFS kind {kind!r}; must be one of {sorted(BFS_KINDS)}"
198
+ )
199
+ visited = visited_by_kind.get(kind, set())
200
+ for name in refs:
201
+ if name in visited:
202
+ cycles.append((kind, name))
203
+ continue
204
+ # Not yet visited AND not already pending on a prior wave:
205
+ # OR-in unconditionally — `pending |= (new - visited)` semantics.
206
+ new_pending[kind].add(name)
207
+ # Merge with the caller's existing pending. Caller passes the authoritative
208
+ # pending buckets; we return the delta merged in so they can just replace.
209
+ merged: dict[str, set[str]] = {k: set(pending_by_kind.get(k, set())) for k in BFS_KINDS}
210
+ for kind, names in new_pending.items():
211
+ merged[kind] |= names
212
+ return merged, cycles
213
+
214
+
215
+ def empty_kind_sets() -> dict[str, set[str]]:
216
+ """Build a fresh {kind: set()} dict covering every BFS_KINDS entry.
217
+
218
+ callers use this to seed per-kind visited / pending
219
+ buckets. Keeping it centralized means new kinds added to BFS_KINDS
220
+ automatically flow through harvest_waves, bfs_step, and main().
221
+
222
+ promoted from `_empty_kind_sets` → public so
223
+ cross-module callers (main.py) can import a stable name instead of
224
+ reaching into a private symbol. The underscore alias below is kept
225
+ for backwards compatibility; it will be removed in the next minor
226
+ version.
227
+ """
228
+ return {k: set() for k in BFS_KINDS}
229
+
230
+
231
+ # deprecated alias. Retained for backwards compatibility with any
232
+ # lingering `from parse_wave import _empty_kind_sets` imports; new code
233
+ # MUST use `empty_kind_sets` (public). Planned removal in the next minor
234
+ # version.
235
+ _empty_kind_sets = empty_kind_sets
236
+
237
+
238
+ def harvest_waves(
239
+ work_dir: pathlib.Path,
240
+ ) -> tuple[dict[str, list[dict]], dict[str, set[str]], dict[str, set[str]], list[tuple[str, str]]]:
241
+ """Walk sf_meta/*/unpackaged/ dirs.
242
+
243
+ Returns (flow_children, visited_by_kind, pending_by_kind, cycles).
244
+
245
+ every ref collected from Flow XML (actionCalls + subflows)
246
+ is routed through `bfs_step` per-flow. The tuple-keyed visited set is
247
+ the authoritative record; flat per-name sets have been removed.
248
+
249
+ the two dicts are keyed by `BFS_KINDS` tokens
250
+ (FLOW/APEX/PROMPT_TEMPLATE/STANDARD_ACTION) — matching the runtime
251
+ tree's `kind` field and the on-disk `_pending_fetches` layout.
252
+ """
253
+ flow_children: dict[str, list[dict]] = {}
254
+ visited_by_kind: dict[str, set[str]] = empty_kind_sets()
255
+ pending_by_kind: dict[str, set[str]] = empty_kind_sets()
256
+ cycles: list[tuple[str, str]] = []
257
+
258
+ sf_meta = work_dir / "sf_meta"
259
+ if not sf_meta.exists():
260
+ return flow_children, visited_by_kind, pending_by_kind, cycles
261
+
262
+ all_wave_dirs = sorted(
263
+ p / "unpackaged" for p in sf_meta.iterdir()
264
+ if p.is_dir() and (p / "unpackaged").exists()
265
+ )
266
+
267
+ # Pass 1: flows (+ route per-flow refs through bfs_step).
268
+ for wave_dir in all_wave_dirs:
269
+ flows_dir = wave_dir / "flows"
270
+ if not flows_dir.exists():
271
+ continue
272
+ for f in sorted(flows_dir.glob("*.flow")):
273
+ flow_name = f.stem
274
+ if flow_name in flow_children:
275
+ continue # already harvested this wave dir (earlier iter)
276
+ visited_by_kind["FLOW"].add(flow_name)
277
+ try:
278
+ root = ET.parse(f).getroot()
279
+ except ET.ParseError:
280
+ flow_children[flow_name] = []
281
+ continue
282
+ children: list[dict] = []
283
+ # Collect per-flow refs into kind-bucketed sets for one bfs_step.
284
+ new_refs: dict[str, set[str]] = empty_kind_sets()
285
+ for ac in root.findall("sf:actionCalls", NS):
286
+ n = _t(ac, "sf:name")
287
+ at = _t(ac, "sf:actionType") or ""
288
+ an = _t(ac, "sf:actionName")
289
+ item = classify_action_call(at, an, n)
290
+ children.append(item)
291
+ k = item["kind"]
292
+ api = item["api_name"]
293
+ # Only route FETCHABLE kinds into `new_refs` — STANDARD_ACTION
294
+ # items stay as leaf children with no further fetching implied
295
+ # . UNKNOWN items are similarly skipped.
296
+ if k in FETCHABLE_KINDS and api:
297
+ new_refs[k].add(api)
298
+ for sub in root.findall("sf:subflows", NS):
299
+ n = _t(sub, "sf:name")
300
+ fn = _t(sub, "sf:flowName")
301
+ if fn:
302
+ children.append({"kind": "FLOW", "element_name": n, "api_name": fn})
303
+ new_refs["FLOW"].add(fn)
304
+ flow_children[flow_name] = children
305
+ # bfs_step is the canonical merge point.
306
+ pending_by_kind, step_cycles = bfs_step(
307
+ pending_by_kind, visited_by_kind, new_refs
308
+ )
309
+ cycles.extend(step_cycles)
310
+
311
+ # Pass 2: APEX classes (mark visited).
312
+ for wave_dir in all_wave_dirs:
313
+ apex_dir = wave_dir / "classes"
314
+ if apex_dir.exists():
315
+ for f in sorted(apex_dir.glob("*.cls-meta.xml")):
316
+ visited_by_kind["APEX"].add(f.name.replace(".cls-meta.xml", ""))
317
+
318
+ # Pass 3: Prompt templates (mark visited).
319
+ for wave_dir in all_wave_dirs:
320
+ prompt_dir = wave_dir / "genAiPromptTemplates"
321
+ if prompt_dir.exists():
322
+ for f in sorted(prompt_dir.glob("*.genAiPromptTemplate")):
323
+ visited_by_kind["PROMPT_TEMPLATE"].add(f.stem)
324
+
325
+ # Passes 2/3 may have added visited entries AFTER pass-1 pending routing;
326
+ # prune anything that's now visited. This preserves the invariant that
327
+ # `pending ∩ visited = ∅` without needing a second bfs_step.
328
+ for kind in BFS_KINDS:
329
+ pending_by_kind[kind] -= visited_by_kind[kind]
330
+
331
+ return flow_children, visited_by_kind, pending_by_kind, cycles
332
+
333
+
334
+ def init_tree(work_dir: pathlib.Path, bundle: dict) -> dict:
335
+ bd_rec = {}
336
+ bd_file = work_dir / "_bot_definition.json"
337
+ if bd_file.exists():
338
+ try:
339
+ recs = (json.loads(bd_file.read_text()).get("result") or {}).get("records") or []
340
+ if recs:
341
+ bd_rec = recs[0]
342
+ except (OSError, json.JSONDecodeError):
343
+ pass
344
+
345
+ tree = {
346
+ # 3.0 bumps the _pending_fetches key convention from
347
+ # Metadata-API tokens (Flow/ApexClass/GenAiPromptTemplate) to the
348
+ # runtime-tree `kind` tokens (FLOW/APEX/PROMPT_TEMPLATE/
349
+ # STANDARD_ACTION). `cache_check.py`'s schema-version gate busts any
350
+ # pre-3.0 cache on first run.
351
+ #
352
+ # 3.1 (2026-05-05) canonicalizes `invocation_type` on STANDARD_ACTION
353
+ # nodes (formerly split across `raw_invocation_type` on bundle-
354
+ # sourced nodes and `raw_action_type` on flow-actionCall-sourced
355
+ # nodes). Readers fall back to the two legacy keys for one release.
356
+ "_schema_version": "3.1",
357
+ "agent": {
358
+ "api_name": os.environ["AGENT_API_NAME"],
359
+ "version": os.environ["AGENT_VERSION"],
360
+ "bot_id": os.environ.get("BOT_ID", ""),
361
+ "master_label": bd_rec.get("MasterLabel") or os.environ.get("BOT_MASTER_LABEL", ""),
362
+ "description": bd_rec.get("Description"),
363
+ "agent_type": bd_rec.get("AgentType"),
364
+ "type": bd_rec.get("Type"),
365
+ "agent_template": bd_rec.get("AgentTemplate"),
366
+ "bot_source": bd_rec.get("BotSource"),
367
+ "generation": bundle.get("generation", "unknown"),
368
+ "planner_name": bundle.get("plannerName"),
369
+ "planner_type": bundle.get("plannerType"),
370
+ "_version_auto_picked": (os.environ.get("VERSION_AUTO_PICKED", "") == "true"),
371
+ },
372
+ "root": {
373
+ "kind": "BOT_DEFINITION",
374
+ "api_name": os.environ["AGENT_API_NAME"],
375
+ "children": [],
376
+ },
377
+ "node_count": 0,
378
+ "depth": 0,
379
+ # `_partial` + `_partial_reason` propagate from parse_wave's
380
+ # depth-cap detection through to emit_result's PARTIAL_OK status.
381
+ # `_partial_reason` values: "max-depth-cap" | "pending-refs" | null.
382
+ "_partial": True,
383
+ "_partial_reason": None,
384
+ "_pending_fetches": {k: [] for k in BFS_KINDS},
385
+ "_unresolved": [],
386
+ "_visited": [],
387
+ }
388
+ return tree
389
+
390
+
391
+ def build_root_children(
392
+ bundle: dict,
393
+ visited_by_kind: dict[str, set[str]],
394
+ aux_visited: set[tuple[str, str]],
395
+ ) -> tuple[list[dict], dict[str, set[str]]]:
396
+ """Build the root's topic / plannerAction children.
397
+
398
+ bundle-scoped FLOW/APEX/PROMPT_TEMPLATE refs are collected
399
+ into a kind-keyed `new_refs` dict and returned to the caller, which runs
400
+ them through `bfs_step` for uniform pending-merge + cycle tracking.
401
+ This used to directly mutate three flat sets — split out so the tuple-
402
+ keyed semantics apply to bundle-derived refs too, not just wave-derived
403
+ ones.
404
+
405
+ `aux_visited` tracks non-BFS node identity (GEN_AI_FUNCTION / TOPIC) so
406
+ the persisted `_visited` list still represents every node the walker
407
+ has laid hands on. It is NOT consulted by bfs_step.
408
+ """
409
+ children: list[dict] = []
410
+ new_refs: dict[str, set[str]] = empty_kind_sets()
411
+
412
+ def action_node(action: dict) -> dict:
413
+ unwraps, leaf = classify_bundle_action(action)
414
+ aux_visited.add(("GEN_AI_FUNCTION", action["name"]))
415
+ node = {
416
+ "kind": "GEN_AI_FUNCTION",
417
+ "api_name": action["name"],
418
+ "unwraps_to": unwraps,
419
+ "children": [leaf] if leaf else [],
420
+ }
421
+ if unwraps:
422
+ tgt = unwraps.get("api_name")
423
+ kind = unwraps.get("kind")
424
+ # STANDARD_ACTION is declared-only, never
425
+ # fetched; routing it into `new_refs` would pollute
426
+ # `_pending_fetches.STANDARD_ACTION`. Gate on FETCHABLE_KINDS.
427
+ if kind in FETCHABLE_KINDS and tgt and tgt not in visited_by_kind[kind]:
428
+ new_refs[kind].add(tgt)
429
+ return node
430
+
431
+ for topic in bundle.get("topics", []) or []:
432
+ aux_visited.add(("TOPIC", topic["name"]))
433
+ children.append({
434
+ "kind": "TOPIC",
435
+ "api_name": topic["name"],
436
+ "children": [action_node(a) for a in topic.get("actions", []) or []],
437
+ })
438
+
439
+ # bundle["plannerActions"] is always [] now (a planner never has
440
+ # direct functions — 2026-05-05). The key stays in the bundle dict
441
+ # for back-compat with consumers that still call `.get("plannerActions")`.
442
+
443
+ return children, new_refs
444
+
445
+
446
+ # ---------------------------------------------------------------------------
447
+ # cycle-safe inflate with (kind, canonical_name) tuple-keyed tracking.
448
+ #
449
+ # Visited tracking uses tuple keys so a Flow `Foo` and an Apex `Foo` are
450
+ # distinct nodes — a flat name-only set would silently drop the second one.
451
+ # Flow→subflow cycles (direct or cross-type A→B→A) are annotated with
452
+ # `_cycle_back_to` on the repeat node and recursion terminates immediately.
453
+ #
454
+ # , revised 2026-05-03: `MAX_BFS_DEPTH` is a DEFENSIVE termination
455
+ # guard, not a functional constraint on chain depth. Must stay in sync
456
+ # with `scripts/config.py::MAX_BFS_DEPTH` — duplicated (rather than
457
+ # imported) because this module runs as a standalone subprocess under
458
+ # `python3 parse_wave.py` (see `main()`) and the module has a long-standing
459
+ # "no intra-skill imports" convention (search `no intra-skill imports'
460
+ # convention`).
461
+ #
462
+ # Real cycle detection is per-branch via `visited_in_path`: a flow that
463
+ # appears on sibling branches is NOT a cycle (e.g. `handleFlowFault`,
464
+ # invoked from many parents), but the same flow recurring along its own
465
+ # ancestor chain IS. Textbook DFS cycle detection.
466
+ #
467
+ # Before the revision this was `5` and the cap itself tripped on shared
468
+ # utility flows (`handleFlowFault`, logging subflows) — those landed in
469
+ # `_pending_fetches["FLOW"]` and the user saw `PARTIAL_REASON=max-depth-cap`
470
+ # on trees that were in fact fully knowable. Bumping to 20 keeps a safety
471
+ # net for pathological graphs while letting per-branch cycle detection do
472
+ # the real work. On every real bot tree observed to date, the per-branch
473
+ # check terminates well below depth 20.
474
+ #
475
+ # When this cap *does* trip (truly exceptional), the unreached FLOW lands
476
+ # in `pending_out["FLOW"]` and the leaf is annotated `_truncated =
477
+ # {reason: "max-depth", target: "FLOW:<name>"}`, exactly as before.
478
+ MAX_BFS_DEPTH = 20
479
+
480
+ # Retained alias so the `MAX_INFLATE_DEPTH` symbol (referenced in older
481
+ # agent notes / docs) still resolves. Tracks `MAX_BFS_DEPTH` exactly; the
482
+ # 2026-05-03 revision reinterpreted this as a defensive guard rather than
483
+ # a functional cap, but the alias semantics are unchanged.
484
+ MAX_INFLATE_DEPTH = MAX_BFS_DEPTH
485
+ _FLOW_CYCLE_KINDS = frozenset({"FLOW"}) # the only kind that recurses here
486
+
487
+
488
+ def _cycle_key(node: dict) -> tuple[str, str]:
489
+ """Tuple key for visited / cycle detection. Flows use canonical api_name.
490
+
491
+ keying on `(kind, api_name)` — NOT just name — so cross-kind
492
+ collisions (Flow Foo + Apex Foo) stay distinct.
493
+ """
494
+ return (node.get("kind") or "", node.get("api_name") or "")
495
+
496
+
497
+ def inflate_flow_leaf(
498
+ leaf: dict,
499
+ flow_children: dict,
500
+ depth: int = 0,
501
+ visited_in_path: frozenset[tuple[str, str]] | None = None,
502
+ pending_out: dict[str, set[str]] | None = None,
503
+ ) -> None:
504
+ """Recursively expand a FLOW leaf's subflow tree.
505
+
506
+ `visited_in_path` is threaded through the recursion as an
507
+ IMMUTABLE frozenset — siblings at the same depth must NOT observe
508
+ each other's descent. Mutating a shared set would cause the second
509
+ sibling to be pruned as a false cycle.
510
+
511
+ `pending_out` is an OPTIONAL mutable accumulator keyed by
512
+ `BFS_KINDS` tokens. When the depth cap trips (`depth >= MAX_BFS_DEPTH`)
513
+ and the current leaf still has subflow children we'd normally expand,
514
+ those unreached targets are added to `pending_out[kind]` so a caller
515
+ can surface them as `_pending_fetches` on the tree with
516
+ `_partial=True` + `_partial_reason="max-depth-cap"`. When `pending_out`
517
+ is None (legacy callers), the cap behaves as before — silently skip.
518
+ """
519
+ # depth-cap check. We use `>=` (not `>`) so the cap matches
520
+ # `MAX_BFS_DEPTH` exactly — at depth 5 we do NOT expand the current
521
+ # leaf, and the leaf's own (kind, api_name) is recorded in
522
+ # `pending_out` as "unreached". Rationale: this node was supposed
523
+ # to be fully explored but we hit the cap on the way in. Naming
524
+ # the leaf itself (not its kids) is what downstream callers need
525
+ # to surface as `_pending_fetches["FLOW"] = [<unreached>]`.
526
+ if depth >= MAX_BFS_DEPTH:
527
+ if leaf.get("kind") == "FLOW":
528
+ flow_name = leaf.get("api_name")
529
+ if flow_name:
530
+ if pending_out is not None:
531
+ pending_out.setdefault("FLOW", set()).add(flow_name)
532
+ # annotate the truncated leaf with the unified
533
+ # `_truncated` sub-object so rendered tree views can
534
+ # mark depth-capped nodes alongside cycle nodes using
535
+ # one predicate. The target points at the leaf itself
536
+ # (it was supposed to be fully explored but the cap
537
+ # tripped on the way in).
538
+ leaf["_truncated"] = {
539
+ "reason": TRUNCATION_MAX_DEPTH,
540
+ "target": f"FLOW:{flow_name}",
541
+ }
542
+ return
543
+ if leaf.get("kind") != "FLOW":
544
+ return
545
+ flow_name = leaf.get("api_name")
546
+ if not flow_name:
547
+ return
548
+ kids = flow_children.get(flow_name)
549
+ if not kids:
550
+ return # no data this wave; preserve any existing children
551
+
552
+ # Extend the path-visited set with this node. Frozenset keeps sibling
553
+ # branches independent — mutation would cross-contaminate.
554
+ leaf_key = _cycle_key(leaf)
555
+ path_set = (visited_in_path or frozenset()) | {leaf_key}
556
+
557
+ new_children: list[dict] = []
558
+ for k in kids:
559
+ item = {"kind": k["kind"], "element_name": k.get("element_name"), "api_name": k["api_name"]}
560
+ # Carry the STANDARD_ACTION / UNKNOWN invocation-type qualifier
561
+ # through to the expanded child. Canonical key is `invocation_type`
562
+ # (schema 3.1); legacy `raw_action_type` kept for one release so
563
+ # caches built by an older parse_wave still render cleanly.
564
+ if "invocation_type" in k:
565
+ item["invocation_type"] = k["invocation_type"]
566
+ if "raw_action_type" in k:
567
+ item["raw_action_type"] = k["raw_action_type"]
568
+ if k["kind"] == "FLOW":
569
+ item["children"] = []
570
+ child_key = _cycle_key(item)
571
+ if child_key in path_set:
572
+ # cycle detected — annotate and do NOT recurse.
573
+ # Path format: "<KIND>:<name>" of the first encounter.
574
+ # unified `_truncated` sub-object + legacy
575
+ # `_cycle_back_to` for backcompat.
576
+ target_path = f"{child_key[0]}:{child_key[1]}"
577
+ item["_truncated"] = {
578
+ "reason": TRUNCATION_CYCLE,
579
+ "target": target_path,
580
+ }
581
+ item["_cycle_back_to"] = target_path # deprecated alias
582
+ new_children.append(item)
583
+ continue
584
+ new_children.append(item)
585
+ inflate_flow_leaf(item, flow_children, depth + 1, path_set, pending_out)
586
+ else:
587
+ new_children.append(item)
588
+ leaf["children"] = new_children
589
+
590
+
591
+ def walk_and_inflate(
592
+ node: dict,
593
+ flow_children: dict,
594
+ depth: int = 0,
595
+ pending_out: dict[str, set[str]] | None = None,
596
+ ) -> None:
597
+ """Walk the tree and inflate every FLOW leaf we encounter.
598
+
599
+ Each inflate call starts with an empty path-visited set — tree-walk
600
+ descent is not itself a Flow recursion, so ancestor GEN_AI_FUNCTION
601
+ or TOPIC nodes don't count toward cycle detection.
602
+
603
+ `pending_out` (optional) is threaded into every `inflate_flow_leaf`
604
+ call so depth-cap truncations accumulate into a single shared dict,
605
+ which `main()` merges into `tree["_pending_fetches"]` + flips
606
+ `tree["_partial"]` with reason `max-depth-cap`.
607
+ """
608
+ if node.get("kind") == "GEN_AI_FUNCTION":
609
+ for child in node.get("children", []) or []:
610
+ if child.get("kind") == "FLOW":
611
+ inflate_flow_leaf(child, flow_children, depth, pending_out=pending_out)
612
+ elif node.get("kind") == "FLOW":
613
+ inflate_flow_leaf(node, flow_children, depth, pending_out=pending_out)
614
+ for c in node.get("children", []) or []:
615
+ walk_and_inflate(c, flow_children, depth + 1, pending_out)
616
+
617
+
618
+ def compute_stats(root: dict) -> tuple[int, int, dict]:
619
+ counts: Counter = Counter()
620
+
621
+ def walk(node: dict, d: int = 0) -> int:
622
+ k = node.get("kind")
623
+ if k:
624
+ counts[k] += 1
625
+ max_d = d
626
+ for c in node.get("children", []) or []:
627
+ max_d = max(max_d, walk(c, d + 1))
628
+ return max_d
629
+
630
+ depth = walk(root)
631
+ return sum(counts.values()), depth, dict(counts)
632
+
633
+
634
+ def atomic_write_json(path: pathlib.Path, obj: dict) -> None:
635
+ tmp = path.with_suffix(path.suffix + ".tmp")
636
+ tmp.write_text(json.dumps(obj, indent=2))
637
+ os.replace(tmp, path)
638
+
639
+
640
+ def finalize_cap(tree: dict) -> dict:
641
+ """Move remaining _pending_fetches items to _unresolved[].
642
+
643
+ pending and unresolved both use the same kind tokens now
644
+ (FLOW/APEX/PROMPT_TEMPLATE/STANDARD_ACTION) — matching the runtime
645
+ tree's `kind` field.
646
+
647
+ depth-cap truncation writes `_partial=True` +
648
+ `_partial_reason="max-depth-cap"` earlier in the pipeline. Finalize
649
+ preserves those signals when it drains pending → unresolved so the
650
+ downstream emit_result.py can still surface PARTIAL_OK with the
651
+ correct reason. If BOTH the depth cap AND finalize-cap fire on the
652
+ same run, depth-cap wins priority (set first, not overwritten).
653
+ """
654
+ pending = tree.get("_pending_fetches") or {}
655
+ unresolved = tree.setdefault("_unresolved", [])
656
+ drained_any = False
657
+ for kind, items in pending.items():
658
+ for n in items:
659
+ drained_any = True
660
+ unresolved.append({
661
+ "kind": kind,
662
+ "api_name": n,
663
+ "reason": "max-wave-depth exceeded",
664
+ })
665
+ tree["_pending_fetches"] = {k: [] for k in BFS_KINDS}
666
+
667
+ # if pending was drained but no `_partial_reason` is set yet,
668
+ # mark it as wave-depth exhaustion. DO NOT overwrite an existing
669
+ # `max-depth-cap` reason — the depth-cap trigger is strictly earlier
670
+ # and more specific.
671
+ if drained_any:
672
+ tree["_partial"] = True
673
+ if not tree.get("_partial_reason"):
674
+ tree["_partial_reason"] = "max-wave-depth"
675
+ return tree
676
+
677
+
678
+ def main() -> int:
679
+ finalize_cap_mode = "--finalize-cap" in sys.argv
680
+
681
+ try:
682
+ work_dir = pathlib.Path(os.environ["WORK_DIR"])
683
+ except KeyError as e:
684
+ sys.stderr.write(f"parse_wave.py: missing env {e}\n")
685
+ return 1
686
+
687
+ tree_path = work_dir / "declared_action_tree.json"
688
+
689
+ # --finalize-cap: trivial; just drain pending → unresolved on existing tree
690
+ if finalize_cap_mode:
691
+ if not tree_path.is_file():
692
+ sys.stderr.write("parse_wave.py: --finalize-cap with no tree file; noop\n")
693
+ return 0
694
+ try:
695
+ tree = json.loads(tree_path.read_text())
696
+ except (OSError, json.JSONDecodeError) as e:
697
+ sys.stderr.write(f"parse_wave.py: cannot read tree: {e}\n")
698
+ return 1
699
+ tree = finalize_cap(tree)
700
+ try:
701
+ atomic_write_json(tree_path, tree)
702
+ except OSError as e:
703
+ sys.stderr.write(f"parse_wave.py: write failed: {e}\n")
704
+ return 1
705
+ sys.stderr.write(
706
+ f"[parse_wave] finalize-cap: {len(tree.get('_unresolved', []))} nodes unresolved\n"
707
+ )
708
+ return 0
709
+
710
+ # Standard init-or-reparse path
711
+ bundle_path = work_dir / "_bundle_parsed.json"
712
+ if not bundle_path.is_file():
713
+ sys.stderr.write(f"parse_wave.py: missing {bundle_path}\n")
714
+ return 1
715
+ try:
716
+ bundle = json.loads(bundle_path.read_text())
717
+ except (OSError, json.JSONDecodeError) as e:
718
+ sys.stderr.write(f"parse_wave.py: bundle parse error: {e}\n")
719
+ return 1
720
+
721
+ # Harvest wave dirs. Returns kind-keyed visited + pending dicts plus
722
+ # the list of (kind, name) cycles detected during per-flow BFS merges.
723
+ flow_children, wave_visited_by_kind, pending_by_kind, _wave_cycles = harvest_waves(work_dir)
724
+
725
+ # Load-or-init tree
726
+ if tree_path.is_file():
727
+ try:
728
+ tree = json.loads(tree_path.read_text())
729
+ except (OSError, json.JSONDecodeError) as e:
730
+ sys.stderr.write(f"parse_wave.py: cannot re-parse; reinitializing ({e})\n")
731
+ tree = init_tree(work_dir, bundle)
732
+ else:
733
+ tree = init_tree(work_dir, bundle)
734
+
735
+ # Rehydrate tuple-keyed visited from the persisted _visited list and
736
+ # union with wave-derived visited. Non-BFS kinds (TOPIC/GEN_AI_FUNCTION)
737
+ # live in `aux_visited` — they're persisted in _visited but do NOT
738
+ # participate in BFS routing.
739
+ visited_by_kind: dict[str, set[str]] = empty_kind_sets()
740
+ for kind, names in wave_visited_by_kind.items():
741
+ visited_by_kind[kind] |= names
742
+ aux_visited: set[tuple[str, str]] = set()
743
+ for pair in (tree.get("_visited") or []):
744
+ if not pair or len(pair) != 2:
745
+ continue
746
+ k, n = pair[0], pair[1]
747
+ if k in BFS_KINDS:
748
+ visited_by_kind[k].add(n)
749
+ else:
750
+ aux_visited.add((k, n))
751
+
752
+ # If the tree was just initialized, populate root children (only once).
753
+ # bundle-derived refs now flow through bfs_step via
754
+ # build_root_children's new_refs return value — same code path as
755
+ # wave-derived refs.
756
+ is_fresh = not (tree.get("root", {}).get("children"))
757
+ if is_fresh:
758
+ children, bundle_new_refs = build_root_children(
759
+ bundle, visited_by_kind, aux_visited
760
+ )
761
+ tree["root"]["children"] = children
762
+ pending_by_kind, _bundle_cycles = bfs_step(
763
+ pending_by_kind, visited_by_kind, bundle_new_refs
764
+ )
765
+
766
+ # Inflate Flow leaves wherever possible.
767
+ # `depth_cap_pending` captures subflow refs we skipped because
768
+ # the depth cap tripped — these get merged into `_pending_fetches`
769
+ # below so downstream emit_result.py can surface `PARTIAL_OK`.
770
+ depth_cap_pending: dict[str, set[str]] = empty_kind_sets()
771
+ walk_and_inflate(tree["root"], flow_children, pending_out=depth_cap_pending)
772
+
773
+ # Merge depth-cap-suppressed refs back into pending_by_kind so they
774
+ # land in `_pending_fetches`. Preserve any existing (already-visited)
775
+ # exclusion semantics — refs already fetched this run MUST NOT be
776
+ # re-reported as pending.
777
+ for kind in BFS_KINDS:
778
+ pending_by_kind[kind] |= depth_cap_pending.get(kind, set())
779
+
780
+ depth_cap_tripped = any(depth_cap_pending[k] for k in BFS_KINDS)
781
+
782
+ # Recompute pending (exclude anything now visited). the
783
+ # persisted dict uses the same `BFS_KINDS` tokens as the runtime tree
784
+ # and the in-memory buckets — no more Metadata-API-style aliasing.
785
+ tree["_pending_fetches"] = {
786
+ k: sorted(pending_by_kind.get(k, set()) - visited_by_kind.get(k, set()))
787
+ for k in BFS_KINDS
788
+ }
789
+
790
+ # surface a `_partial` signal when:
791
+ # (a) depth-cap-pending refs exist (we suppressed deep subflows), OR
792
+ # (b) any `_pending_fetches` bucket is non-empty at write time.
793
+ # `_partial_reason` identifies which of the two tripped — `max-depth-cap`
794
+ # takes priority when depth_cap_pending is non-empty, since the cap is
795
+ # the reason pending refs weren't drained to `_unresolved` yet.
796
+ any_pending = any(tree["_pending_fetches"][k] for k in BFS_KINDS)
797
+ if depth_cap_tripped:
798
+ tree["_partial"] = True
799
+ tree["_partial_reason"] = "max-depth-cap"
800
+ elif any_pending:
801
+ # Still incomplete but NOT because of the depth cap. Leave any
802
+ # existing reason intact if prior run set it; default to a neutral
803
+ # marker so emit_result.py can still flip to PARTIAL_OK.
804
+ tree["_partial"] = True
805
+ if not tree.get("_partial_reason"):
806
+ tree["_partial_reason"] = "pending-refs"
807
+ else:
808
+ tree["_partial"] = False
809
+ tree["_partial_reason"] = None
810
+
811
+ # Recompute node_count + depth + kind_counts
812
+ node_count, depth, kind_counts = compute_stats(tree["root"])
813
+ tree["node_count"] = node_count
814
+ tree["depth"] = depth
815
+ tree["_kind_counts"] = kind_counts
816
+
817
+ # Persist visited as sorted list of [kind, name] pairs. Includes aux
818
+ # kinds (TOPIC/GEN_AI_FUNCTION) so the replay surface is complete.
819
+ all_visited: set[tuple[str, str]] = set(aux_visited)
820
+ for kind, names in visited_by_kind.items():
821
+ for n in names:
822
+ all_visited.add((kind, n))
823
+ tree["_visited"] = [list(v) for v in sorted(all_visited)]
824
+
825
+ try:
826
+ atomic_write_json(tree_path, tree)
827
+ except OSError as e:
828
+ sys.stderr.write(f"parse_wave.py: write failed: {e}\n")
829
+ return 1
830
+
831
+ # log line mirrors the persisted dict's kind tokens.
832
+ pending_total = sum(len(v) for v in tree["_pending_fetches"].values())
833
+ sys.stderr.write(
834
+ f"[parse_wave] parsed: {node_count} nodes, depth {depth}, counts={kind_counts}, "
835
+ f"pending={pending_total} "
836
+ f"(flow={len(tree['_pending_fetches']['FLOW'])} "
837
+ f"apex={len(tree['_pending_fetches']['APEX'])} "
838
+ f"prompt_template={len(tree['_pending_fetches']['PROMPT_TEMPLATE'])} "
839
+ f"standard_action={len(tree['_pending_fetches']['STANDARD_ACTION'])})\n"
840
+ )
841
+ return 0
842
+
843
+
844
+ if __name__ == "__main__":
845
+ sys.exit(main())