@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.
- package/package.json +1 -1
- package/skills/activating-datacloud/SKILL.md +0 -1
- package/skills/analyzing-omnistudio-dependencies/SKILL.md +0 -1
- package/skills/applying-slds/SKILL.md +322 -0
- package/skills/applying-slds/checklists.md +83 -0
- package/skills/applying-slds/examples.md +283 -0
- package/skills/applying-slds/guidance/README.md +83 -0
- package/skills/applying-slds/guidance/blueprints-index.md +213 -0
- package/skills/applying-slds/guidance/icons-guidance.md +186 -0
- package/skills/applying-slds/guidance/overviews/borders.md +236 -0
- package/skills/applying-slds/guidance/overviews/color.md +266 -0
- package/skills/applying-slds/guidance/overviews/display-density.md +366 -0
- package/skills/applying-slds/guidance/overviews/icons.md +240 -0
- package/skills/applying-slds/guidance/overviews/illustrations.md +235 -0
- package/skills/applying-slds/guidance/overviews/shadows.md +176 -0
- package/skills/applying-slds/guidance/overviews/spacing.md +216 -0
- package/skills/applying-slds/guidance/overviews/typography.md +323 -0
- package/skills/applying-slds/guidance/overviews/utilities.md +542 -0
- package/skills/applying-slds/guidance/slds-development-guide.md +288 -0
- package/skills/applying-slds/guidance/styling-hooks/borders.md +202 -0
- package/skills/applying-slds/guidance/styling-hooks/color/expressive-palette-hooks.md +153 -0
- package/skills/applying-slds/guidance/styling-hooks/color/index.md +171 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/accent-hooks.md +204 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/feedback-hooks.md +768 -0
- package/skills/applying-slds/guidance/styling-hooks/color/semantic/surface-hooks.md +337 -0
- package/skills/applying-slds/guidance/styling-hooks/color/system-hooks.md +132 -0
- package/skills/applying-slds/guidance/styling-hooks/index.md +327 -0
- package/skills/applying-slds/guidance/styling-hooks/shadows.md +238 -0
- package/skills/applying-slds/guidance/styling-hooks/spacing.md +254 -0
- package/skills/applying-slds/guidance/styling-hooks/typography.md +448 -0
- package/skills/applying-slds/guidance/utilities/alignment.md +119 -0
- package/skills/applying-slds/guidance/utilities/borders.md +131 -0
- package/skills/applying-slds/guidance/utilities/box.md +125 -0
- package/skills/applying-slds/guidance/utilities/color.md +165 -0
- package/skills/applying-slds/guidance/utilities/dark-mode.md +111 -0
- package/skills/applying-slds/guidance/utilities/description-list.md +168 -0
- package/skills/applying-slds/guidance/utilities/floats.md +117 -0
- package/skills/applying-slds/guidance/utilities/grid.md +264 -0
- package/skills/applying-slds/guidance/utilities/horizontal-list.md +110 -0
- package/skills/applying-slds/guidance/utilities/hyphenation.md +84 -0
- package/skills/applying-slds/guidance/utilities/index.md +205 -0
- package/skills/applying-slds/guidance/utilities/interactions.md +89 -0
- package/skills/applying-slds/guidance/utilities/layout.md +109 -0
- package/skills/applying-slds/guidance/utilities/line-clamp.md +131 -0
- package/skills/applying-slds/guidance/utilities/margin.md +155 -0
- package/skills/applying-slds/guidance/utilities/media-object.md +161 -0
- package/skills/applying-slds/guidance/utilities/name-value-list.md +152 -0
- package/skills/applying-slds/guidance/utilities/padding.md +155 -0
- package/skills/applying-slds/guidance/utilities/position.md +177 -0
- package/skills/applying-slds/guidance/utilities/print.md +114 -0
- package/skills/applying-slds/guidance/utilities/scrollable.md +126 -0
- package/skills/applying-slds/guidance/utilities/sizing.md +190 -0
- package/skills/applying-slds/guidance/utilities/themes.md +121 -0
- package/skills/applying-slds/guidance/utilities/truncate.md +127 -0
- package/skills/applying-slds/guidance/utilities/typography.md +166 -0
- package/skills/applying-slds/guidance/utilities/vertical-list.md +166 -0
- package/skills/applying-slds/guidance/utilities/visibility.md +228 -0
- package/skills/applying-slds/metadata/README.md +84 -0
- package/skills/applying-slds/metadata/blueprints/components/accordion.yaml +304 -0
- package/skills/applying-slds/metadata/blueprints/components/activity-timeline.yaml +92 -0
- package/skills/applying-slds/metadata/blueprints/components/alert.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/app-launcher.yaml +94 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar-group.yaml +81 -0
- package/skills/applying-slds/metadata/blueprints/components/avatar.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/badges.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/brand-band.yaml +198 -0
- package/skills/applying-slds/metadata/blueprints/components/breadcrumbs.yaml +95 -0
- package/skills/applying-slds/metadata/blueprints/components/builder-header.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/button-groups.yaml +82 -0
- package/skills/applying-slds/metadata/blueprints/components/button-icons.yaml +295 -0
- package/skills/applying-slds/metadata/blueprints/components/buttons.yaml +230 -0
- package/skills/applying-slds/metadata/blueprints/components/cards.yaml +124 -0
- package/skills/applying-slds/metadata/blueprints/components/carousel.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/chat.yaml +179 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button-group.yaml +192 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-button.yaml +204 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox-toggle.yaml +177 -0
- package/skills/applying-slds/metadata/blueprints/components/checkbox.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/color-picker.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/combobox.yaml +136 -0
- package/skills/applying-slds/metadata/blueprints/components/counter.yaml +147 -0
- package/skills/applying-slds/metadata/blueprints/components/data-tables.yaml +157 -0
- package/skills/applying-slds/metadata/blueprints/components/datepickers.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/datetime-picker.yaml +155 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-composer.yaml +201 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-form-footer.yaml +161 -0
- package/skills/applying-slds/metadata/blueprints/components/docked-utility-bar.yaml +175 -0
- package/skills/applying-slds/metadata/blueprints/components/drop-zone.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/dueling-picklist.yaml +196 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-icons.yaml +128 -0
- package/skills/applying-slds/metadata/blueprints/components/dynamic-menu.yaml +141 -0
- package/skills/applying-slds/metadata/blueprints/components/expandable-section.yaml +115 -0
- package/skills/applying-slds/metadata/blueprints/components/expression.yaml +143 -0
- package/skills/applying-slds/metadata/blueprints/components/feeds.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/file-selector.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/files.yaml +119 -0
- package/skills/applying-slds/metadata/blueprints/components/form-element.yaml +145 -0
- package/skills/applying-slds/metadata/blueprints/components/global-header.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/global-navigation.yaml +100 -0
- package/skills/applying-slds/metadata/blueprints/components/icons.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/illustration.yaml +205 -0
- package/skills/applying-slds/metadata/blueprints/components/input.yaml +151 -0
- package/skills/applying-slds/metadata/blueprints/components/list-builder.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/lookups.yaml +132 -0
- package/skills/applying-slds/metadata/blueprints/components/map.yaml +118 -0
- package/skills/applying-slds/metadata/blueprints/components/menus.yaml +134 -0
- package/skills/applying-slds/metadata/blueprints/components/modals.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/notifications.yaml +88 -0
- package/skills/applying-slds/metadata/blueprints/components/page-headers.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/panels.yaml +149 -0
- package/skills/applying-slds/metadata/blueprints/components/path.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/picklist.yaml +125 -0
- package/skills/applying-slds/metadata/blueprints/components/pills.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/popovers.yaml +120 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-bar.yaml +110 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-indicator.yaml +133 -0
- package/skills/applying-slds/metadata/blueprints/components/progress-ring.yaml +102 -0
- package/skills/applying-slds/metadata/blueprints/components/prompt.yaml +126 -0
- package/skills/applying-slds/metadata/blueprints/components/publishers.yaml +178 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-button-group.yaml +172 -0
- package/skills/applying-slds/metadata/blueprints/components/radio-group.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/rich-text-editor.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-notifications.yaml +188 -0
- package/skills/applying-slds/metadata/blueprints/components/scoped-tabs.yaml +97 -0
- package/skills/applying-slds/metadata/blueprints/components/select.yaml +127 -0
- package/skills/applying-slds/metadata/blueprints/components/setup-assistant.yaml +152 -0
- package/skills/applying-slds/metadata/blueprints/components/slider.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/spinners.yaml +135 -0
- package/skills/applying-slds/metadata/blueprints/components/split-view.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/summary-detail.yaml +103 -0
- package/skills/applying-slds/metadata/blueprints/components/tabs.yaml +138 -0
- package/skills/applying-slds/metadata/blueprints/components/textarea.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/tiles.yaml +108 -0
- package/skills/applying-slds/metadata/blueprints/components/timepicker.yaml +111 -0
- package/skills/applying-slds/metadata/blueprints/components/toast.yaml +154 -0
- package/skills/applying-slds/metadata/blueprints/components/tooltips.yaml +107 -0
- package/skills/applying-slds/metadata/blueprints/components/tree-grid.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trees.yaml +116 -0
- package/skills/applying-slds/metadata/blueprints/components/trial-bar.yaml +112 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-navigation.yaml +130 -0
- package/skills/applying-slds/metadata/blueprints/components/vertical-tabs.yaml +140 -0
- package/skills/applying-slds/metadata/blueprints/components/visual-picker.yaml +150 -0
- package/skills/applying-slds/metadata/blueprints/components/welcome-mat.yaml +136 -0
- package/skills/applying-slds/metadata/hooks-index.json +6272 -0
- package/skills/applying-slds/metadata/icon-metadata.json +38466 -0
- package/skills/applying-slds/metadata/utilities-index.json +21912 -0
- package/skills/applying-slds/references/component-selection.md +112 -0
- package/skills/applying-slds/references/icons-decision-guide.md +124 -0
- package/skills/applying-slds/references/styling-decision-guide.md +228 -0
- package/skills/applying-slds/references/utilities-quick-ref.md +125 -0
- package/skills/applying-slds/scripts/search-blueprints.cjs +117 -0
- package/skills/applying-slds/scripts/search-hooks.cjs +139 -0
- package/skills/applying-slds/scripts/search-icons.cjs +174 -0
- package/skills/applying-slds/scripts/search-utilities.cjs +161 -0
- package/skills/building-mobile-apps/SKILL.md +0 -1
- package/skills/building-omnistudio-callable-apex/SKILL.md +0 -1
- package/skills/building-omnistudio-datamapper/SKILL.md +0 -1
- package/skills/building-omnistudio-flexcard/SKILL.md +0 -1
- package/skills/building-omnistudio-integration-procedure/SKILL.md +0 -1
- package/skills/building-omnistudio-omniscript/SKILL.md +0 -1
- package/skills/building-sf-integrations/SKILL.md +0 -1
- package/skills/configuring-connected-apps/SKILL.md +0 -1
- package/skills/connecting-datacloud/SKILL.md +0 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +0 -1
- package/skills/debugging-apex-logs/SKILL.md +0 -1
- package/skills/deploying-metadata/SKILL.md +0 -1
- package/skills/deploying-omnistudio-datapacks/SKILL.md +0 -1
- package/skills/developing-agentforce/SKILL.md +0 -1
- package/skills/fetching-salesforce-docs/SKILL.md +0 -1
- package/skills/generating-custom-lightning-type/SKILL.md +17 -39
- package/skills/generating-custom-lightning-type/assets/primitive-types-and-constraints.md +41 -0
- package/skills/generating-custom-lightning-type/references/widget-rendition.md +124 -0
- package/skills/generating-lwc-components/SKILL.md +0 -1
- package/skills/generating-mermaid-diagrams/SKILL.md +0 -1
- package/skills/generating-visual-diagrams/SKILL.md +0 -1
- package/skills/handling-sf-data/SKILL.md +0 -1
- package/skills/harmonizing-datacloud/SKILL.md +0 -1
- package/skills/integrating-b2b-commerce-open-code-components/SKILL.md +0 -1
- package/skills/investigating-agentforce-architecture/README.md +156 -0
- package/skills/investigating-agentforce-architecture/SKILL.md +230 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_sobject.yaml +16 -0
- package/skills/investigating-agentforce-architecture/assets/cli/describe_tooling_sobject.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/list_metadata_genaiprompttemplate.yaml +17 -0
- package/skills/investigating-agentforce-architecture/assets/cli/org_display.yaml +15 -0
- package/skills/investigating-agentforce-architecture/assets/cli/retrieve_genai_plugin.yaml +18 -0
- package/skills/investigating-agentforce-architecture/assets/cli/show_access_token.yaml +27 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/action_tree.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/data_flow.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/dependency_graph.mmd +19 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/invocation_sequence.mmd +20 -0
- package/skills/investigating-agentforce-architecture/assets/mermaid/planner_state.mmd +18 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/apex_class_bodies_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_definition_details.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/bot_version_lookup.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_by_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_ids_by_names.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_definition_view_by_durable_ids.soql +4 -0
- package/skills/investigating-agentforce-architecture/assets/soql/flow_metadata_by_id.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/functions_by_plugins.soql +5 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_attrs_by_parent_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_bundle_functions.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/planner_definition_by_agent_chain.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_functions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugin_instructions_by_plugin_ids.soql +3 -0
- package/skills/investigating-agentforce-architecture/assets/soql/plugins_by_planner.soql +4 -0
- package/skills/investigating-agentforce-architecture/references/architecture_sections.md +243 -0
- package/skills/investigating-agentforce-architecture/references/contract.json +244 -0
- package/skills/investigating-agentforce-architecture/references/soql_fields.md +512 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/__init__.py +1 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/fs_guard.py +329 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/paths.py +110 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-architecture/scripts/_shared/sql.py +10 -0
- package/skills/investigating-agentforce-architecture/scripts/cache_check.py +234 -0
- package/skills/investigating-agentforce-architecture/scripts/config.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/fetch_soql.py +689 -0
- package/skills/investigating-agentforce-architecture/scripts/finalize.py +295 -0
- package/skills/investigating-agentforce-architecture/scripts/main.py +2835 -0
- package/skills/investigating-agentforce-architecture/scripts/metadata_listing.py +265 -0
- package/skills/investigating-agentforce-architecture/scripts/parallel_retrieve.py +69 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_bundle.py +215 -0
- package/skills/investigating-agentforce-architecture/scripts/parse_wave.py +845 -0
- package/skills/investigating-agentforce-architecture/scripts/probe_channels.py +302 -0
- package/skills/investigating-agentforce-architecture/scripts/render_architecture.py +1043 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_bot.py +255 -0
- package/skills/investigating-agentforce-architecture/scripts/resolve_invocation_target.py +130 -0
- package/skills/investigating-agentforce-architecture/scripts/rest_client.py +763 -0
- package/skills/investigating-agentforce-architecture/scripts/retrieve_planner.py +13 -0
- package/skills/investigating-agentforce-architecture/scripts/sf_cli.py +242 -0
- package/skills/investigating-agentforce-architecture/scripts/soql_loader.py +253 -0
- package/skills/investigating-agentforce-architecture/scripts/summarize_tree.py +143 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/_bootstrap.py +23 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/fixtures/genai_payloads.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check.py +307 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_cache_check_main.py +283 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_config.py +115 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_end_to_end_fixture.py +651 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_finalize.py +278 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_flow_children_inflation.py +582 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_fs_guard.py +113 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_iterative_wave_b.py +478 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_main_pipeline.py +3359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parallel_retrieve.py +131 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_bundle.py +400 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave.py +644 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_classifiers.py +224 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_helpers.py +380 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py +397 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_per_branch_visited.py +244 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_channels.py +359 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_probe_cli_recipes.py +185 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_render_architecture.py +810 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_bot.py +203 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_creds.py +157 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_resolve_invocation_target.py +145 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_rest_client.py +1253 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_runtime_override.py +100 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_sf_cli.py +261 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_signature_stamping.py +466 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_soql_loader.py +501 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_summarize_tree.py +241 -0
- package/skills/investigating-agentforce-architecture/scripts/tests/test_write_emit_ctx.py +480 -0
- package/skills/investigating-agentforce-architecture/tools/emit_env.py +157 -0
- package/skills/investigating-agentforce-architecture/tools/emit_result.py +262 -0
- package/skills/investigating-agentforce-architecture/tools/sanitize.py +33 -0
- package/skills/investigating-agentforce-architecture/tools/write_emit_ctx.py +332 -0
- package/skills/investigating-agentforce-d360/README.md +123 -0
- package/skills/investigating-agentforce-d360/SKILL.md +163 -0
- package/skills/investigating-agentforce-d360/assets/dc/app_generation.sql +51 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_category.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/content_quality.sql +41 -0
- package/skills/investigating-agentforce-d360/assets/dc/discover_sessions.sql +36 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback.sql +47 -0
- package/skills/investigating-agentforce-d360/assets/dc/feedback_details.sql +38 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_records.sql +45 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_llm.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_metadata.sql +44 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_request_tags.sql +42 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_requests.sql +89 -0
- package/skills/investigating-agentforce-d360/assets/dc/gateway_responses.sql +43 -0
- package/skills/investigating-agentforce-d360/assets/dc/generations.sql +52 -0
- package/skills/investigating-agentforce-d360/assets/dc/interactions.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messages.sql +53 -0
- package/skills/investigating-agentforce-d360/assets/dc/messaging_session.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/moment_interactions.sql +34 -0
- package/skills/investigating-agentforce-d360/assets/dc/moments.sql +39 -0
- package/skills/investigating-agentforce-d360/assets/dc/participants.sql +48 -0
- package/skills/investigating-agentforce-d360/assets/dc/sessions.sql +78 -0
- package/skills/investigating-agentforce-d360/assets/dc/steps.sql +64 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_associations.sql +46 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definition_associations.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/tag_definitions.sql +50 -0
- package/skills/investigating-agentforce-d360/assets/dc/tags.sql +37 -0
- package/skills/investigating-agentforce-d360/assets/dc/telemetry_spans.sql +55 -0
- package/skills/investigating-agentforce-d360/references/artifacts.md +50 -0
- package/skills/investigating-agentforce-d360/references/dc_dmo_fields.md +823 -0
- package/skills/investigating-agentforce-d360/references/dc_pipeline_contract.md +608 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/__init__.py +2 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/cli_override.py +98 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/fs_guard.py +334 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/paths.py +155 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/runtime.py +59 -0
- package/skills/investigating-agentforce-d360/scripts/_shared/sql.py +14 -0
- package/skills/investigating-agentforce-d360/scripts/assemble_dc.py +1624 -0
- package/skills/investigating-agentforce-d360/scripts/config.py +45 -0
- package/skills/investigating-agentforce-d360/scripts/dc.py +188 -0
- package/skills/investigating-agentforce-d360/scripts/discover_sessions.py +556 -0
- package/skills/investigating-agentforce-d360/scripts/fetch_dc.py +1045 -0
- package/skills/investigating-agentforce-d360/scripts/render_dc.py +1750 -0
- package/skills/investigating-agentforce-d360/scripts/resolve_session.py +264 -0
- package/skills/investigating-agentforce-d360/scripts/storage.py +92 -0
- package/skills/investigating-agentforce-d360/scripts/tests/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/_bootstrap.py +15 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/__init__.py +0 -0
- package/skills/investigating-agentforce-d360/scripts/tests/fixtures/synthetic_session.py +424 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_bootstrap_and_mode.py +115 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_gateway_direct_integration.py +158 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_helpers.py +287 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_assemble_dc_integration.py +247 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_dc_and_resolve_session.py +433 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions.py +458 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_discover_sessions_grep_ci.py +193 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_helpers.py +266 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_identity.py +528 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_main.py +251 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall.py +229 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_fetch_dc_waterfall_full.py +283 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_identity_coherence.py +327 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_branches.py +256 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_gateway_direct.py +130 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_helpers.py +291 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_integration.py +220 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_planner_llm_calls.py +284 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_render_dc_show_prompts_gating.py +215 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_from_disk.py +100 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_resolve_session_main.py +149 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_runtime_override.py +104 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape.py +95 -0
- package/skills/investigating-agentforce-d360/scripts/tests/test_session_shape_dropped_by_stdm.py +85 -0
- package/skills/managing-managed-event-subscription/SKILL.md +152 -0
- package/skills/managing-managed-event-subscription/assets/managed-event-subscription-template.xml +20 -0
- package/skills/managing-managed-event-subscription/references/delete-guide.md +57 -0
- package/skills/managing-managed-event-subscription/references/topic-name-formats.md +26 -0
- package/skills/managing-managed-event-subscription/references/update-constraints.md +30 -0
- package/skills/modeling-omnistudio-epc-catalog/SKILL.md +0 -1
- package/skills/observing-agentforce/SKILL.md +0 -1
- package/skills/orchestrating-datacloud/SKILL.md +0 -1
- package/skills/preparing-datacloud/SKILL.md +0 -1
- package/skills/querying-soql/SKILL.md +0 -1
- package/skills/retrieving-datacloud/SKILL.md +0 -1
- package/skills/running-apex-tests/SKILL.md +0 -1
- package/skills/running-code-analyzer/SKILL.md +0 -1
- package/skills/segmenting-datacloud/SKILL.md +0 -1
- package/skills/testing-agentforce/SKILL.md +0 -1
- package/skills/uplifting-components-to-slds2/SKILL.md +3 -2
- package/skills/uplifting-components-to-slds2/references/color-hooks-decision-guide.md +30 -9
- package/skills/uplifting-components-to-slds2/references/examples.md +24 -6
- package/skills/validating-slds/SKILL.md +262 -0
- package/skills/validating-slds/references/quality-checks.md +308 -0
- package/skills/validating-slds/references/report-format.md +302 -0
- 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())
|