@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,1043 @@
1
+ """Render architecture.md from metadata_tree.json.
2
+
3
+ Phase 2 Batch 2.2: 8-section architecture document with up to 3 Mermaid
4
+ diagrams (2 core + 1 conditional dependency graph). Generation-aware:
5
+ classic/ReAct, classic/SequentialPlannerIntentClassifier, NGA/
6
+ ConcurrentMultiAgentOrchestration, search/BYOP all produce distinct
7
+ structural output on the same fixture schema.
8
+
9
+ Consumers
10
+ ---------
11
+ - `main.py` phase 10 — called with the finalized tree_path + a target
12
+ out_path under the agent data_dir.
13
+ - Tests — `scripts/tests/test_render_architecture.py` exercises every
14
+ per-generation branch and the cap/partial/cycle edge cases.
15
+
16
+ Template loader
17
+ ---------------
18
+ `load_mermaid(name, **params)` mirrors `soql_loader.load_soql`'s
19
+ substitution shape (single-pass `str.replace` on `{{KEY}}` tokens) but
20
+ deliberately does NOT run `fs_guard.validate_api_name` on the values.
21
+ Mermaid strings are not SQL, not a filesystem path, and not a shell
22
+ argument — there is no injection surface downstream. We only guard
23
+ against a *substituted* value itself containing `{{` / `}}`, which would
24
+ confuse readers of the rendered diff if it silently chained into a
25
+ second substitution pass; the loader logs a warning and proceeds.
26
+ Template filenames are validated — they flow into a `Path.is_file()`
27
+ check on disk, same traversal surface as `load_soql`.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import json
32
+ import logging
33
+ from pathlib import Path
34
+ from typing import Any, Dict, List, Optional, Tuple
35
+
36
+ from config import MERMAID_DIR, SKILL_ROOT, fs_guard # fs_guard re-exported by config.py from _shared/
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+ # P2.2-1: per-diagram-type node caps. Above cap -> summary placeholder.
41
+ DEFAULT_MAX_MERMAID_NODES: Dict[str, int] = {
42
+ "flowchart": 200,
43
+ "stateDiagram": 40,
44
+ "sequenceDiagram": 60,
45
+ "graph": 100,
46
+ }
47
+
48
+
49
+ def _display_name(node: Dict[str, Any]) -> str:
50
+ """Return the node's rendered label.
51
+
52
+ For STANDARD_ACTION nodes, append the invocation-type qualifier
53
+ (e.g. `streamKnowledgeSearch (standardinvocableaction)`) so the
54
+ rendered tree distinguishes a real Salesforce-owned builtin from
55
+ a flow-element STANDARD_ACTION whose action-name equals its
56
+ invocation-target. Canonical key is `invocation_type` (schema
57
+ 3.1); legacy `raw_invocation_type` / `raw_action_type` fall
58
+ back for caches built by an older parse_wave.
59
+ """
60
+ name = node.get("api_name") or node.get("element_name") or "?"
61
+ if node.get("kind") == "STANDARD_ACTION":
62
+ inv = (
63
+ node.get("invocation_type")
64
+ or node.get("raw_invocation_type")
65
+ or node.get("raw_action_type")
66
+ )
67
+ if inv:
68
+ return f"{name} ({inv})"
69
+ return name
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Template loader
74
+ # ---------------------------------------------------------------------------
75
+
76
+
77
+ def load_mermaid(name: str, **params: str) -> str:
78
+ """Read assets/mermaid/<name>.mmd and substitute {{PARAM}} values.
79
+
80
+ P2.2-1: template name is regex-validated BEFORE any filesystem access.
81
+ Traversal via `../../etc/...` is blocked by `fs_guard.validate_api_name`.
82
+ Param *values* are NOT validated — mermaid strings don't flow into
83
+ SOQL, REST, or filesystem paths and carry no injection surface.
84
+ We DO check for nested `{{`/`}}` in substituted values and log a
85
+ warning (the first-pass substitute call won't re-trigger, but a
86
+ stray `{{OTHER}}` inside a value makes the rendered markdown
87
+ confusing to diff). Raises `FileNotFoundError` with the template
88
+ name (no absolute-path leak) when the template file is missing.
89
+ """
90
+ try:
91
+ fs_guard.validate_api_name(name, label="mermaid_template_name")
92
+ except fs_guard.ValidationError as e:
93
+ # Template name is attacker-controlled in theory (a caller could
94
+ # source it from a config file). Don't leak the full SKILL_ROOT
95
+ # via the default FileNotFoundError message.
96
+ raise FileNotFoundError(
97
+ f"Mermaid template name rejected: {e.reason}"
98
+ ) from None
99
+
100
+ path = MERMAID_DIR / f"{name}.mmd"
101
+ if not path.is_file():
102
+ # Match soql_loader's SoqlTemplateNotFound hygiene — carry only
103
+ # the template *name*, not the absolute MERMAID_DIR path, so
104
+ # error text surfacing in logs doesn't disclose filesystem layout.
105
+ raise FileNotFoundError(f"Mermaid template not found: {name}")
106
+
107
+ template = path.read_text()
108
+ # the `%%` header-comment block in each template is kept
109
+ # verbatim in the rendered output. Mermaid treats `%%` lines as
110
+ # comments and ignores them at render time, and keeping them aids
111
+ # debuggability (rendered diff still carries the author's contract
112
+ # notes). Templates MUST document placeholder names as bare tokens
113
+ # (e.g. `NODES placeholder:`) rather than `{{NODES}}` — otherwise
114
+ # the single-pass `str.replace` below would substitute the comment's
115
+ # placeholder reference and corrupt the header.
116
+
117
+ for key, value in params.items():
118
+ if not isinstance(value, str):
119
+ # Fail loud rather than produce a literal 'None' or '[<Node>...]'
120
+ # in the rendered markdown.
121
+ raise TypeError(
122
+ f"Mermaid param {key!r} must be str, got {type(value).__name__}"
123
+ )
124
+ if "{{" in value or "}}" in value:
125
+ # Defensive log; single-pass `str.replace` still renders safely.
126
+ logger.warning(
127
+ "load_mermaid(%s): param %r contains nested placeholder tokens; "
128
+ "rendered output may be confusing",
129
+ name, key,
130
+ )
131
+ template = template.replace(f"{{{{{key}}}}}", value)
132
+ return template.strip()
133
+
134
+
135
+ # ---------------------------------------------------------------------------
136
+ # Public entry point
137
+ # ---------------------------------------------------------------------------
138
+
139
+
140
+ def render(
141
+ tree_path: Path,
142
+ out_path: Path,
143
+ *,
144
+ max_mermaid_nodes: Optional[Dict[str, int]] = None,
145
+ ) -> None:
146
+ """Read metadata_tree.json and write architecture.md to out_path.
147
+
148
+ The renderer walks the tree once to collect catalog data (topics,
149
+ actions, flows, apex, prompts, unresolved refs, cycles) then emits
150
+ 8 sections in document order. Generation-aware branches live at the
151
+ section-level helpers below.
152
+
153
+ Note: `_render_invocation_sequence` and `_render_planner_state`
154
+ remain defined and tested but are no longer part of the default
155
+ pipeline (heuristics distrusted by reviewers as of 2026-05).
156
+ Uncomment the relevant `parts.append` calls below to re-enable.
157
+ """
158
+ caps = dict(DEFAULT_MAX_MERMAID_NODES)
159
+ if max_mermaid_nodes:
160
+ caps.update(max_mermaid_nodes)
161
+
162
+ tree = json.loads(Path(tree_path).read_text())
163
+ agent = tree.get("agent") or {}
164
+ generation = (agent.get("generation") or "classic").lower()
165
+
166
+ walker = _TreeWalker(tree)
167
+ walker.walk()
168
+
169
+ parts: List[str] = []
170
+ parts.append(_render_header(tree, agent))
171
+ parts.append(_render_anatomy_summary(tree, walker))
172
+ # parts.append(_render_invocation_sequence(tree, agent, walker, caps))
173
+ # ^ Disabled 2026-05: heuristic over-simplified real orchestration
174
+ # paths. Function + template retained; re-enable by uncommenting.
175
+ parts.append(_render_action_tree(tree, walker, caps))
176
+ parts.append(_render_topic_anatomy(walker))
177
+ parts.append(_render_action_catalog(walker))
178
+ # parts.append(_render_planner_state(agent, generation, caps))
179
+ # ^ Disabled 2026-05: generation-specific state diagram added more
180
+ # noise than signal in review. Function + template retained; re-enable
181
+ # by uncommenting.
182
+ parts.append(_render_data_flow(tree, walker, caps))
183
+ parts.append(_render_artifact_catalogs(walker))
184
+ parts.append(_render_unresolved(tree, walker))
185
+
186
+ # Conditional dependency-graph section — emitted only if there are
187
+ # unresolved refs or cycles detected. Not counted as one of the 9.
188
+ if tree.get("_unresolved") or walker.cycles:
189
+ parts.append(_render_dependency_graph(tree, walker, caps))
190
+
191
+ Path(out_path).write_text("\n\n".join(parts).rstrip() + "\n")
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Tree walker — single pass over the tree to collect catalog data
196
+ # ---------------------------------------------------------------------------
197
+
198
+
199
+ class _TreeWalker:
200
+ """Collects nodes, edges, topics, cycles in a single depth-first pass.
201
+
202
+ P2.2-1: BFS-equivalent visit budget already enforced by parse_wave
203
+ (MAX_BFS_DEPTH=5), so we don't need a depth cap here — the tree is
204
+ already bounded. We DO guard against a malformed tree with a
205
+ self-referential `children` ring by tracking id()'s.
206
+ """
207
+
208
+ def __init__(self, tree: Dict[str, Any]) -> None:
209
+ self.tree = tree
210
+ self.topics: List[Dict[str, Any]] = []
211
+ # actions = top-level children of each topic plus planner-level actions
212
+ self.actions: List[Dict[str, Any]] = []
213
+ self.flows: Dict[str, Dict[str, Any]] = {}
214
+ self.apex: Dict[str, Dict[str, Any]] = {}
215
+ self.prompts: Dict[str, Dict[str, Any]] = {}
216
+ self.standard_actions: Dict[str, Dict[str, Any]] = {}
217
+ # tree edges: parent api_name -> list of child api_name
218
+ self.edges: List[Tuple[str, str, str]] = [] # (parent, child, kind)
219
+ # fan-out map for summary-placeholder top-5
220
+ self.fanout: Dict[str, int] = {}
221
+ # cycle-back annotations: list of (node_label, cycle_back_to)
222
+ self.cycles: List[Tuple[str, str]] = []
223
+ # depth-cap truncations (subset of self.cycles surfaced by
224
+ # the unified _truncated annotation with reason="max-depth").
225
+ # Kept separate so downstream renderers can distinguish the two
226
+ # truncation classes without re-walking the tree.
227
+ self.depth_capped: List[Tuple[str, str]] = []
228
+ self._seen_py_ids: set[int] = set()
229
+
230
+ def walk(self) -> None:
231
+ root = self.tree.get("root") or {}
232
+ for child in root.get("children") or []:
233
+ self._visit(child, parent_label=root.get("api_name") or "ROOT",
234
+ topic=None)
235
+
236
+ def _visit(
237
+ self,
238
+ node: Dict[str, Any],
239
+ *,
240
+ parent_label: str,
241
+ topic: Optional[Dict[str, Any]],
242
+ ) -> None:
243
+ if not isinstance(node, dict):
244
+ return
245
+ nid = id(node)
246
+ if nid in self._seen_py_ids:
247
+ return
248
+ self._seen_py_ids.add(nid)
249
+
250
+ kind = node.get("kind") or "UNKNOWN"
251
+ api_name = node.get("api_name") or node.get("element_name") or ""
252
+
253
+ # Top-level children of a BOT_DEFINITION with kind TOPIC feed the
254
+ # topic list. Non-topic top-level children (planner-level actions)
255
+ # attach to a synthetic `_plannerActions` bucket.
256
+ if kind == "TOPIC" and topic is None:
257
+ topic_rec = {
258
+ "api_name": api_name,
259
+ "label": node.get("master_label") or api_name,
260
+ "actions": [],
261
+ "raw": node,
262
+ }
263
+ self.topics.append(topic_rec)
264
+ topic = topic_rec
265
+ elif topic is None and kind == "GEN_AI_FUNCTION":
266
+ # plannerAction (classic Sequential / NGA) — no parent topic.
267
+ self.actions.append({
268
+ "kind": kind,
269
+ "api_name": api_name,
270
+ "topic": None,
271
+ "raw": node,
272
+ })
273
+ elif topic is not None and kind == "GEN_AI_FUNCTION":
274
+ action_rec = {
275
+ "kind": kind,
276
+ "api_name": api_name,
277
+ "topic": topic["api_name"],
278
+ "raw": node,
279
+ }
280
+ topic["actions"].append(action_rec)
281
+ self.actions.append(action_rec)
282
+
283
+ # Per-kind catalog buckets. We key on api_name; dupes collapse.
284
+ if kind == "FLOW" and api_name:
285
+ self.flows.setdefault(api_name, node)
286
+ elif kind == "APEX" and api_name:
287
+ self.apex.setdefault(api_name, node)
288
+ elif kind == "PROMPT_TEMPLATE" and api_name:
289
+ self.prompts.setdefault(api_name, node)
290
+ elif kind == "STANDARD_ACTION" and api_name:
291
+ self.standard_actions.setdefault(api_name, node)
292
+
293
+ # per-node truncation annotation (cycle OR depth-cap).
294
+ # Prefer the unified `_truncated` sub-object; fall back to the
295
+ # deprecated `_cycle_back_to` string for trees produced by
296
+ # older parse_wave versions.
297
+ trunc = node.get("_truncated") or {}
298
+ cycle_to = trunc.get("target") or node.get("_cycle_back_to")
299
+ reason = trunc.get("reason") or ("cycle" if cycle_to else None)
300
+ if cycle_to:
301
+ self.cycles.append((api_name or parent_label, str(cycle_to)))
302
+ # Optional: downstream renderers may want to distinguish
303
+ # the two truncation classes. We keep that open by stashing
304
+ # `reason` when present.
305
+ if reason and reason != "cycle":
306
+ self.depth_capped.append((api_name or parent_label, str(cycle_to)))
307
+
308
+ # Edge + fanout bookkeeping
309
+ children = node.get("children") or []
310
+ if api_name and parent_label and parent_label != api_name:
311
+ self.edges.append((parent_label, api_name, kind))
312
+ self.fanout[parent_label] = self.fanout.get(parent_label, 0) + 1
313
+
314
+ for child in children:
315
+ self._visit(child, parent_label=api_name or parent_label,
316
+ topic=topic)
317
+
318
+ # ---- helpers -----------------------------------------------------
319
+
320
+ def total_nodes(self) -> int:
321
+ counts = self.tree.get("_kind_counts") or {}
322
+ return sum(counts.values()) or self.tree.get("node_count", 0)
323
+
324
+ def top_fanout(self, n: int = 5) -> List[Tuple[str, int]]:
325
+ return sorted(self.fanout.items(), key=lambda kv: (-kv[1], kv[0]))[:n]
326
+
327
+
328
+ # ---------------------------------------------------------------------------
329
+ # Section renderers
330
+ # ---------------------------------------------------------------------------
331
+
332
+
333
+ def _md_escape(value: Any) -> str:
334
+ if value is None:
335
+ return "-"
336
+ s = str(value)
337
+ # Escape the two characters that break markdown tables.
338
+ return s.replace("|", r"\|").replace("\n", " ")
339
+
340
+
341
+ def _render_header(tree: Dict[str, Any], agent: Dict[str, Any]) -> str:
342
+ # P2.2-1: section 1 — kv table. No input is trusted (agent fields
343
+ # come from SOQL + metadata retrieve), but we escape pipes anyway.
344
+ lines = ["# Architecture — `{}` `{}`".format(
345
+ _md_escape(agent.get("api_name") or "?"),
346
+ _md_escape(agent.get("version") or "?"),
347
+ )]
348
+ lines.append("")
349
+ lines.append("| Field | Value |")
350
+ lines.append("|---|---|")
351
+ rows = [
352
+ ("Master label", agent.get("master_label")),
353
+ ("Description", agent.get("description")),
354
+ ("Agent type", agent.get("agent_type")),
355
+ ("Type", agent.get("type")),
356
+ ("Template", agent.get("agent_template")),
357
+ ("Bot source", agent.get("bot_source")),
358
+ ("Generation", agent.get("generation")),
359
+ ("Planner name", agent.get("planner_name")),
360
+ ("Planner type", agent.get("planner_type")),
361
+ ("Bot id", agent.get("bot_id")),
362
+ ("Schema version", tree.get("_schema_version")),
363
+ ]
364
+ for label, value in rows:
365
+ lines.append(f"| {label} | {_md_escape(value)} |")
366
+ return "\n".join(lines)
367
+
368
+
369
+ def _render_anatomy_summary(tree: Dict[str, Any], walker: _TreeWalker) -> str:
370
+ # P2.2-1: section 2 renders health callout when _partial=true.
371
+ kc = tree.get("_kind_counts") or {}
372
+ lines = ["## 2. Anatomy summary", ""]
373
+ topic_count = kc.get("TOPIC", len(walker.topics))
374
+ action_count = kc.get("GEN_AI_FUNCTION", len(walker.actions))
375
+ flow_count = kc.get("FLOW", len(walker.flows))
376
+ apex_count = kc.get("APEX", len(walker.apex))
377
+ prompt_count = kc.get("PROMPT_TEMPLATE", len(walker.prompts))
378
+ stdaction_count = kc.get("STANDARD_ACTION", len(walker.standard_actions))
379
+
380
+ lines.append(
381
+ "Agent exposes **{} topics** and **{} declared actions** "
382
+ "spanning **{} flows**, **{} apex classes**, **{} prompt templates**, "
383
+ "and **{} standard actions**. Tree depth is {} across {} total nodes.".format(
384
+ topic_count, action_count, flow_count, apex_count,
385
+ prompt_count, stdaction_count,
386
+ tree.get("depth", "?"), walker.total_nodes(),
387
+ )
388
+ )
389
+
390
+ if tree.get("_partial"):
391
+ pending = tree.get("_pending_fetches") or {}
392
+ pending_count = sum(len(v) for v in pending.values())
393
+ reason = tree.get("_partial_reason") or "unspecified"
394
+ lines.append("")
395
+ lines.append("> **Health: PARTIAL.** The tree did not fully converge.")
396
+ lines.append(f"> - Reason: `{_md_escape(reason)}`")
397
+ lines.append(f"> - Pending fetches: {pending_count}")
398
+ if pending:
399
+ for key, items in pending.items():
400
+ if items:
401
+ lines.append(
402
+ f"> - `{_md_escape(key)}`: {len(items)} outstanding"
403
+ )
404
+
405
+ # P2.2-1: health callout when planner_name is missing.
406
+ agent = tree.get("agent") or {}
407
+ if not agent.get("planner_name"):
408
+ lines.append("")
409
+ lines.append(
410
+ "> **Health: WARN.** `planner_name` missing from agent metadata — "
411
+ "downstream sections render best-effort from tree shape alone."
412
+ )
413
+
414
+ if tree.get("_unresolved"):
415
+ lines.append("")
416
+ lines.append(
417
+ "> **Health: WARN.** {} unresolved references — see section 8.".format(
418
+ len(tree["_unresolved"])
419
+ )
420
+ )
421
+
422
+ return "\n".join(lines)
423
+
424
+
425
+ def _render_invocation_sequence(
426
+ tree: Dict[str, Any],
427
+ agent: Dict[str, Any],
428
+ walker: _TreeWalker,
429
+ caps: Dict[str, int],
430
+ ) -> str:
431
+ # P2.2-1: section 3 — sequenceDiagram with cap check.
432
+ lines = ["## 3. Invocation sequence", ""]
433
+ generation = (agent.get("generation") or "classic").lower()
434
+
435
+ participants = ["participant User", " participant Planner"]
436
+ if generation == "nga":
437
+ participants.append(" participant Orchestrator")
438
+ participants.append(" participant SubAgent")
439
+ else:
440
+ participants.append(" participant TopicClassifier")
441
+ participants.append(" participant ActionExecutor")
442
+
443
+ messages: List[str] = []
444
+ messages.append(" User->>+Planner: utterance")
445
+ if generation == "nga":
446
+ messages.append(" Planner->>+Orchestrator: plan")
447
+ messages.append(" Orchestrator->>+SubAgent: dispatch (par/and)")
448
+ messages.append(" SubAgent->>+ActionExecutor: invoke action")
449
+ messages.append(" ActionExecutor-->>-SubAgent: result")
450
+ messages.append(" SubAgent-->>-Orchestrator: subresult")
451
+ messages.append(" Orchestrator-->>-Planner: aggregated")
452
+ elif (agent.get("planner_type") or "").endswith("SequentialPlannerIntentClassifier"):
453
+ messages.append(" Planner->>+ActionExecutor: direct intent->action")
454
+ messages.append(" ActionExecutor-->>-Planner: result")
455
+ else:
456
+ # Classic ReAct — one round-trip per topic (sampled at :5 for
457
+ # readability). The cap check below counts the ACTUAL rendered
458
+ # messages, so a 30-topic bot whose ReAct branch only emits 12
459
+ # lines is correctly NOT truncated.
460
+ for topic in walker.topics[:5]: # sample for readability
461
+ label = _md_escape(topic["api_name"])
462
+ messages.append(f" Planner->>+TopicClassifier: classify → {label}")
463
+ messages.append(f" TopicClassifier-->>-Planner: topic={label}")
464
+ messages.append(" Planner->>+ActionExecutor: invoke action")
465
+ messages.append(" ActionExecutor-->>-Planner: result")
466
+
467
+ messages.append(" Planner-->>-User: response")
468
+
469
+ # cap against ACTUAL rendered message count, not a
470
+ # potential / over-estimated figure. The prior implementation used
471
+ # `2 * len(walker.topics) + len(walker.actions) + 2`, which false-
472
+ # tripped on large bots because the ReAct branch only emits
473
+ # `2 * min(len(walker.topics), 5) + 2` lines (topics[:5] sampling).
474
+ # The rendered-list length is the single source of truth.
475
+ msg_count = len(messages)
476
+ if msg_count > caps.get("sequenceDiagram", 60):
477
+ lines.append(_truncation_placeholder(
478
+ kind="sequenceDiagram", total=msg_count,
479
+ cap=caps["sequenceDiagram"],
480
+ top_fanout=walker.top_fanout(5),
481
+ catalog_pointer="section 5 (action catalog)",
482
+ ))
483
+ return "\n".join(lines)
484
+
485
+ rendered = load_mermaid(
486
+ "invocation_sequence",
487
+ PARTICIPANTS="\n".join(participants).lstrip(),
488
+ MESSAGES="\n".join(messages).lstrip(),
489
+ )
490
+ lines.append("```mermaid")
491
+ lines.append(rendered)
492
+ lines.append("```")
493
+ return "\n".join(lines)
494
+
495
+
496
+ def _render_action_tree(
497
+ tree: Dict[str, Any],
498
+ walker: _TreeWalker,
499
+ caps: Dict[str, int],
500
+ ) -> str:
501
+ # P2.2-1: section 3 — flowchart + subgraphs per topic; cycle back-edges.
502
+ lines = ["## 3. Action tree", ""]
503
+ total_nodes = walker.total_nodes()
504
+ if total_nodes > caps.get("flowchart", 200):
505
+ lines.append(_truncation_placeholder(
506
+ kind="flowchart", total=total_nodes,
507
+ cap=caps["flowchart"],
508
+ top_fanout=walker.top_fanout(5),
509
+ catalog_pointer="section 5 (action catalog) and section 7 (artifact catalogs)",
510
+ ))
511
+ lines.append("")
512
+ lines.append(_render_action_tree_ascii(walker))
513
+ return "\n".join(lines)
514
+
515
+ # Build subgraphs and edges
516
+ subgraphs: List[str] = []
517
+ for topic in walker.topics:
518
+ sg_id = _safe_id(topic["api_name"])
519
+ sg_lines = [f" subgraph {sg_id}[\"{_md_escape(topic['label'])}\"]"]
520
+ for action in topic["actions"]:
521
+ node_id = _safe_id(action["api_name"])
522
+ label = _display_name(action.get("raw") or action)
523
+ sg_lines.append(f" {node_id}[\"{_md_escape(label)}\"]")
524
+ sg_lines.append(" end")
525
+ subgraphs.append("\n".join(sg_lines))
526
+
527
+ # Planner-level actions (no topic)
528
+ planner_actions = [a for a in walker.actions if a.get("topic") is None]
529
+ if planner_actions:
530
+ sg_lines = [" subgraph _plannerActions[\"(plannerActions)\"]"]
531
+ for action in planner_actions:
532
+ node_id = _safe_id(action["api_name"])
533
+ label = _display_name(action.get("raw") or action)
534
+ sg_lines.append(f" {node_id}[\"{_md_escape(label)}\"]")
535
+ sg_lines.append(" end")
536
+ subgraphs.append("\n".join(sg_lines))
537
+
538
+ edges: List[str] = []
539
+ for parent, child, kind in walker.edges:
540
+ pid = _safe_id(parent)
541
+ cid = _safe_id(child)
542
+ edges.append(f" {pid} --> {cid}")
543
+
544
+ # Cycle back-edges (dotted)
545
+ for node_label, cycle_to in walker.cycles:
546
+ nid = _safe_id(node_label)
547
+ tid = _safe_id(cycle_to)
548
+ edges.append(f" {nid} -.->|cycle_back_to: {_md_escape(cycle_to)}| {tid}")
549
+
550
+ rendered = load_mermaid(
551
+ "action_tree",
552
+ SUBGRAPHS="\n\n".join(subgraphs).lstrip() if subgraphs else "%% no topics",
553
+ EDGES="\n".join(edges).lstrip() if edges else "%% no edges",
554
+ )
555
+ lines.append("```mermaid")
556
+ lines.append(rendered)
557
+ lines.append("```")
558
+ lines.append("")
559
+ lines.append("**ASCII appendix**")
560
+ lines.append("")
561
+ lines.append("```")
562
+ lines.append(_render_action_tree_ascii(walker))
563
+ lines.append("```")
564
+ return "\n".join(lines)
565
+
566
+
567
+ def _render_action_tree_ascii(walker: _TreeWalker) -> str:
568
+ out: List[str] = []
569
+ root = walker.tree.get("root") or {}
570
+ out.append(f"{root.get('api_name', 'ROOT')} ({root.get('kind', 'BOT_DEFINITION')})")
571
+ _ascii_recurse(root.get("children") or [], out, depth=1, seen=set())
572
+ return "\n".join(out)
573
+
574
+
575
+ def _ascii_recurse(
576
+ children: List[Dict[str, Any]],
577
+ out: List[str],
578
+ depth: int,
579
+ seen: set[int],
580
+ ) -> None:
581
+ for child in children:
582
+ if not isinstance(child, dict):
583
+ continue
584
+ nid = id(child)
585
+ if nid in seen:
586
+ continue
587
+ seen.add(nid)
588
+ name = _display_name(child)
589
+ kind = child.get("kind") or "?"
590
+ # surface BOTH cycle AND max-depth truncation in the ASCII
591
+ # tree view. Prefer `_truncated["reason"]`; fall back to the
592
+ # legacy `_cycle_back_to` string (older parse_wave output).
593
+ trunc = child.get("_truncated") or {}
594
+ if trunc.get("reason") == "max-depth":
595
+ marker = " [depth-capped]"
596
+ elif trunc.get("reason") == "cycle" or child.get("_cycle_back_to"):
597
+ marker = " [cycle]"
598
+ else:
599
+ marker = ""
600
+ out.append(f"{' ' * depth}├── [{kind}] {name}{marker}")
601
+ grand = child.get("children") or []
602
+ if grand:
603
+ _ascii_recurse(grand, out, depth + 1, seen)
604
+
605
+
606
+ def _render_topic_anatomy(walker: _TreeWalker) -> str:
607
+ # P2.2-1: section 4 — H3 per topic + kv list. Empty-case: 0 topics is
608
+ # valid for SequentialPlannerIntentClassifier.
609
+ lines = ["## 4. Topic anatomy", ""]
610
+ if not walker.topics:
611
+ lines.append("_No topics defined (planner exposes actions directly)._")
612
+ return "\n".join(lines)
613
+ for topic in walker.topics:
614
+ lines.append(f"### `{_md_escape(topic['api_name'])}`")
615
+ lines.append("")
616
+ lines.append(f"- Label: {_md_escape(topic['label'])}")
617
+ lines.append(f"- Action count: {len(topic['actions'])}")
618
+ if topic["actions"]:
619
+ lines.append("- Actions:")
620
+ for action in topic["actions"]:
621
+ label = _display_name(action.get("raw") or action)
622
+ lines.append(f" - `{_md_escape(label)}`")
623
+ lines.append("")
624
+ return "\n".join(lines).rstrip()
625
+
626
+
627
+ def _render_action_catalog(walker: _TreeWalker) -> str:
628
+ # P2.2-1: section 5 — markdown table of actions.
629
+ lines = ["## 5. Action catalog", ""]
630
+ if not walker.actions:
631
+ lines.append("_No actions declared._")
632
+ return "\n".join(lines)
633
+ lines.append("| Action | Topic | Unwraps to |")
634
+ lines.append("|---|---|---|")
635
+ for action in walker.actions:
636
+ raw = action.get("raw") or {}
637
+ unwrap = raw.get("unwraps_to") or {}
638
+ unwrap_str = "-"
639
+ if unwrap:
640
+ unwrap_str = "{} `{}`".format(
641
+ unwrap.get("kind", "?"),
642
+ _display_name(unwrap),
643
+ )
644
+ lines.append("| `{}` | {} | {} |".format(
645
+ _md_escape(action["api_name"]),
646
+ _md_escape(action.get("topic") or "(plannerAction)"),
647
+ _md_escape(unwrap_str),
648
+ ))
649
+ return "\n".join(lines)
650
+
651
+
652
+ def _planner_state_for_generation(
653
+ agent: Dict[str, Any], generation: str,
654
+ ) -> Tuple[List[str], List[str]]:
655
+ """Return (states, transitions) for the planner state machine."""
656
+ planner_type = (agent.get("planner_type") or "").lower()
657
+ if generation == "nga":
658
+ states = [
659
+ "[*] --> Planning",
660
+ " state Planning",
661
+ " state Orchestration {",
662
+ " direction LR",
663
+ " [*] --> Dispatch",
664
+ " Dispatch --> SubAgentA",
665
+ " Dispatch --> SubAgentB",
666
+ " --",
667
+ " SubAgentA --> Aggregate",
668
+ " SubAgentB --> Aggregate",
669
+ " }",
670
+ " state Respond",
671
+ ]
672
+ transitions = [
673
+ " Planning --> Orchestration: par/and dispatch",
674
+ " Orchestration --> Respond: aggregated",
675
+ " Respond --> [*]",
676
+ ]
677
+ return states, transitions
678
+ if planner_type.endswith("sequentialplannerintentclassifier"):
679
+ states = [
680
+ "[*] --> Classify",
681
+ " state Classify",
682
+ " state Execute",
683
+ " state Respond",
684
+ ]
685
+ transitions = [
686
+ " Classify --> Execute: intent",
687
+ " Execute --> Respond: result",
688
+ " Respond --> [*]",
689
+ ]
690
+ return states, transitions
691
+ # Default: classic ReAct
692
+ states = [
693
+ "[*] --> Thought",
694
+ " state Thought",
695
+ " state Action",
696
+ " state Observation",
697
+ " state Respond",
698
+ ]
699
+ transitions = [
700
+ " Thought --> Action: pick tool",
701
+ " Action --> Observation: tool result",
702
+ " Observation --> Thought: more reasoning",
703
+ " Observation --> Respond: done",
704
+ " Respond --> [*]",
705
+ ]
706
+ return states, transitions
707
+
708
+
709
+ def _render_planner_state(
710
+ agent: Dict[str, Any],
711
+ generation: str,
712
+ caps: Dict[str, int],
713
+ ) -> str:
714
+ # P2.2-1: section 6 — stateDiagram-v2 with generation-aware branches.
715
+ lines = ["## 6. Planner state machine", ""]
716
+
717
+ if generation in ("search", "byop"):
718
+ lines.append(
719
+ "_Custom planner — structure depends on the planner's Apex class_ "
720
+ f"(`{_md_escape(agent.get('planner_type') or '?')}`). State "
721
+ "diagram skipped; see section 7 for the backing Apex class body."
722
+ )
723
+ return "\n".join(lines)
724
+
725
+ states, transitions = _planner_state_for_generation(agent, generation)
726
+ total = len(states) + len(transitions)
727
+ if total > caps.get("stateDiagram", 40):
728
+ lines.append(_truncation_placeholder(
729
+ kind="stateDiagram", total=total,
730
+ cap=caps["stateDiagram"],
731
+ top_fanout=[],
732
+ catalog_pointer="section 7 (artifact catalogs)",
733
+ ))
734
+ return "\n".join(lines)
735
+
736
+ rendered = load_mermaid(
737
+ "planner_state",
738
+ STATES="\n".join(states).lstrip(),
739
+ TRANSITIONS="\n".join(transitions).lstrip(),
740
+ )
741
+ lines.append("```mermaid")
742
+ lines.append(rendered)
743
+ lines.append("```")
744
+ return "\n".join(lines)
745
+
746
+
747
+ def _render_data_flow(
748
+ tree: Dict[str, Any],
749
+ walker: _TreeWalker,
750
+ caps: Dict[str, int],
751
+ ) -> str:
752
+ # P2.2-1: section 6 — flowchart LR with labeled param edges.
753
+ lines = ["## 6. Data flow / context propagation", ""]
754
+
755
+ # Build node list — User, Planner, each topic, each action.
756
+ nodes: List[str] = [" User([User utterance])", " Planner[Planner]"]
757
+ for topic in walker.topics:
758
+ nodes.append(f" {_safe_id(topic['api_name'])}[Topic: {_md_escape(topic['api_name'])}]")
759
+
760
+ edges: List[str] = [" User --> Planner"]
761
+ for topic in walker.topics:
762
+ edges.append(f" Planner --> {_safe_id(topic['api_name'])}")
763
+ for action in topic["actions"]:
764
+ label = _display_name(action.get("raw") or action)
765
+ nodes.append(
766
+ f" {_safe_id(action['api_name'])}[[Action: {_md_escape(label)}]]"
767
+ )
768
+ # Labeled edge when the planner attr metadata declares a
769
+ # parameter hand-off; fall back to a bare edge otherwise.
770
+ attr = (action.get("raw") or {}).get("planner_attr") or {}
771
+ var_name = attr.get("variable_name") or attr.get("name")
772
+ var_type = attr.get("data_type") or attr.get("type")
773
+ if var_name:
774
+ label = _md_escape(var_name)
775
+ if var_type:
776
+ label = f"{label}: {_md_escape(var_type)}"
777
+ edges.append(
778
+ f" {_safe_id(topic['api_name'])} -->|{label}| "
779
+ f"{_safe_id(action['api_name'])}"
780
+ )
781
+ else:
782
+ edges.append(
783
+ f" {_safe_id(topic['api_name'])} --> "
784
+ f"{_safe_id(action['api_name'])}"
785
+ )
786
+
787
+ total = len(nodes) + len(edges)
788
+ if total > caps.get("flowchart", 200):
789
+ lines.append(_truncation_placeholder(
790
+ kind="flowchart", total=total,
791
+ cap=caps["flowchart"],
792
+ top_fanout=walker.top_fanout(5),
793
+ catalog_pointer="section 7 (artifact catalogs)",
794
+ ))
795
+ return "\n".join(lines)
796
+
797
+ rendered = load_mermaid(
798
+ "data_flow",
799
+ NODES="\n".join(nodes).lstrip(),
800
+ EDGES="\n".join(edges).lstrip(),
801
+ )
802
+ lines.append("```mermaid")
803
+ lines.append(rendered)
804
+ lines.append("```")
805
+ return "\n".join(lines)
806
+
807
+
808
+ def _render_artifact_catalogs(walker: _TreeWalker) -> str:
809
+ # P2.2-1: section 7 — H3 per flow / apex / prompt + signature.
810
+ lines = ["## 7. Flow / Apex / Prompt catalogs", ""]
811
+
812
+ if walker.flows:
813
+ lines.append("### Flows")
814
+ lines.append("")
815
+ for name in sorted(walker.flows):
816
+ node = walker.flows[name]
817
+ sig = node.get("signature") or node.get("_signature")
818
+ # Gap 2 fix (2026-05-05): main._stamp_signatures stamps
819
+ # `_signature_reason` on flows whose body we can't retrieve
820
+ # (managed package, no active version, metadata fetch miss).
821
+ # Surface the reason so the rendered markdown distinguishes
822
+ # a known limitation from a silent hole.
823
+ reason = node.get("_signature_reason")
824
+ lines.append(f"#### `{_md_escape(name)}`")
825
+ lines.append("")
826
+ if sig:
827
+ lines.append("```")
828
+ lines.append(str(sig))
829
+ lines.append("```")
830
+ elif reason:
831
+ lines.append(f"_Signature not captured — {_md_escape(reason)}._")
832
+ else:
833
+ lines.append("_Signature not captured._")
834
+ lines.append("")
835
+
836
+ if walker.apex:
837
+ lines.append("### Apex classes")
838
+ lines.append("")
839
+ for name in sorted(walker.apex):
840
+ node = walker.apex[name]
841
+ sig = node.get("signature") or node.get("_signature")
842
+ lines.append(f"#### `{_md_escape(name)}`")
843
+ lines.append("")
844
+ if sig:
845
+ lines.append("```")
846
+ lines.append(str(sig))
847
+ lines.append("```")
848
+ else:
849
+ lines.append("_Signature not captured._")
850
+ lines.append("")
851
+
852
+ if walker.prompts:
853
+ lines.append("### Prompt templates")
854
+ lines.append("")
855
+ for name in sorted(walker.prompts):
856
+ node = walker.prompts[name]
857
+ sig = node.get("signature") or node.get("_signature")
858
+ prompt_type = node.get("prompt_type")
859
+ # Gap C (2026-05-05): retrieve_prompt_templates stamps
860
+ # master_label / content / inputs / _body_available onto
861
+ # each PROMPT_TEMPLATE leaf. Emit the real body when
862
+ # available; fall back to the stub only when `_body_available`
863
+ # is explicitly False (retrieve failed / not requested) AND
864
+ # there's no signature/type to show.
865
+ master_label = node.get("master_label")
866
+ content = node.get("content")
867
+ inputs = node.get("inputs") or []
868
+ body_available = node.get("_body_available")
869
+ lines.append(f"#### `{_md_escape(name)}`")
870
+ lines.append("")
871
+ if master_label:
872
+ lines.append(f"_Label: {_md_escape(master_label)}_")
873
+ lines.append("")
874
+ if prompt_type:
875
+ lines.append(f"- Type: `{_md_escape(prompt_type)}`")
876
+ if inputs:
877
+ lines.append("**Inputs**:")
878
+ for inp in inputs:
879
+ if not isinstance(inp, dict):
880
+ continue
881
+ iname = inp.get("name") or "?"
882
+ itype = inp.get("dataType")
883
+ if itype:
884
+ lines.append(
885
+ f"- `{_md_escape(iname)}`: "
886
+ f"`{_md_escape(itype)}`"
887
+ )
888
+ else:
889
+ lines.append(f"- `{_md_escape(iname)}`")
890
+ lines.append("")
891
+ if content:
892
+ lines.append("```text")
893
+ lines.append(str(content))
894
+ lines.append("```")
895
+ elif sig:
896
+ lines.append("```")
897
+ lines.append(str(sig))
898
+ lines.append("```")
899
+ if (
900
+ not content and not sig and not prompt_type
901
+ and not master_label and not inputs
902
+ ):
903
+ if body_available is False:
904
+ lines.append("_Body not retrieved._")
905
+ else:
906
+ lines.append("_Details not captured._")
907
+ lines.append("")
908
+
909
+ if not (walker.flows or walker.apex or walker.prompts):
910
+ lines.append("_No backing artifacts in tree._")
911
+
912
+ return "\n".join(lines).rstrip()
913
+
914
+
915
+ def _render_unresolved(tree: Dict[str, Any], walker: _TreeWalker) -> str:
916
+ # P2.2-1: section 8 — unresolved refs + artifact pointers.
917
+ lines = ["## 8. Unresolved refs + artifact pointers", ""]
918
+ unresolved = tree.get("_unresolved") or []
919
+ if unresolved:
920
+ lines.append("> **{} unresolved refs.**".format(len(unresolved)))
921
+ lines.append("")
922
+ lines.append("| Kind | Api name | Reason |")
923
+ lines.append("|---|---|---|")
924
+ for ref in unresolved:
925
+ lines.append("| {} | `{}` | {} |".format(
926
+ _md_escape(ref.get("kind") or "?"),
927
+ _md_escape(ref.get("api_name") or "?"),
928
+ _md_escape(ref.get("reason") or "?"),
929
+ ))
930
+ else:
931
+ lines.append("_No unresolved references._")
932
+ lines.append("")
933
+ lines.append("### Artifact pointers")
934
+ lines.append("")
935
+ lines.append(
936
+ "- Full tree JSON: same directory as this file (`metadata_tree.json`)"
937
+ )
938
+ lines.append("- Build manifest: cache dir / `manifest.json`")
939
+ return "\n".join(lines)
940
+
941
+
942
+ def _render_dependency_graph(
943
+ tree: Dict[str, Any],
944
+ walker: _TreeWalker,
945
+ caps: Dict[str, int],
946
+ ) -> str:
947
+ # P2.2-1: conditional — render only on unresolved or cycles.
948
+ lines = ["## Dependency graph (conditional)", ""]
949
+ unresolved = tree.get("_unresolved") or []
950
+
951
+ nodes: List[str] = []
952
+ edges: List[str] = []
953
+ seen: set[str] = set()
954
+
955
+ def add_node(label: str, unresolved_flag: bool) -> None:
956
+ nid = _safe_id(label)
957
+ if nid in seen:
958
+ return
959
+ seen.add(nid)
960
+ suffix = ":::unresolved" if unresolved_flag else ""
961
+ nodes.append(f" {nid}[{_md_escape(label)}]{suffix}")
962
+
963
+ for parent, child, _ in walker.edges:
964
+ add_node(parent, False)
965
+ add_node(child, False)
966
+ edges.append(f" {_safe_id(parent)} --> {_safe_id(child)}")
967
+
968
+ for ref in unresolved:
969
+ name = ref.get("api_name") or "?"
970
+ add_node(name, True)
971
+
972
+ for node_label, cycle_to in walker.cycles:
973
+ add_node(node_label, False)
974
+ add_node(cycle_to, False)
975
+ edges.append(
976
+ f" {_safe_id(node_label)} -.->|cycle| {_safe_id(cycle_to)}"
977
+ )
978
+
979
+ total = len(nodes)
980
+ if total > caps.get("graph", 100):
981
+ lines.append(_truncation_placeholder(
982
+ kind="graph", total=total,
983
+ cap=caps["graph"],
984
+ top_fanout=walker.top_fanout(5),
985
+ catalog_pointer="section 8 (unresolved refs)",
986
+ ))
987
+ return "\n".join(lines)
988
+
989
+ rendered = load_mermaid(
990
+ "dependency_graph",
991
+ NODES="\n".join(nodes).lstrip() if nodes else "%% no nodes",
992
+ EDGES="\n".join(edges).lstrip() if edges else "%% no edges",
993
+ )
994
+ lines.append("```mermaid")
995
+ lines.append(rendered)
996
+ lines.append("```")
997
+ return "\n".join(lines)
998
+
999
+
1000
+ # ---------------------------------------------------------------------------
1001
+ # Helpers
1002
+ # ---------------------------------------------------------------------------
1003
+
1004
+
1005
+ def _truncation_placeholder(
1006
+ *,
1007
+ kind: str,
1008
+ total: int,
1009
+ cap: int,
1010
+ top_fanout: List[Tuple[str, int]],
1011
+ catalog_pointer: str,
1012
+ ) -> str:
1013
+ # P2.2-1: explicit visual placeholder. Mentions the diagram kind, the
1014
+ # over-cap count, and points at the catalog section.
1015
+ lines = [
1016
+ f"> **[diagram truncated: {kind} — {total} elements exceed cap of {cap}]**",
1017
+ ]
1018
+ if top_fanout:
1019
+ lines.append(">")
1020
+ lines.append("> Top 5 nodes by fan-out:")
1021
+ for name, count in top_fanout:
1022
+ lines.append(f"> - `{_md_escape(name)}` ({count})")
1023
+ lines.append(">")
1024
+ lines.append(f"> See {catalog_pointer} for the full listing.")
1025
+ return "\n".join(lines)
1026
+
1027
+
1028
+ def _safe_id(value: str) -> str:
1029
+ """Return a mermaid-safe identifier. Mermaid node ids must be
1030
+ alphanumeric + underscore; anything else breaks the parser."""
1031
+ if not value:
1032
+ return "n_empty"
1033
+ out = []
1034
+ for ch in str(value):
1035
+ if ch.isalnum() or ch == "_":
1036
+ out.append(ch)
1037
+ else:
1038
+ out.append("_")
1039
+ # Prefix digit-leading ids so they don't collide with mermaid syntax.
1040
+ result = "".join(out)
1041
+ if result and result[0].isdigit():
1042
+ result = f"n_{result}"
1043
+ return result or "n_empty"