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