create-yss-spec 1.1.2 → 1.1.3
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/template/.codex/skills/data-analytics/.app.json +84 -0
- package/template/.codex/skills/data-analytics/.codex-plugin/plugin.json +66 -0
- package/template/.codex/skills/data-analytics/.mcp.json +30 -0
- package/template/.codex/skills/data-analytics/AGENTS.md +20 -0
- package/template/.codex/skills/data-analytics/DEPENDENCIES.MD +27 -0
- package/template/.codex/skills/data-analytics/README.md +60 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part005 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-artifact-widget.html.gz.b64.part006 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part005 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part006 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-chart-widget.html.gz.b64.part007 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-small.svg +5 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html +16 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part001 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part002 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part003 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience-table-widget.html.gz.b64.part004 +1 -0
- package/template/.codex/skills/data-analytics/assets/datascience.png +0 -0
- package/template/.codex/skills/data-analytics/assets/datascience.svg +10 -0
- package/template/.codex/skills/data-analytics/mcp/server.cjs +2963 -0
- package/template/.codex/skills/data-analytics/package-lock.json +3048 -0
- package/template/.codex/skills/data-analytics/package.json +43 -0
- package/template/.codex/skills/data-analytics/scripts/normalize-widget-assets.mjs +75 -0
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/SKILL.md +160 -0
- package/template/.codex/skills/data-analytics/skills/analyze-data-quality/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/SKILL.md +148 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/bi-platform-dashboard.md +18 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/html-dashboard.md +24 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/mcp-artifact-dashboard.md +71 -0
- package/template/.codex/skills/data-analytics/skills/build-dashboard/specifications/streamlit-dashboard.md +85 -0
- package/template/.codex/skills/data-analytics/skills/build-report/SKILL.md +207 -0
- package/template/.codex/skills/data-analytics/skills/build-report/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/assets/executive-report-shell.html +70 -0
- package/template/.codex/skills/data-analytics/skills/build-report/assets/technical-report-shell.html +66 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/SKILL.md +118 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/__init__.py +1 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/cli.py +57 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/constants.py +61 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/docx_writer.py +362 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/html_parser.py +829 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/model.py +30 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/plan.py +129 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/quality.py +374 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/rendering.py +613 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/table_utils.py +54 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc/utils.py +16 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-doc/scripts/report_to_google_doc_plan.py +9 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/SKILL.md +78 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-google-slides/scripts/report_to_google_slides.py +2379 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/SKILL.md +88 -0
- package/template/.codex/skills/data-analytics/skills/build-report/report-to-pdf/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/executive-report.md +97 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/mcp-app-report.md +59 -0
- package/template/.codex/skills/data-analytics/skills/build-report/specifications/technical-report.md +75 -0
- package/template/.codex/skills/data-analytics/skills/design-kpis/SKILL.md +103 -0
- package/template/.codex/skills/data-analytics/skills/design-kpis/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/gather-business-context/SKILL.md +68 -0
- package/template/.codex/skills/data-analytics/skills/gather-business-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/index/SKILL.md +251 -0
- package/template/.codex/skills/data-analytics/skills/index/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/SKILL.md +131 -0
- package/template/.codex/skills/data-analytics/skills/jupyter-notebooks/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/SKILL.md +141 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/kpi-reporting/references/report-templates.md +32 -0
- package/template/.codex/skills/data-analytics/skills/market-sizing/SKILL.md +106 -0
- package/template/.codex/skills/data-analytics/skills/market-sizing/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/SKILL.md +130 -0
- package/template/.codex/skills/data-analytics/skills/metric-diagnostics/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/SKILL.md +141 -0
- package/template/.codex/skills/data-analytics/skills/product-business-analysis/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/SKILL.md +178 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/agents/openai.yaml +9 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/assets/file-spreadsheet.png +0 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/charts.md +31 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/references/artifact_tool_api.md +466 -0
- package/template/.codex/skills/data-analytics/skills/spreadsheets/style_guidelines.md +99 -0
- package/template/.codex/skills/data-analytics/skills/user-context/SKILL.md +195 -0
- package/template/.codex/skills/data-analytics/skills/user-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/automation-config.md +26 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/source-category-config.json +51 -0
- package/template/.codex/skills/data-analytics/skills/user-context/plugin-author-config/user-context-config.md +66 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/automation.md +69 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-examples.md +204 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding-state-template.json +87 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/onboarding.md +497 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/connector-playbook.md +74 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/setup.md +65 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/skill-template.md +160 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/source-intake.md +75 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/semantic-layer/weekly-polling-automation.md +96 -0
- package/template/.codex/skills/data-analytics/skills/user-context/references/source-category-runtime.md +263 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/data_analytics_preflight.py +1460 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/init_user_context_state.py +128 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/reset_user_context_state.py +101 -0
- package/template/.codex/skills/data-analytics/skills/user-context/scripts/validate_user_context_preflight.py +499 -0
- package/template/.codex/skills/data-analytics/skills/user-context/tests/test_state_helpers.py +978 -0
- package/template/.codex/skills/data-analytics/skills/validate-data/SKILL.md +190 -0
- package/template/.codex/skills/data-analytics/skills/validate-data/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/SKILL.md +157 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/agents/openai.yaml +6 -0
- package/template/.codex/skills/data-analytics/skills/visualize-data/references/seaborn-templates.md +774 -0
- package/template/.codex/skills/data-analytics/src/DESIGN.md +222 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/App.tsx +3666 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/analytics-layout.test.mjs +136 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartFrame.tsx +54 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartLegend.tsx +96 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartRenderer.tsx +1647 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/ChartTooltip.tsx +245 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-app-helpers.tsx +462 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-capabilities.ts +107 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-compatibility.ts +164 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-contract.ts +192 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-theme.ts +203 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-tokens.css +619 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/charting/chart-transforms.ts +402 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/fonts/SystemSansVariableVF.woff2 +0 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/imageExport.ts +370 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/AnalyticsLayoutCanvas.tsx +536 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/RichMarkdown.tsx +681 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/layout/analyticsLayoutCore.ts +164 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/main.tsx +21 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/chart_contract.py +472 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/check_analytics_app_runtime_links.py +75 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/design_contract.py +142 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/package_utils.py +142 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/scripts/tests/test_package_utils.py +377 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/styles.css +3468 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.d.ts +44 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/DataTable.jsx +639 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tables/data-table.css +327 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/tokens.css +225 -0
- package/template/.codex/skills/data-analytics/src/analytics-app/types.ts +204 -0
- package/template/.codex/skills/data-analytics/src/analytics-app-core.md +85 -0
- package/template/.codex/skills/data-analytics/src/codex-style-contract.md +30 -0
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.html +12 -0
- package/template/.codex/skills/data-analytics/src/datascience-artifact-widget.jsx +1008 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.css +2117 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.html +122 -0
- package/template/.codex/skills/data-analytics/src/datascience-chart-widget.js +3588 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.css +483 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.html +32 -0
- package/template/.codex/skills/data-analytics/src/datascience-table-widget.js +333 -0
- package/template/.codex/skills/data-analytics/src/mcp-host.js +153 -0
- package/template/.codex/skills/data-analytics/src/recharts-config.js +315 -0
- package/template/.codex/skills/data-analytics/src/recharts-renderer.jsx +294 -0
- package/template/.codex/skills/data-analytics/src/sql-source-view.css +33 -0
- package/template/.codex/skills/data-analytics/src/sql-source-view.js +157 -0
- package/template/.codex/skills/data-analytics/src/styles/codex-theme.css +121 -0
- package/template/.codex/skills/data-analytics/src/table-renderer.jsx +28 -0
- package/template/.codex/skills/data-analytics/tests/chart-transforms.test.mjs +87 -0
- package/template/.codex/skills/data-analytics/tests/funnel-smoke.html +105 -0
- package/template/.codex/skills/data-analytics/tests/inline-widget-compare.html +233 -0
- package/template/.codex/skills/data-analytics/tests/mcp-server.test.mjs +1500 -0
- package/template/.codex/skills/data-analytics/tests/native-style-contract.test.mjs +219 -0
- package/template/.codex/skills/data-analytics/tests/recharts-config.test.mjs +144 -0
- package/template/.codex/skills/data-analytics/tests/recharts-renderer.test.mjs +11 -0
- package/template/.codex/skills/data-analytics/tests/rich-markdown.test.mjs +54 -0
- package/template/.codex/skills/data-analytics/tests/widget-render-harness.html +321 -0
- package/template/.codex/skills/data-analytics/tsconfig.json +19 -0
- package/template/.codex/skills/data-analytics/vite.config.ts +46 -0
- package/template/.codex/skills/product-design/.app.json +7 -0
- package/template/.codex/skills/product-design/.codex-plugin/plugin.json +50 -0
- package/template/.codex/skills/product-design/README.md +50 -0
- package/template/.codex/skills/product-design/agents/openai.yaml +4 -0
- package/template/.codex/skills/product-design/assets/composerIcon.svg +9 -0
- package/template/.codex/skills/product-design/assets/logo.png +0 -0
- package/template/.codex/skills/product-design/package.json +11 -0
- package/template/.codex/skills/product-design/references/browser-order.md +9 -0
- package/template/.codex/skills/product-design/references/communication-protocol.md +45 -0
- package/template/.codex/skills/product-design/references/critical-overrides.md +58 -0
- package/template/.codex/skills/product-design/references/local-prototype-preflight.md +16 -0
- package/template/.codex/skills/product-design/scripts/bootstrap-prototype.mjs +147 -0
- package/template/.codex/skills/product-design/skills/audit/SKILL.md +159 -0
- package/template/.codex/skills/product-design/skills/audit/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/audit/references/design-audit-framework.md +73 -0
- package/template/.codex/skills/product-design/skills/design-qa/SKILL.md +133 -0
- package/template/.codex/skills/product-design/skills/design-qa/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/design-qa/references/qa-rubric.md +61 -0
- package/template/.codex/skills/product-design/skills/get-context/SKILL.md +81 -0
- package/template/.codex/skills/product-design/skills/get-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/ideate/SKILL.md +179 -0
- package/template/.codex/skills/product-design/skills/ideate/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/image-to-code/SKILL.md +105 -0
- package/template/.codex/skills/product-design/skills/image-to-code/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/index/SKILL.md +126 -0
- package/template/.codex/skills/product-design/skills/index/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/prototype/SKILL.md +128 -0
- package/template/.codex/skills/product-design/skills/prototype/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/prototype/references/existing-codebase-edits.md +18 -0
- package/template/.codex/skills/product-design/skills/research/SKILL.md +92 -0
- package/template/.codex/skills/product-design/skills/research/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/share/SKILL.md +41 -0
- package/template/.codex/skills/product-design/skills/share/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/url-to-code/SKILL.md +124 -0
- package/template/.codex/skills/product-design/skills/url-to-code/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/user-context/SKILL.md +147 -0
- package/template/.codex/skills/product-design/skills/user-context/agents/openai.yaml +6 -0
- package/template/.codex/skills/product-design/skills/user-context/plugin-author-config/user-context-template.md +63 -0
- package/template/.codex/skills/product-design/skills/user-context/references/onboarding.md +66 -0
- package/template/.codex/skills/product-design/skills/user-context/scripts/init_user_context.py +86 -0
- package/template/.codex/skills/product-design/skills/user-context/scripts/user_context_preflight.py +204 -0
- package/template/.codex/skills/product-design/templates/prototype/AGENTS.md +7 -0
- package/template/.codex/skills/product-design/templates/prototype/index.html +12 -0
- package/template/.codex/skills/product-design/templates/prototype/package.json +18 -0
- package/template/.codex/skills/product-design/templates/prototype/src/App.jsx +5 -0
- package/template/.codex/skills/product-design/templates/prototype/src/main.jsx +10 -0
- package/template/.codex/skills/product-design/templates/prototype/src/styles.css +13 -0
- package/template/.codex/skills/product-design/templates/prototype/vite.config.mjs +14 -0
- package/template/.codex/skills/yss-product-lifecycle/SKILL.md +3 -3
- package/template/AGENTS.md +3 -3
- package/template/docs/adr/adr-product-design-prototype-entrypoint.md +54 -0
- package/template/docs/api/templates/openapi-draft-review-checklist.md +4 -2
- package/template/docs/design/README.md +8 -4
- package/template/docs/design/templates/interaction-spec-template.md +4 -1
- package/template/docs/design/templates/prototype-confirmation-template.md +57 -0
- package/template/docs/design/templates/prototype-review-checklist.md +6 -2
- package/template/docs/design/templates/state-matrix-template.md +1 -1
- package/template/docs/process/harness-executive-blueprint.md +2 -1
- package/template/docs/process/harness-process-tailoring.md +1 -1
- package/template/docs/process/harness-work-unit-map.md +1 -1
- package/template/docs/process/lifecycle-artifact-map.md +4 -3
- package/template/docs/process/product-prototype-workflow.md +82 -0
- package/template/docs/user-guide/product-lifecycle-workflow.md +2 -2
- package/template/docs/user-guide/product-rd-lifecycle-best-practices.md +15 -10
|
@@ -0,0 +1,1460 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Read Data Analytics local state and emit a deterministic preflight payload."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
17
|
+
ONBOARDING_REFERENCE = PLUGIN_ROOT / "skills/user-context/references/onboarding.md"
|
|
18
|
+
SOURCE_CATEGORY_CONFIG_REFERENCE = (
|
|
19
|
+
PLUGIN_ROOT / "skills/user-context/plugin-author-config/source-category-config.json"
|
|
20
|
+
)
|
|
21
|
+
APP_MANIFEST_REFERENCE = PLUGIN_ROOT / ".app.json"
|
|
22
|
+
DEFAULT_MAX_CONTEXT_BYTES = 200_000
|
|
23
|
+
CORE_CATEGORY_IDS = ("structured_data", "team_communication", "company_docs")
|
|
24
|
+
ONBOARDING_RESOLVED_SOURCE_STATUSES = {
|
|
25
|
+
"declined",
|
|
26
|
+
"deferred",
|
|
27
|
+
"deferred_environment_api_limitations",
|
|
28
|
+
"not_applicable",
|
|
29
|
+
"skipped",
|
|
30
|
+
"skipped_for_now",
|
|
31
|
+
"unavailable",
|
|
32
|
+
}
|
|
33
|
+
SOURCE_CLASSIFIED_STATUSES = {
|
|
34
|
+
"active",
|
|
35
|
+
"needs_confirmation",
|
|
36
|
+
"missing",
|
|
37
|
+
*ONBOARDING_RESOLVED_SOURCE_STATUSES,
|
|
38
|
+
}
|
|
39
|
+
SOURCE_ACTION_REQUIRED_STATUSES = {"needs_confirmation", "missing"}
|
|
40
|
+
CORE_SOURCE_EXPLICIT_FALLBACK_RESOLUTIONS = {
|
|
41
|
+
"user_continued_with_known_gap",
|
|
42
|
+
"user_declined",
|
|
43
|
+
"user_deferred",
|
|
44
|
+
"user_marked_unavailable",
|
|
45
|
+
"user_skipped",
|
|
46
|
+
"user_confirmed_not_applicable",
|
|
47
|
+
}
|
|
48
|
+
SEMANTIC_SETUP_COMPLETE_STATUSES = {
|
|
49
|
+
"created",
|
|
50
|
+
"refreshed",
|
|
51
|
+
"inspected",
|
|
52
|
+
"repaired",
|
|
53
|
+
"planned",
|
|
54
|
+
"skipped",
|
|
55
|
+
"deferred",
|
|
56
|
+
"unavailable",
|
|
57
|
+
"blocked",
|
|
58
|
+
}
|
|
59
|
+
SEMANTIC_SETUP_TARGET_STATUSES = {"created", "refreshed", "inspected", "repaired", "planned"}
|
|
60
|
+
SEMANTIC_REFRESH_COMPLETE_STATUSES = {
|
|
61
|
+
"accepted",
|
|
62
|
+
"declined",
|
|
63
|
+
"deferred",
|
|
64
|
+
"unavailable",
|
|
65
|
+
"skipped",
|
|
66
|
+
"blocked",
|
|
67
|
+
}
|
|
68
|
+
HERO_PROMPT_COMPLETE_STATUSES = {"tried", "skipped", "deferred", "completed"}
|
|
69
|
+
HERO_SKILLS = ("metric-diagnostics", "product-business-analysis", "kpi-reporting")
|
|
70
|
+
SOURCE_ROUTING_PREFERENCES_HEADING = "data analytics source routing preferences"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_args() -> argparse.Namespace:
|
|
74
|
+
parser = argparse.ArgumentParser(
|
|
75
|
+
description=(
|
|
76
|
+
"Read Data Analytics user-context and onboarding-state files and return "
|
|
77
|
+
"context plus workflow control obligations as JSON."
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument("--workflow", default="ordinary")
|
|
81
|
+
parser.add_argument(
|
|
82
|
+
"--request-mode",
|
|
83
|
+
choices=("ordinary_workflow", "direct_onboarding_status", "guided_onboarding_workflow"),
|
|
84
|
+
default="ordinary_workflow",
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument("--codex-home", type=Path, default=None)
|
|
87
|
+
parser.add_argument("--state-dir", type=Path, default=None)
|
|
88
|
+
parser.add_argument("--max-context-bytes", type=int, default=DEFAULT_MAX_CONTEXT_BYTES)
|
|
89
|
+
parser.add_argument("--section", action="append", default=[])
|
|
90
|
+
return parser.parse_args()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def default_codex_home() -> Path:
|
|
94
|
+
env_home = os.environ.get("CODEX_HOME")
|
|
95
|
+
if env_home:
|
|
96
|
+
return Path(env_home).expanduser()
|
|
97
|
+
return Path.home() / ".codex"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def state_dir_from_args(args: argparse.Namespace) -> Path:
|
|
101
|
+
if args.state_dir:
|
|
102
|
+
return args.state_dir.expanduser()
|
|
103
|
+
codex_home = args.codex_home.expanduser() if args.codex_home else default_codex_home()
|
|
104
|
+
return codex_home / "state/plugins/data-analytics"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def sha256_text(text: str) -> str:
|
|
108
|
+
return hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def file_mtime(path: Path) -> str | None:
|
|
112
|
+
try:
|
|
113
|
+
return datetime.fromtimestamp(path.stat().st_mtime, tz=timezone.utc).isoformat()
|
|
114
|
+
except OSError:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def section_bounds(markdown: str) -> dict[str, tuple[int, int, str]]:
|
|
119
|
+
matches = list(re.finditer(r"(?m)^(#{1,6})\s+(.+?)\s*$", markdown))
|
|
120
|
+
bounds: dict[str, tuple[int, int, str]] = {}
|
|
121
|
+
for index, match in enumerate(matches):
|
|
122
|
+
start = match.start()
|
|
123
|
+
level = len(match.group(1))
|
|
124
|
+
end = len(markdown)
|
|
125
|
+
for next_match in matches[index + 1 :]:
|
|
126
|
+
if len(next_match.group(1)) <= level:
|
|
127
|
+
end = next_match.start()
|
|
128
|
+
break
|
|
129
|
+
heading = match.group(2).strip()
|
|
130
|
+
bounds[heading.casefold()] = (start, end, heading)
|
|
131
|
+
return bounds
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def extract_sections(markdown: str, requested: list[str]) -> list[dict[str, str]]:
|
|
135
|
+
if not requested:
|
|
136
|
+
return []
|
|
137
|
+
bounds = section_bounds(markdown)
|
|
138
|
+
sections: list[dict[str, str]] = []
|
|
139
|
+
for heading in requested:
|
|
140
|
+
bound = bounds.get(heading.casefold())
|
|
141
|
+
if not bound:
|
|
142
|
+
sections.append({"heading": heading, "status": "missing", "content": ""})
|
|
143
|
+
continue
|
|
144
|
+
start, end, canonical_heading = bound
|
|
145
|
+
sections.append(
|
|
146
|
+
{
|
|
147
|
+
"heading": canonical_heading,
|
|
148
|
+
"status": "present",
|
|
149
|
+
"content": markdown[start:end].strip(),
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
return sections
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def normalize_source_name(value: Any) -> str:
|
|
156
|
+
return re.sub(r"[^a-z0-9]+", "", str(value or "").casefold())
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_source_preference_values(raw_value: str) -> list[str]:
|
|
160
|
+
return [value.strip() for value in raw_value.split(",") if value.strip()]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def extract_saved_source_preferences(
|
|
164
|
+
markdown: str | None,
|
|
165
|
+
source_category_config: dict[str, dict[str, Any]],
|
|
166
|
+
) -> dict[str, dict[str, list[str]]]:
|
|
167
|
+
"""Read durable Data Analytics source-routing choices from user-context.md."""
|
|
168
|
+
if not markdown:
|
|
169
|
+
return {}
|
|
170
|
+
bounds = section_bounds(markdown)
|
|
171
|
+
source_routing_section = bounds.get(SOURCE_ROUTING_PREFERENCES_HEADING)
|
|
172
|
+
if not source_routing_section:
|
|
173
|
+
return {}
|
|
174
|
+
start, end, _ = source_routing_section
|
|
175
|
+
section = markdown[start:end]
|
|
176
|
+
preferences: dict[str, dict[str, list[str]]] = {}
|
|
177
|
+
current_category: str | None = None
|
|
178
|
+
for raw_line in section.splitlines():
|
|
179
|
+
line = raw_line.strip()
|
|
180
|
+
category_match = re.match(r"^##\s+([a-z0-9_]+)\s*$", line)
|
|
181
|
+
if category_match:
|
|
182
|
+
category_id = category_match.group(1)
|
|
183
|
+
current_category = category_id if category_id in source_category_config else None
|
|
184
|
+
continue
|
|
185
|
+
if not current_category:
|
|
186
|
+
continue
|
|
187
|
+
preference_match = re.match(r"^-\s*(Prefer|Avoid):\s*(.*?)\s*$", line)
|
|
188
|
+
if not preference_match:
|
|
189
|
+
continue
|
|
190
|
+
disposition = "preferred" if preference_match.group(1) == "Prefer" else "avoid"
|
|
191
|
+
configured_sources = {
|
|
192
|
+
normalize_source_name(str(source)): str(source)
|
|
193
|
+
for source in preferred_source_routes(source_category_config[current_category])
|
|
194
|
+
}
|
|
195
|
+
for source_name in parse_source_preference_values(preference_match.group(2)):
|
|
196
|
+
configured_source = configured_sources.get(normalize_source_name(source_name))
|
|
197
|
+
if not configured_source:
|
|
198
|
+
continue
|
|
199
|
+
category_preferences = preferences.setdefault(current_category, {})
|
|
200
|
+
values = category_preferences.setdefault(disposition, [])
|
|
201
|
+
if configured_source not in values:
|
|
202
|
+
values.append(configured_source)
|
|
203
|
+
return preferences
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def summarize_user_context(
|
|
207
|
+
markdown: str | None,
|
|
208
|
+
source_category_config: dict[str, dict[str, Any]],
|
|
209
|
+
) -> dict[str, Any]:
|
|
210
|
+
"""Return the narrow normalized Data Analytics user-context surface."""
|
|
211
|
+
return {
|
|
212
|
+
"scope": "source_routing_preferences_and_semantic_layer_registry",
|
|
213
|
+
"source_routing_preferences": extract_saved_source_preferences(
|
|
214
|
+
markdown,
|
|
215
|
+
source_category_config,
|
|
216
|
+
),
|
|
217
|
+
"semantic_layer_count": len(semantic_layer_registry(markdown)),
|
|
218
|
+
"normalization_complete": markdown is not None,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def read_text_payload(path: Path, max_context_bytes: int, sections: list[str]) -> dict[str, Any]:
|
|
223
|
+
payload: dict[str, Any] = {
|
|
224
|
+
"path": str(path),
|
|
225
|
+
"status": "missing",
|
|
226
|
+
"bytes": 0,
|
|
227
|
+
"sha256": None,
|
|
228
|
+
"mtime": None,
|
|
229
|
+
"content": None,
|
|
230
|
+
"sections": [],
|
|
231
|
+
"omitted": False,
|
|
232
|
+
"error": None,
|
|
233
|
+
}
|
|
234
|
+
if not path.exists():
|
|
235
|
+
return payload
|
|
236
|
+
try:
|
|
237
|
+
text = path.read_text(encoding="utf-8")
|
|
238
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
239
|
+
payload["status"] = "unreadable"
|
|
240
|
+
payload["error"] = str(exc)
|
|
241
|
+
return payload
|
|
242
|
+
|
|
243
|
+
encoded_len = len(text.encode("utf-8"))
|
|
244
|
+
payload.update(
|
|
245
|
+
{
|
|
246
|
+
"status": "present",
|
|
247
|
+
"bytes": encoded_len,
|
|
248
|
+
"sha256": sha256_text(text),
|
|
249
|
+
"mtime": file_mtime(path),
|
|
250
|
+
"sections": extract_sections(text, sections),
|
|
251
|
+
"_content": text,
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
if encoded_len <= max_context_bytes:
|
|
255
|
+
payload["content"] = text
|
|
256
|
+
else:
|
|
257
|
+
payload["omitted"] = True
|
|
258
|
+
payload["omission_reason"] = (
|
|
259
|
+
f"file exceeds max_context_bytes ({encoded_len} > {max_context_bytes})"
|
|
260
|
+
)
|
|
261
|
+
return payload
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def payload_text(payload: dict[str, Any]) -> str | None:
|
|
265
|
+
text = payload.get("_content")
|
|
266
|
+
if isinstance(text, str):
|
|
267
|
+
return text
|
|
268
|
+
content = payload.get("content")
|
|
269
|
+
return content if isinstance(content, str) else None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def public_file_payload(
|
|
273
|
+
payload: dict[str, Any],
|
|
274
|
+
*,
|
|
275
|
+
include_content: bool,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
public = {
|
|
278
|
+
key: value for key, value in payload.items() if key not in {"_content", "data"}
|
|
279
|
+
}
|
|
280
|
+
if not include_content:
|
|
281
|
+
public.pop("content", None)
|
|
282
|
+
return public
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def read_json_payload(path: Path, max_context_bytes: int) -> dict[str, Any]:
|
|
286
|
+
payload = read_text_payload(path, max_context_bytes=max_context_bytes, sections=[])
|
|
287
|
+
payload["data"] = None
|
|
288
|
+
if payload["status"] != "present":
|
|
289
|
+
return payload
|
|
290
|
+
try:
|
|
291
|
+
payload["data"] = json.loads(payload_text(payload) or path.read_text(encoding="utf-8"))
|
|
292
|
+
except json.JSONDecodeError as exc:
|
|
293
|
+
payload["status"] = "unreadable"
|
|
294
|
+
payload["error"] = f"invalid json: {exc}"
|
|
295
|
+
return payload
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def markdown_template_from_onboarding_reference(heading_name: str) -> str:
|
|
299
|
+
text = ONBOARDING_REFERENCE.read_text(encoding="utf-8")
|
|
300
|
+
heading = re.search(rf"(?m)^## {re.escape(heading_name)}\s*$", text)
|
|
301
|
+
if not heading:
|
|
302
|
+
raise RuntimeError(f"{heading_name} heading not found")
|
|
303
|
+
following = text[heading.end() :]
|
|
304
|
+
code_block = re.search(r"```md\n(.*?)\n```", following, flags=re.DOTALL)
|
|
305
|
+
if not code_block:
|
|
306
|
+
raise RuntimeError(f"{heading_name} markdown block not found")
|
|
307
|
+
return code_block.group(1).strip()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def load_source_category_config() -> dict[str, Any]:
|
|
311
|
+
return json.loads(SOURCE_CATEGORY_CONFIG_REFERENCE.read_text(encoding="utf-8"))["categories"]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def unique_strings(values: list[Any]) -> list[str]:
|
|
315
|
+
return list(dict.fromkeys(str(value) for value in values if isinstance(value, str) and value))
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def preferred_source_routes(metadata: dict[str, Any]) -> list[str]:
|
|
319
|
+
return unique_strings(
|
|
320
|
+
[
|
|
321
|
+
*(metadata.get("preferred_plugins") or []),
|
|
322
|
+
*(metadata.get("preferred_apps") or []),
|
|
323
|
+
]
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def normalize_source_kind(value: Any) -> str | None:
|
|
328
|
+
source_kind = str(value or "").casefold()
|
|
329
|
+
if source_kind in {"plugin", "app", "connector", "manual"}:
|
|
330
|
+
return source_kind
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
PLUGIN_INSTALL_EVIDENCE = {
|
|
335
|
+
"installed_plugin_skill_visible",
|
|
336
|
+
"plugin_in_available_plugins",
|
|
337
|
+
"plugin_owned_skill_visible",
|
|
338
|
+
"request_plugin_install_completed",
|
|
339
|
+
"user_confirmed_installed_plugin",
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
PLUGIN_SKILL_SURFACE_EVIDENCE = {
|
|
343
|
+
"plugin_skills_visible",
|
|
344
|
+
"plugin_owned_skill_visible",
|
|
345
|
+
"plugin_owned_skills_visible",
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
PLUGIN_TOOL_SURFACE_EVIDENCE = {
|
|
349
|
+
"callable_app_tools_visible",
|
|
350
|
+
"callable_plugin_tools_visible",
|
|
351
|
+
"plugin_tools_visible",
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
PLUGIN_FIRST_SETUP_STATUSES = {
|
|
355
|
+
"active",
|
|
356
|
+
"missing",
|
|
357
|
+
"needs_confirmation",
|
|
358
|
+
"unavailable",
|
|
359
|
+
"deferred",
|
|
360
|
+
"deferred_environment_api_limitations",
|
|
361
|
+
"skipped_for_now",
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def compact_mapping(value: Any) -> dict[str, Any] | None:
|
|
366
|
+
if not isinstance(value, dict):
|
|
367
|
+
return None
|
|
368
|
+
compact = {
|
|
369
|
+
str(key): item
|
|
370
|
+
for key, item in value.items()
|
|
371
|
+
if isinstance(key, str) and (isinstance(item, (str, int, float, bool)) or item is None)
|
|
372
|
+
}
|
|
373
|
+
return compact or None
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def default_skill_surface(source_kind: str | None, metadata: dict[str, Any]) -> str | None:
|
|
377
|
+
if source_kind == "plugin":
|
|
378
|
+
return "plugin_owned"
|
|
379
|
+
if source_kind in {"app", "connector"}:
|
|
380
|
+
if metadata.get("relevant_skills"):
|
|
381
|
+
return "data_analytics_vendored_helper"
|
|
382
|
+
return "direct_tool"
|
|
383
|
+
if source_kind == "manual":
|
|
384
|
+
return "manual"
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def default_runtime_action(source_kind: str | None, skill_surface: str | None) -> str | None:
|
|
389
|
+
if source_kind == "plugin":
|
|
390
|
+
return "use_plugin_owned_skill_or_tools"
|
|
391
|
+
if source_kind in {"app", "connector"}:
|
|
392
|
+
if skill_surface == "data_analytics_vendored_helper":
|
|
393
|
+
return "use_connector_or_app_with_data_analytics_helper_guidance"
|
|
394
|
+
return "use_direct_tool_when_workflow_needs_source"
|
|
395
|
+
if source_kind == "manual":
|
|
396
|
+
return "use_manual_or_exported_context"
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def copy_configured_route_metadata(
|
|
401
|
+
entry: dict[str, Any],
|
|
402
|
+
raw_entry: dict[str, Any],
|
|
403
|
+
metadata: dict[str, Any],
|
|
404
|
+
) -> None:
|
|
405
|
+
source_kind = normalize_source_kind(raw_entry.get("source_kind"))
|
|
406
|
+
if source_kind is None:
|
|
407
|
+
if isinstance(raw_entry.get("plugin"), dict):
|
|
408
|
+
source_kind = "plugin"
|
|
409
|
+
elif isinstance(raw_entry.get("connector"), dict):
|
|
410
|
+
source_kind = "connector"
|
|
411
|
+
elif isinstance(raw_entry.get("app"), dict):
|
|
412
|
+
source_kind = "app"
|
|
413
|
+
elif isinstance(raw_entry.get("manual"), dict) or raw_entry.get("manual_source"):
|
|
414
|
+
source_kind = "manual"
|
|
415
|
+
elif entry.get("status") == "active" and entry.get("preferred"):
|
|
416
|
+
source_kind = "connector"
|
|
417
|
+
|
|
418
|
+
if source_kind:
|
|
419
|
+
entry["source_kind"] = source_kind
|
|
420
|
+
|
|
421
|
+
for key in ("plugin", "app", "connector", "manual"):
|
|
422
|
+
compact = compact_mapping(raw_entry.get(key))
|
|
423
|
+
if compact is not None:
|
|
424
|
+
entry[key] = compact
|
|
425
|
+
|
|
426
|
+
manual_source = raw_entry.get("manual_source")
|
|
427
|
+
if isinstance(manual_source, str) and manual_source:
|
|
428
|
+
entry["manual_source"] = manual_source
|
|
429
|
+
|
|
430
|
+
skill_surface = str(raw_entry.get("skill_surface") or "") or default_skill_surface(
|
|
431
|
+
source_kind,
|
|
432
|
+
metadata,
|
|
433
|
+
)
|
|
434
|
+
if skill_surface:
|
|
435
|
+
entry["skill_surface"] = skill_surface
|
|
436
|
+
|
|
437
|
+
runtime_action = str(raw_entry.get("runtime_action") or "") or default_runtime_action(
|
|
438
|
+
source_kind,
|
|
439
|
+
skill_surface,
|
|
440
|
+
)
|
|
441
|
+
if runtime_action:
|
|
442
|
+
entry["runtime_action"] = runtime_action
|
|
443
|
+
|
|
444
|
+
for evidence_key in (
|
|
445
|
+
"installed_evidence",
|
|
446
|
+
"plugin_install_evidence",
|
|
447
|
+
"app_surface_evidence",
|
|
448
|
+
"connector_surface_evidence",
|
|
449
|
+
"skill_surface_evidence",
|
|
450
|
+
"tool_surface_evidence",
|
|
451
|
+
):
|
|
452
|
+
evidence_value = raw_entry.get(evidence_key)
|
|
453
|
+
if isinstance(evidence_value, str) and evidence_value:
|
|
454
|
+
entry[evidence_key] = evidence_value
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def copy_source_resolution_metadata(
|
|
458
|
+
entry: dict[str, Any],
|
|
459
|
+
raw_entry: dict[str, Any],
|
|
460
|
+
) -> None:
|
|
461
|
+
for key in ("resolution", "resolved_at", "resolution_note"):
|
|
462
|
+
value = raw_entry.get(key)
|
|
463
|
+
if isinstance(value, str) and value:
|
|
464
|
+
entry[key] = value
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def core_source_fallback_is_explicit(entry: dict[str, Any]) -> bool:
|
|
468
|
+
return (
|
|
469
|
+
str(entry.get("resolution") or "").casefold()
|
|
470
|
+
in CORE_SOURCE_EXPLICIT_FALLBACK_RESOLUTIONS
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def normalize_unacknowledged_core_source_fallback(
|
|
475
|
+
category_id: str,
|
|
476
|
+
entry: dict[str, Any],
|
|
477
|
+
metadata: dict[str, Any],
|
|
478
|
+
) -> None:
|
|
479
|
+
if category_id not in CORE_CATEGORY_IDS:
|
|
480
|
+
return
|
|
481
|
+
if entry.get("status") not in ONBOARDING_RESOLVED_SOURCE_STATUSES:
|
|
482
|
+
return
|
|
483
|
+
if core_source_fallback_is_explicit(entry):
|
|
484
|
+
return
|
|
485
|
+
|
|
486
|
+
recorded_status = str(entry["status"])
|
|
487
|
+
previous_setup_action = str(
|
|
488
|
+
entry.get("fallback_setup_action") or entry.get("setup_action") or ""
|
|
489
|
+
)
|
|
490
|
+
preferred_routes = preferred_source_routes(metadata)
|
|
491
|
+
entry["recorded_status"] = recorded_status
|
|
492
|
+
entry["status"] = "needs_confirmation"
|
|
493
|
+
entry["candidates"] = preferred_routes
|
|
494
|
+
entry["setup_action"] = "run_plugin_first_source_setup"
|
|
495
|
+
if previous_setup_action and previous_setup_action != entry["setup_action"]:
|
|
496
|
+
entry["fallback_setup_action"] = previous_setup_action
|
|
497
|
+
entry["setup_note"] = "core_source_requires_explicit_user_resolution_before_fallback"
|
|
498
|
+
entry["plugin_preference_order"] = preferred_routes
|
|
499
|
+
entry["setup_recovery"] = plugin_first_setup_recovery(metadata, previous_setup_action)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def compact_source_route(raw_route: Any, metadata: dict[str, Any]) -> dict[str, Any] | None:
|
|
503
|
+
if not isinstance(raw_route, dict):
|
|
504
|
+
return None
|
|
505
|
+
route: dict[str, Any] = {}
|
|
506
|
+
for key in ("status", "preferred", "name", "label", "fallback", "setup_action"):
|
|
507
|
+
value = raw_route.get(key)
|
|
508
|
+
if isinstance(value, str) and value:
|
|
509
|
+
route[key] = value
|
|
510
|
+
copy_configured_route_metadata(route, raw_route, metadata)
|
|
511
|
+
if "preferred" not in route:
|
|
512
|
+
for route_key in ("plugin", "app", "connector", "manual"):
|
|
513
|
+
raw_value = raw_route.get(route_key)
|
|
514
|
+
if isinstance(raw_value, str) and raw_value:
|
|
515
|
+
route["preferred"] = raw_value
|
|
516
|
+
break
|
|
517
|
+
if isinstance(raw_value, dict):
|
|
518
|
+
for name_key in ("name", "display_name", "id"):
|
|
519
|
+
name_value = raw_value.get(name_key)
|
|
520
|
+
if isinstance(name_value, str) and name_value:
|
|
521
|
+
route["preferred"] = name_value
|
|
522
|
+
break
|
|
523
|
+
if "preferred" in route:
|
|
524
|
+
break
|
|
525
|
+
if "runtime_action" not in route and route.get("source_kind"):
|
|
526
|
+
runtime_action = default_runtime_action(
|
|
527
|
+
str(route.get("source_kind") or ""),
|
|
528
|
+
str(route.get("skill_surface") or ""),
|
|
529
|
+
)
|
|
530
|
+
if runtime_action:
|
|
531
|
+
route["runtime_action"] = runtime_action
|
|
532
|
+
return route or None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def active_plugin_route_present(entry: dict[str, Any]) -> bool:
|
|
536
|
+
if entry.get("source_kind") == "plugin":
|
|
537
|
+
return True
|
|
538
|
+
routes = entry.get("routes")
|
|
539
|
+
if not isinstance(routes, list):
|
|
540
|
+
return False
|
|
541
|
+
return any(isinstance(route, dict) and route.get("source_kind") == "plugin" for route in routes)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def unproven_plugin_route(entry: dict[str, Any]) -> bool:
|
|
545
|
+
if entry.get("source_kind") != "plugin":
|
|
546
|
+
return False
|
|
547
|
+
installed_evidence = str(entry.get("installed_evidence") or "")
|
|
548
|
+
plugin_install_evidence = str(entry.get("plugin_install_evidence") or "")
|
|
549
|
+
if installed_evidence in PLUGIN_INSTALL_EVIDENCE:
|
|
550
|
+
return False
|
|
551
|
+
if plugin_install_evidence not in PLUGIN_INSTALL_EVIDENCE:
|
|
552
|
+
return True
|
|
553
|
+
if entry.get("skill_surface") == "plugin_owned":
|
|
554
|
+
skill_surface_evidence = str(entry.get("skill_surface_evidence") or "")
|
|
555
|
+
if skill_surface_evidence not in PLUGIN_SKILL_SURFACE_EVIDENCE:
|
|
556
|
+
return True
|
|
557
|
+
surface_evidence = {
|
|
558
|
+
str(entry.get("app_surface_evidence") or ""),
|
|
559
|
+
str(entry.get("tool_surface_evidence") or ""),
|
|
560
|
+
str(entry.get("skill_surface_evidence") or ""),
|
|
561
|
+
}
|
|
562
|
+
return not surface_evidence.intersection(
|
|
563
|
+
PLUGIN_TOOL_SURFACE_EVIDENCE | PLUGIN_SKILL_SURFACE_EVIDENCE
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def list_from_inventory(inventory: dict[str, Any], *keys: str) -> list[str]:
|
|
568
|
+
values: list[str] = []
|
|
569
|
+
for key in keys:
|
|
570
|
+
raw = inventory.get(key)
|
|
571
|
+
if isinstance(raw, list):
|
|
572
|
+
values.extend(str(item) for item in raw if isinstance(item, str) and item)
|
|
573
|
+
return values
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def matching_configured_sources(preferred_sources: list[str], candidates: set[str]) -> list[str]:
|
|
577
|
+
return [
|
|
578
|
+
source for source in preferred_sources if normalize_source_name(source) in candidates
|
|
579
|
+
]
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def missing_source_it_options(preferred_sources: list[str]) -> list[str]:
|
|
583
|
+
source_list = ", ".join(preferred_sources) if preferred_sources else "a matching source"
|
|
584
|
+
return [
|
|
585
|
+
f"Ask IT or a workspace admin to install or enable one of: {source_list}.",
|
|
586
|
+
"Use exported, pasted, or manually supplied context for the current workflow.",
|
|
587
|
+
]
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def load_app_connector_ids() -> dict[str, str]:
|
|
591
|
+
manifest = json.loads(APP_MANIFEST_REFERENCE.read_text(encoding="utf-8"))
|
|
592
|
+
apps = manifest.get("apps") if isinstance(manifest, dict) else None
|
|
593
|
+
if not isinstance(apps, dict):
|
|
594
|
+
return {}
|
|
595
|
+
return {
|
|
596
|
+
normalize_source_name(app_name): str(metadata["id"])
|
|
597
|
+
for app_name, metadata in apps.items()
|
|
598
|
+
if isinstance(app_name, str)
|
|
599
|
+
and isinstance(metadata, dict)
|
|
600
|
+
and isinstance(metadata.get("id"), str)
|
|
601
|
+
and metadata["id"]
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def plugin_connector_ids(
|
|
606
|
+
plugin_name: str,
|
|
607
|
+
metadata: dict[str, Any],
|
|
608
|
+
app_connector_ids: dict[str, str],
|
|
609
|
+
) -> set[str]:
|
|
610
|
+
plugin_alias = normalize_source_name(plugin_name)
|
|
611
|
+
return {
|
|
612
|
+
connector_id
|
|
613
|
+
for source_name in preferred_source_routes(metadata)
|
|
614
|
+
if normalize_source_name(source_name) == plugin_alias
|
|
615
|
+
for connector_id in [app_connector_ids.get(normalize_source_name(source_name))]
|
|
616
|
+
if connector_id
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def compact_install_candidate(candidate: dict[str, Any]) -> dict[str, Any]:
|
|
621
|
+
return {
|
|
622
|
+
key: candidate[key]
|
|
623
|
+
for key in (
|
|
624
|
+
"id",
|
|
625
|
+
"name",
|
|
626
|
+
"display_name",
|
|
627
|
+
"tool_type",
|
|
628
|
+
"has_skills",
|
|
629
|
+
"app_connector_ids",
|
|
630
|
+
)
|
|
631
|
+
if key in candidate
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def match_installable_plugin_candidates(
|
|
636
|
+
source_category_config: dict[str, Any],
|
|
637
|
+
candidates: list[dict[str, Any]] | dict[str, Any],
|
|
638
|
+
app_connector_ids: dict[str, str] | None = None,
|
|
639
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
640
|
+
"""Rank installable plugins using config order, plugin slugs, and connector ids."""
|
|
641
|
+
if isinstance(candidates, dict):
|
|
642
|
+
raw_candidates = candidates.get("tools")
|
|
643
|
+
candidates = raw_candidates if isinstance(raw_candidates, list) else []
|
|
644
|
+
connector_ids = app_connector_ids if app_connector_ids is not None else load_app_connector_ids()
|
|
645
|
+
matches: dict[str, list[dict[str, Any]]] = {}
|
|
646
|
+
for category_id, metadata in source_category_config.items():
|
|
647
|
+
category_matches: list[dict[str, Any]] = []
|
|
648
|
+
for rank, plugin_name in enumerate(metadata.get("preferred_plugins") or []):
|
|
649
|
+
plugin_alias = normalize_source_name(plugin_name)
|
|
650
|
+
configured_connector_ids = plugin_connector_ids(plugin_name, metadata, connector_ids)
|
|
651
|
+
for candidate in candidates:
|
|
652
|
+
if not isinstance(candidate, dict) or candidate.get("tool_type") != "plugin":
|
|
653
|
+
continue
|
|
654
|
+
candidate_aliases = {
|
|
655
|
+
normalize_source_name(candidate.get("name")),
|
|
656
|
+
normalize_source_name(candidate.get("display_name")),
|
|
657
|
+
normalize_source_name(str(candidate.get("id") or "").split("@", 1)[0]),
|
|
658
|
+
}
|
|
659
|
+
candidate_connector_ids = {
|
|
660
|
+
str(connector_id)
|
|
661
|
+
for connector_id in candidate.get("app_connector_ids") or []
|
|
662
|
+
if isinstance(connector_id, str)
|
|
663
|
+
}
|
|
664
|
+
reasons: list[str] = []
|
|
665
|
+
if plugin_alias and plugin_alias in candidate_aliases:
|
|
666
|
+
reasons.append("plugin_name")
|
|
667
|
+
if configured_connector_ids & candidate_connector_ids:
|
|
668
|
+
reasons.append("app_connector_id")
|
|
669
|
+
if not reasons:
|
|
670
|
+
continue
|
|
671
|
+
category_matches.append(
|
|
672
|
+
{
|
|
673
|
+
"preferred_plugin": plugin_name,
|
|
674
|
+
"preference_rank": rank,
|
|
675
|
+
"match_reasons": reasons,
|
|
676
|
+
"candidate": compact_install_candidate(candidate),
|
|
677
|
+
}
|
|
678
|
+
)
|
|
679
|
+
break
|
|
680
|
+
if category_matches:
|
|
681
|
+
matches[category_id] = category_matches
|
|
682
|
+
return matches
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def plugin_first_setup_recovery(
|
|
686
|
+
metadata: dict[str, Any],
|
|
687
|
+
fallback_setup_action: str,
|
|
688
|
+
) -> dict[str, Any]:
|
|
689
|
+
recovery = {
|
|
690
|
+
"type": "plugin_first_source_setup",
|
|
691
|
+
"preferred_plugins": unique_strings(metadata.get("preferred_plugins") or []),
|
|
692
|
+
"configured_source_routes": preferred_source_routes(metadata),
|
|
693
|
+
"candidate_lookup": "functions.list_available_plugins_to_install",
|
|
694
|
+
"candidate_match": "plugin_name_slug_or_app_connector_id_intersection",
|
|
695
|
+
"install_request": "functions.request_plugin_install",
|
|
696
|
+
"install_requires_user_approval": True,
|
|
697
|
+
"fallback_setup_action": fallback_setup_action,
|
|
698
|
+
}
|
|
699
|
+
legacy_app_fallbacks = unique_strings(metadata.get("preferred_apps") or [])
|
|
700
|
+
if legacy_app_fallbacks:
|
|
701
|
+
recovery["legacy_app_fallbacks"] = legacy_app_fallbacks
|
|
702
|
+
return recovery
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def connector_confirmation_base_entry(
|
|
706
|
+
category_id: str,
|
|
707
|
+
metadata: dict[str, Any],
|
|
708
|
+
) -> dict[str, Any]:
|
|
709
|
+
preferred_plugins = unique_strings(metadata.get("preferred_plugins") or [])
|
|
710
|
+
preferred_apps = unique_strings(metadata.get("preferred_apps") or [])
|
|
711
|
+
entry: dict[str, Any] = {
|
|
712
|
+
"id": category_id,
|
|
713
|
+
"label": metadata["label"],
|
|
714
|
+
"state_scope": "onboarding_only_not_durable_connector_readiness",
|
|
715
|
+
"workflow_time_behavior": "attempt_actual_reads_only_when_a_workflow_needs_the_source",
|
|
716
|
+
"eager_read": False,
|
|
717
|
+
}
|
|
718
|
+
if preferred_plugins:
|
|
719
|
+
entry["preferred_plugins"] = preferred_plugins
|
|
720
|
+
if preferred_apps:
|
|
721
|
+
entry["preferred_apps"] = preferred_apps
|
|
722
|
+
return entry
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def onboarding_status(onboarding_state: dict[str, Any] | None) -> str:
|
|
726
|
+
if not onboarding_state:
|
|
727
|
+
return "missing"
|
|
728
|
+
return str(onboarding_state.get("status") or "active")
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def connector_categories(onboarding_state: dict[str, Any] | None) -> dict[str, dict[str, Any]]:
|
|
732
|
+
state = onboarding_state or {}
|
|
733
|
+
connector_confirmation = state.get("connector_confirmation") or {}
|
|
734
|
+
if not isinstance(connector_confirmation, dict):
|
|
735
|
+
return {}
|
|
736
|
+
categories = connector_confirmation.get("categories")
|
|
737
|
+
if isinstance(categories, dict):
|
|
738
|
+
return categories
|
|
739
|
+
return {
|
|
740
|
+
str(category_id): entry
|
|
741
|
+
for category_id, entry in connector_confirmation.items()
|
|
742
|
+
if category_id not in {"status", "last_shown", "preferences"}
|
|
743
|
+
and isinstance(entry, dict)
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def source_status(entry: dict[str, Any] | None) -> str:
|
|
748
|
+
raw_status = str((entry or {}).get("status") or "needs_confirmation").casefold()
|
|
749
|
+
aliases = {
|
|
750
|
+
"available": "needs_confirmation",
|
|
751
|
+
"blocked": "unavailable",
|
|
752
|
+
"choose_source": "needs_confirmation",
|
|
753
|
+
"connected": "active",
|
|
754
|
+
"manual": "active",
|
|
755
|
+
"needs_setup_probe": "needs_confirmation",
|
|
756
|
+
"not_available": "missing",
|
|
757
|
+
"selected": "needs_confirmation",
|
|
758
|
+
"selected_for_verification": "needs_confirmation",
|
|
759
|
+
"selected_needs_auth": "needs_confirmation",
|
|
760
|
+
}
|
|
761
|
+
return aliases.get(raw_status, raw_status)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def source_setup_action(status: str) -> str:
|
|
765
|
+
actions = {
|
|
766
|
+
"active": "none_try_on_use",
|
|
767
|
+
"needs_confirmation": "ask_user_to_choose_or_confirm_source",
|
|
768
|
+
"missing": "explain_workflow_impact_and_it_admin_options",
|
|
769
|
+
"unavailable": "ask_admin_or_manual_fallback",
|
|
770
|
+
"declined": "none_declined",
|
|
771
|
+
"skipped": "none_skipped",
|
|
772
|
+
"skipped_for_now": "none_skipped",
|
|
773
|
+
"deferred": "none_deferred",
|
|
774
|
+
"deferred_environment_api_limitations": "none_deferred",
|
|
775
|
+
"not_applicable": "none_not_applicable",
|
|
776
|
+
}
|
|
777
|
+
return actions.get(status, "ask_user_to_choose_or_confirm_source")
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
def normalized_connector_confirmation(
|
|
781
|
+
source_category_config: dict[str, Any],
|
|
782
|
+
onboarding_state: dict[str, Any] | None,
|
|
783
|
+
) -> dict[str, dict[str, Any]]:
|
|
784
|
+
raw_categories = connector_categories(onboarding_state)
|
|
785
|
+
state = onboarding_state or {}
|
|
786
|
+
raw_confirmation = state.get("connector_confirmation") or {}
|
|
787
|
+
raw_preferences = raw_confirmation.get("preferences") if isinstance(raw_confirmation, dict) else {}
|
|
788
|
+
if not isinstance(raw_preferences, dict):
|
|
789
|
+
raw_preferences = {}
|
|
790
|
+
inventory = state.get("connector_inventory") or {}
|
|
791
|
+
if not isinstance(inventory, dict):
|
|
792
|
+
inventory = {}
|
|
793
|
+
confirmation: dict[str, dict[str, Any]] = {}
|
|
794
|
+
for category_id, metadata in source_category_config.items():
|
|
795
|
+
raw_entry = raw_categories.get(category_id)
|
|
796
|
+
if raw_entry is None:
|
|
797
|
+
raw_entry = raw_preferences.get(category_id)
|
|
798
|
+
raw_entry = raw_entry if isinstance(raw_entry, dict) else {}
|
|
799
|
+
preferred_plugins = unique_strings(metadata.get("preferred_plugins") or [])
|
|
800
|
+
preferred_routes = preferred_source_routes(metadata)
|
|
801
|
+
entry = connector_confirmation_base_entry(category_id, metadata)
|
|
802
|
+
|
|
803
|
+
if raw_entry:
|
|
804
|
+
status = source_status(raw_entry)
|
|
805
|
+
if status in ONBOARDING_RESOLVED_SOURCE_STATUSES:
|
|
806
|
+
entry["status"] = status
|
|
807
|
+
entry["setup_action"] = source_setup_action(status)
|
|
808
|
+
copy_configured_route_metadata(entry, raw_entry, metadata)
|
|
809
|
+
copy_source_resolution_metadata(entry, raw_entry)
|
|
810
|
+
elif status in {"active", "needs_confirmation", "missing"}:
|
|
811
|
+
entry["status"] = status
|
|
812
|
+
preferred = raw_entry.get("preferred") or raw_entry.get("app")
|
|
813
|
+
if isinstance(preferred, str) and preferred:
|
|
814
|
+
entry["preferred"] = preferred
|
|
815
|
+
fallback = raw_entry.get("fallback") or raw_entry.get("backup")
|
|
816
|
+
if isinstance(fallback, str) and fallback:
|
|
817
|
+
entry["fallback"] = fallback
|
|
818
|
+
if status == "active":
|
|
819
|
+
raw_routes = raw_entry.get("routes")
|
|
820
|
+
if isinstance(raw_routes, list):
|
|
821
|
+
compacted_routes = [
|
|
822
|
+
route
|
|
823
|
+
for route in (
|
|
824
|
+
compact_source_route(raw_route, metadata) for raw_route in raw_routes
|
|
825
|
+
)
|
|
826
|
+
if route is not None
|
|
827
|
+
]
|
|
828
|
+
routes = [
|
|
829
|
+
route
|
|
830
|
+
for route in compacted_routes
|
|
831
|
+
if not unproven_plugin_route(route)
|
|
832
|
+
]
|
|
833
|
+
if routes:
|
|
834
|
+
entry["routes"] = routes
|
|
835
|
+
if "preferred" not in entry:
|
|
836
|
+
first_preferred = routes[0].get("preferred")
|
|
837
|
+
if isinstance(first_preferred, str) and first_preferred:
|
|
838
|
+
entry["preferred"] = first_preferred
|
|
839
|
+
elif compacted_routes:
|
|
840
|
+
entry = connector_confirmation_base_entry(category_id, metadata)
|
|
841
|
+
entry.update(
|
|
842
|
+
{
|
|
843
|
+
"status": "needs_confirmation",
|
|
844
|
+
"candidates": preferred_routes,
|
|
845
|
+
"setup_action": "run_plugin_first_source_setup",
|
|
846
|
+
"setup_note": (
|
|
847
|
+
"plugin_route_requires_plugin_install_and_surface_evidence_before_active"
|
|
848
|
+
),
|
|
849
|
+
}
|
|
850
|
+
)
|
|
851
|
+
if entry.get("status") == "active":
|
|
852
|
+
if raw_entry.get("confirmed_at"):
|
|
853
|
+
entry["confirmed_at"] = raw_entry["confirmed_at"]
|
|
854
|
+
entry["setup_action"] = "none_try_on_use"
|
|
855
|
+
copy_configured_route_metadata(entry, raw_entry, metadata)
|
|
856
|
+
copy_source_resolution_metadata(entry, raw_entry)
|
|
857
|
+
if unproven_plugin_route(entry):
|
|
858
|
+
entry = connector_confirmation_base_entry(category_id, metadata)
|
|
859
|
+
entry.update(
|
|
860
|
+
{
|
|
861
|
+
"status": "needs_confirmation",
|
|
862
|
+
"candidates": preferred_routes,
|
|
863
|
+
"setup_action": "run_plugin_first_source_setup",
|
|
864
|
+
"setup_note": (
|
|
865
|
+
"plugin_route_requires_plugin_install_and_surface_evidence_before_active"
|
|
866
|
+
),
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
elif status == "needs_confirmation":
|
|
870
|
+
candidates = raw_entry.get("candidates")
|
|
871
|
+
if not isinstance(candidates, list) or not candidates:
|
|
872
|
+
if preferred:
|
|
873
|
+
candidates = [preferred]
|
|
874
|
+
else:
|
|
875
|
+
candidates = preferred_routes
|
|
876
|
+
entry["candidates"] = unique_strings(candidates)
|
|
877
|
+
entry["setup_action"] = source_setup_action(status)
|
|
878
|
+
copy_configured_route_metadata(entry, raw_entry, metadata)
|
|
879
|
+
copy_source_resolution_metadata(entry, raw_entry)
|
|
880
|
+
else:
|
|
881
|
+
options = raw_entry.get("options")
|
|
882
|
+
if not isinstance(options, list) or not options:
|
|
883
|
+
options = preferred_routes
|
|
884
|
+
entry["options"] = unique_strings(options)
|
|
885
|
+
entry["it_admin_options"] = raw_entry.get(
|
|
886
|
+
"it_admin_options"
|
|
887
|
+
) or missing_source_it_options(entry["options"])
|
|
888
|
+
entry["setup_action"] = source_setup_action(status)
|
|
889
|
+
copy_configured_route_metadata(entry, raw_entry, metadata)
|
|
890
|
+
copy_source_resolution_metadata(entry, raw_entry)
|
|
891
|
+
else:
|
|
892
|
+
entry["status"] = "needs_confirmation"
|
|
893
|
+
entry["candidates"] = preferred_routes
|
|
894
|
+
entry["setup_action"] = source_setup_action("needs_confirmation")
|
|
895
|
+
else:
|
|
896
|
+
configured_apps = {
|
|
897
|
+
normalize_source_name(app)
|
|
898
|
+
for app in list_from_inventory(
|
|
899
|
+
inventory,
|
|
900
|
+
"active_apps",
|
|
901
|
+
"active",
|
|
902
|
+
"installed_and_active",
|
|
903
|
+
"connected_apps",
|
|
904
|
+
"available_apps",
|
|
905
|
+
"available",
|
|
906
|
+
"installed_apps",
|
|
907
|
+
"surfaced_apps",
|
|
908
|
+
)
|
|
909
|
+
}
|
|
910
|
+
available_matches = matching_configured_sources(preferred_routes, configured_apps)
|
|
911
|
+
if available_matches:
|
|
912
|
+
entry["status"] = "needs_confirmation"
|
|
913
|
+
entry["candidates"] = available_matches
|
|
914
|
+
entry["setup_action"] = source_setup_action("needs_confirmation")
|
|
915
|
+
elif inventory:
|
|
916
|
+
entry["status"] = "missing"
|
|
917
|
+
entry["options"] = preferred_routes
|
|
918
|
+
entry["it_admin_options"] = missing_source_it_options(preferred_routes)
|
|
919
|
+
entry["setup_action"] = source_setup_action("missing")
|
|
920
|
+
else:
|
|
921
|
+
entry["status"] = "needs_confirmation"
|
|
922
|
+
entry["candidates"] = preferred_routes
|
|
923
|
+
entry["setup_action"] = source_setup_action("needs_confirmation")
|
|
924
|
+
|
|
925
|
+
if entry.get("status") == "active" and raw_entry.get("status") == "manual":
|
|
926
|
+
entry.setdefault("source_kind", "manual")
|
|
927
|
+
entry.setdefault("skill_surface", "manual")
|
|
928
|
+
entry.setdefault("runtime_action", "use_manual_or_exported_context")
|
|
929
|
+
|
|
930
|
+
if "preferred" not in entry:
|
|
931
|
+
entry["preferred"] = raw_entry.get("preferred")
|
|
932
|
+
|
|
933
|
+
relevant_skills = metadata.get("relevant_skills") or []
|
|
934
|
+
if relevant_skills and str(entry.get("source_kind") or "").casefold() != "plugin":
|
|
935
|
+
entry["helper_skills"] = relevant_skills
|
|
936
|
+
entry["helper_skills_apply_when"] = ["app", "connector"]
|
|
937
|
+
|
|
938
|
+
plugin_preference_order = preferred_source_routes(metadata)
|
|
939
|
+
if (
|
|
940
|
+
preferred_plugins
|
|
941
|
+
and plugin_preference_order
|
|
942
|
+
and entry["status"] in PLUGIN_FIRST_SETUP_STATUSES
|
|
943
|
+
):
|
|
944
|
+
entry["plugin_preference_order"] = plugin_preference_order
|
|
945
|
+
if not active_plugin_route_present(entry):
|
|
946
|
+
previous_setup_action = entry.get("setup_action")
|
|
947
|
+
entry["setup_action"] = "run_plugin_first_source_setup"
|
|
948
|
+
if previous_setup_action and previous_setup_action != entry["setup_action"]:
|
|
949
|
+
entry["fallback_setup_action"] = previous_setup_action
|
|
950
|
+
entry["setup_recovery"] = plugin_first_setup_recovery(
|
|
951
|
+
metadata,
|
|
952
|
+
str(previous_setup_action or source_setup_action(entry["status"])),
|
|
953
|
+
)
|
|
954
|
+
normalize_unacknowledged_core_source_fallback(category_id, entry, metadata)
|
|
955
|
+
confirmation[category_id] = entry
|
|
956
|
+
return confirmation
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def connector_confirmation_status(
|
|
960
|
+
source_category_config: dict[str, Any],
|
|
961
|
+
onboarding_state: dict[str, Any] | None,
|
|
962
|
+
) -> str:
|
|
963
|
+
state = onboarding_state or {}
|
|
964
|
+
if not connector_categories(onboarding_state) and not state.get("connector_inventory"):
|
|
965
|
+
return "pending"
|
|
966
|
+
confirmation = normalized_connector_confirmation(source_category_config, onboarding_state)
|
|
967
|
+
statuses = [confirmation[category_id]["status"] for category_id in CORE_CATEGORY_IDS]
|
|
968
|
+
if statuses and all(status in SOURCE_CLASSIFIED_STATUSES for status in statuses):
|
|
969
|
+
return "completed"
|
|
970
|
+
if any(status for status in statuses):
|
|
971
|
+
return "in_progress"
|
|
972
|
+
return "pending"
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def connector_confirmation_action_required(
|
|
976
|
+
source_category_config: dict[str, Any],
|
|
977
|
+
onboarding_state: dict[str, Any] | None,
|
|
978
|
+
) -> bool:
|
|
979
|
+
if connector_confirmation_status(source_category_config, onboarding_state) == "pending":
|
|
980
|
+
return False
|
|
981
|
+
confirmation = normalized_connector_confirmation(source_category_config, onboarding_state)
|
|
982
|
+
return any(
|
|
983
|
+
confirmation[category_id]["status"] in SOURCE_ACTION_REQUIRED_STATUSES
|
|
984
|
+
for category_id in CORE_CATEGORY_IDS
|
|
985
|
+
)
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
def connector_setup_confirmation_status(
|
|
989
|
+
source_category_config: dict[str, Any],
|
|
990
|
+
onboarding_state: dict[str, Any] | None,
|
|
991
|
+
) -> str:
|
|
992
|
+
confirmation_status = connector_confirmation_status(source_category_config, onboarding_state)
|
|
993
|
+
if confirmation_status == "pending":
|
|
994
|
+
return "pending"
|
|
995
|
+
if connector_confirmation_action_required(source_category_config, onboarding_state):
|
|
996
|
+
return "in_progress"
|
|
997
|
+
return "completed"
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def compact_connector_setup_summary_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
1001
|
+
compact = {
|
|
1002
|
+
key: entry[key]
|
|
1003
|
+
for key in (
|
|
1004
|
+
"id",
|
|
1005
|
+
"label",
|
|
1006
|
+
"status",
|
|
1007
|
+
"preferred",
|
|
1008
|
+
"setup_action",
|
|
1009
|
+
"fallback_setup_action",
|
|
1010
|
+
"source_kind",
|
|
1011
|
+
)
|
|
1012
|
+
if key in entry
|
|
1013
|
+
}
|
|
1014
|
+
for key in ("candidates", "options"):
|
|
1015
|
+
values = entry.get(key)
|
|
1016
|
+
if isinstance(values, list) and values:
|
|
1017
|
+
compact[key] = unique_strings(values)
|
|
1018
|
+
return compact
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def build_connector_setup_summary(
|
|
1022
|
+
source_category_config: dict[str, Any],
|
|
1023
|
+
onboarding_state: dict[str, Any] | None,
|
|
1024
|
+
) -> dict[str, Any]:
|
|
1025
|
+
confirmation = normalized_connector_confirmation(source_category_config, onboarding_state)
|
|
1026
|
+
ready: list[dict[str, Any]] = []
|
|
1027
|
+
needs_attention: list[dict[str, Any]] = []
|
|
1028
|
+
fallback_or_closed: list[dict[str, Any]] = []
|
|
1029
|
+
plugin_setup_opportunities: list[dict[str, Any]] = []
|
|
1030
|
+
unresolved_core_ids: list[str] = []
|
|
1031
|
+
for category_id, entry in confirmation.items():
|
|
1032
|
+
status = entry["status"]
|
|
1033
|
+
summary_entry = compact_connector_setup_summary_entry(entry)
|
|
1034
|
+
if status == "active":
|
|
1035
|
+
ready.append(summary_entry)
|
|
1036
|
+
elif status in ONBOARDING_RESOLVED_SOURCE_STATUSES:
|
|
1037
|
+
fallback_or_closed.append(summary_entry)
|
|
1038
|
+
else:
|
|
1039
|
+
needs_attention.append(summary_entry)
|
|
1040
|
+
if entry.get("setup_action") == "run_plugin_first_source_setup":
|
|
1041
|
+
plugin_setup_opportunities.append(summary_entry)
|
|
1042
|
+
if category_id in CORE_CATEGORY_IDS and status in SOURCE_ACTION_REQUIRED_STATUSES:
|
|
1043
|
+
unresolved_core_ids.append(category_id)
|
|
1044
|
+
next_entry = needs_attention[0] if needs_attention else None
|
|
1045
|
+
return {
|
|
1046
|
+
"status": connector_setup_confirmation_status(source_category_config, onboarding_state),
|
|
1047
|
+
"classification_status": connector_confirmation_status(
|
|
1048
|
+
source_category_config,
|
|
1049
|
+
onboarding_state,
|
|
1050
|
+
),
|
|
1051
|
+
"action_required": connector_confirmation_action_required(
|
|
1052
|
+
source_category_config,
|
|
1053
|
+
onboarding_state,
|
|
1054
|
+
),
|
|
1055
|
+
"has_setup_gaps": bool(needs_attention or plugin_setup_opportunities),
|
|
1056
|
+
"ready": ready,
|
|
1057
|
+
"active": ready,
|
|
1058
|
+
"needs_attention": needs_attention,
|
|
1059
|
+
"needs_choice": needs_attention,
|
|
1060
|
+
"fallback_or_closed": fallback_or_closed,
|
|
1061
|
+
"not_set_up": fallback_or_closed,
|
|
1062
|
+
"plugin_setup_opportunities": plugin_setup_opportunities,
|
|
1063
|
+
"unresolved_core_ids": unresolved_core_ids,
|
|
1064
|
+
"core_complete": connector_setup_confirmation_status(
|
|
1065
|
+
source_category_config,
|
|
1066
|
+
onboarding_state,
|
|
1067
|
+
)
|
|
1068
|
+
== "completed",
|
|
1069
|
+
"next_action": (
|
|
1070
|
+
{
|
|
1071
|
+
"category_id": next_entry["id"],
|
|
1072
|
+
"label": next_entry["label"],
|
|
1073
|
+
"status": next_entry["status"],
|
|
1074
|
+
"preferred": next_entry["preferred"],
|
|
1075
|
+
"setup_action": next_entry["setup_action"],
|
|
1076
|
+
}
|
|
1077
|
+
if next_entry
|
|
1078
|
+
else None
|
|
1079
|
+
),
|
|
1080
|
+
"user_facing_guidance": (
|
|
1081
|
+
"Use this summary for capability or setup-status answers. During onboarding, "
|
|
1082
|
+
"resolve one highest-value source action at a time. Active sources are FYI only; "
|
|
1083
|
+
"attempt actual reads only when a workflow needs the source."
|
|
1084
|
+
),
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def semantic_layer_setup_status(onboarding_state: dict[str, Any] | None) -> str:
|
|
1089
|
+
state = onboarding_state or {}
|
|
1090
|
+
return str((state.get("semantic_layer_setup") or {}).get("status") or "pending")
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def semantic_layer_refresh_status(onboarding_state: dict[str, Any] | None) -> str:
|
|
1094
|
+
state = onboarding_state or {}
|
|
1095
|
+
return str((state.get("semantic_layer_refresh") or {}).get("status") or "pending")
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def semantic_layer_setup_resolved(onboarding_state: dict[str, Any] | None) -> bool:
|
|
1099
|
+
setup_status = semantic_layer_setup_status(onboarding_state)
|
|
1100
|
+
if setup_status not in SEMANTIC_SETUP_COMPLETE_STATUSES:
|
|
1101
|
+
return False
|
|
1102
|
+
if setup_status in SEMANTIC_SETUP_TARGET_STATUSES:
|
|
1103
|
+
return semantic_layer_refresh_status(onboarding_state) in SEMANTIC_REFRESH_COMPLETE_STATUSES
|
|
1104
|
+
return True
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def hero_prompt_state(onboarding_state: dict[str, Any] | None) -> dict[str, Any]:
|
|
1108
|
+
state = onboarding_state or {}
|
|
1109
|
+
choice = state.get("hero_prompt_choice")
|
|
1110
|
+
if isinstance(choice, dict):
|
|
1111
|
+
return choice
|
|
1112
|
+
legacy = state.get("hero_prompts")
|
|
1113
|
+
return legacy if isinstance(legacy, dict) else {}
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def first_hero_prompt_status(onboarding_state: dict[str, Any] | None) -> str:
|
|
1117
|
+
choice = hero_prompt_state(onboarding_state)
|
|
1118
|
+
status = str(choice.get("status") or "")
|
|
1119
|
+
if status in HERO_PROMPT_COMPLETE_STATUSES:
|
|
1120
|
+
return "completed"
|
|
1121
|
+
if status == "selected":
|
|
1122
|
+
return "in_progress"
|
|
1123
|
+
skill_experience = (onboarding_state or {}).get("skill_experience") or {}
|
|
1124
|
+
if any(
|
|
1125
|
+
isinstance(skill_experience.get(skill), dict)
|
|
1126
|
+
and skill_experience[skill].get("first_tried_at")
|
|
1127
|
+
for skill in HERO_SKILLS
|
|
1128
|
+
):
|
|
1129
|
+
return "completed"
|
|
1130
|
+
if (onboarding_state or {}).get("completed_hero_prompts"):
|
|
1131
|
+
return "completed"
|
|
1132
|
+
return "pending"
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
def core_onboarding_complete(
|
|
1136
|
+
source_category_config: dict[str, Any],
|
|
1137
|
+
onboarding_state: dict[str, Any] | None,
|
|
1138
|
+
) -> bool:
|
|
1139
|
+
if not onboarding_state:
|
|
1140
|
+
return False
|
|
1141
|
+
explicit_status = str(
|
|
1142
|
+
(onboarding_state.get("core_onboarding") or {}).get("status") or ""
|
|
1143
|
+
).casefold()
|
|
1144
|
+
if explicit_status in {"completed", "complete", "complete_with_fallback"}:
|
|
1145
|
+
return True
|
|
1146
|
+
return (
|
|
1147
|
+
connector_setup_confirmation_status(source_category_config, onboarding_state)
|
|
1148
|
+
== "completed"
|
|
1149
|
+
and semantic_layer_setup_resolved(onboarding_state)
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
|
|
1153
|
+
def progress_task_list(
|
|
1154
|
+
source_category_config: dict[str, Any],
|
|
1155
|
+
onboarding_state: dict[str, Any] | None,
|
|
1156
|
+
) -> list[dict[str, str]]:
|
|
1157
|
+
state = onboarding_state or {}
|
|
1158
|
+
status = onboarding_status(onboarding_state)
|
|
1159
|
+
task_specs = (
|
|
1160
|
+
(
|
|
1161
|
+
"orientation",
|
|
1162
|
+
"Orientation",
|
|
1163
|
+
(state.get("orientation") or {}).get("status") in {"shown", "completed"},
|
|
1164
|
+
),
|
|
1165
|
+
(
|
|
1166
|
+
"source_setup_confirmation",
|
|
1167
|
+
"Check main analytics sources",
|
|
1168
|
+
connector_setup_confirmation_status(source_category_config, onboarding_state)
|
|
1169
|
+
== "completed",
|
|
1170
|
+
),
|
|
1171
|
+
(
|
|
1172
|
+
"semantic_layer_setup",
|
|
1173
|
+
"Set Up Data Context",
|
|
1174
|
+
semantic_layer_setup_resolved(onboarding_state),
|
|
1175
|
+
),
|
|
1176
|
+
("hero_prompt", "Hero prompt", first_hero_prompt_status(onboarding_state) == "completed"),
|
|
1177
|
+
)
|
|
1178
|
+
found_in_progress = False
|
|
1179
|
+
task_list: list[dict[str, str]] = []
|
|
1180
|
+
for task_id, label, completed in task_specs:
|
|
1181
|
+
if completed:
|
|
1182
|
+
task_status = "completed"
|
|
1183
|
+
elif not found_in_progress and status not in {"complete", "quiet"}:
|
|
1184
|
+
task_status = "in_progress"
|
|
1185
|
+
found_in_progress = True
|
|
1186
|
+
else:
|
|
1187
|
+
task_status = "pending"
|
|
1188
|
+
task_list.append({"id": task_id, "label": label, "status": task_status})
|
|
1189
|
+
return task_list
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
def next_onboarding_action(
|
|
1193
|
+
source_category_config: dict[str, Any],
|
|
1194
|
+
onboarding_state: dict[str, Any] | None,
|
|
1195
|
+
) -> dict[str, str] | None:
|
|
1196
|
+
tasks = progress_task_list(source_category_config, onboarding_state)
|
|
1197
|
+
for task in tasks:
|
|
1198
|
+
if task["status"] == "completed":
|
|
1199
|
+
continue
|
|
1200
|
+
mapping = {
|
|
1201
|
+
"orientation": ("start_data_analytics_onboarding", "show orientation"),
|
|
1202
|
+
"source_setup_confirmation": (
|
|
1203
|
+
"confirm_analytics_sources",
|
|
1204
|
+
"confirm active or missing sources",
|
|
1205
|
+
),
|
|
1206
|
+
"semantic_layer_setup": (
|
|
1207
|
+
"introduce_semantic_layer_setup",
|
|
1208
|
+
"set up data context",
|
|
1209
|
+
),
|
|
1210
|
+
"hero_prompt": ("offer_first_hero_prompt", "run or resolve the first hero prompt"),
|
|
1211
|
+
}
|
|
1212
|
+
action_id, label = mapping[task["id"]]
|
|
1213
|
+
return {"id": action_id, "label": label}
|
|
1214
|
+
return None
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def semantic_layer_registry(user_context_text: str | None) -> list[dict[str, str]]:
|
|
1218
|
+
if not user_context_text:
|
|
1219
|
+
return []
|
|
1220
|
+
bounds = section_bounds(user_context_text)
|
|
1221
|
+
semantic_section = bounds.get("semantic layers")
|
|
1222
|
+
if not semantic_section:
|
|
1223
|
+
return []
|
|
1224
|
+
start, end, _ = semantic_section
|
|
1225
|
+
section = user_context_text[start:end]
|
|
1226
|
+
entries: list[dict[str, str]] = []
|
|
1227
|
+
for block in re.split(r"(?m)^- Area:\s*", section)[1:]:
|
|
1228
|
+
lines = block.splitlines()
|
|
1229
|
+
area = lines[0].strip()
|
|
1230
|
+
entry: dict[str, str] = {"area": area}
|
|
1231
|
+
for line in lines[1:]:
|
|
1232
|
+
match = re.match(
|
|
1233
|
+
r"\s*-\s*(Skill Name|Skill Path|Source Inventory Path|Last Updated):\s*(.+?)\s*$",
|
|
1234
|
+
line,
|
|
1235
|
+
)
|
|
1236
|
+
if match:
|
|
1237
|
+
entry[match.group(1).lower().replace(" ", "_")] = match.group(2)
|
|
1238
|
+
if "skill_path" in entry:
|
|
1239
|
+
entries.append(entry)
|
|
1240
|
+
return entries
|
|
1241
|
+
|
|
1242
|
+
|
|
1243
|
+
def saved_anchor_values(user_context_text: str | None) -> dict[str, list[str]]:
|
|
1244
|
+
if not user_context_text:
|
|
1245
|
+
return {"areas": [], "metrics": [], "dashboards": [], "tables": [], "goals": []}
|
|
1246
|
+
return {
|
|
1247
|
+
"areas": [entry["area"] for entry in semantic_layer_registry(user_context_text)],
|
|
1248
|
+
"metrics": [],
|
|
1249
|
+
"dashboards": [],
|
|
1250
|
+
"tables": [],
|
|
1251
|
+
"goals": [],
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
def build_hero_prompt_candidates(
|
|
1256
|
+
user_context_text: str | None,
|
|
1257
|
+
onboarding_state: dict[str, Any] | None,
|
|
1258
|
+
) -> list[dict[str, str]]:
|
|
1259
|
+
anchors = saved_anchor_values(user_context_text)
|
|
1260
|
+
semantic_setup = (onboarding_state or {}).get("semantic_layer_setup") or {}
|
|
1261
|
+
area = semantic_setup.get("area") or (anchors["areas"][0] if anchors["areas"] else None)
|
|
1262
|
+
metric = anchors["metrics"][0] if anchors["metrics"] else None
|
|
1263
|
+
dashboard = anchors["dashboards"][0] if anchors["dashboards"] else None
|
|
1264
|
+
goal = anchors["goals"][0] if anchors["goals"] else None
|
|
1265
|
+
target = area or goal or dashboard or metric
|
|
1266
|
+
if not target:
|
|
1267
|
+
return []
|
|
1268
|
+
|
|
1269
|
+
return [
|
|
1270
|
+
{
|
|
1271
|
+
"skill": "product-business-analysis",
|
|
1272
|
+
"label": "Build a decision-ready report",
|
|
1273
|
+
"prompt": (
|
|
1274
|
+
f"Build a decision-ready report for {target}: explain what is happening, "
|
|
1275
|
+
"identify the main drivers, recommend where the team should focus next, "
|
|
1276
|
+
"and include sources, caveats, and useful charts."
|
|
1277
|
+
),
|
|
1278
|
+
}
|
|
1279
|
+
]
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def reorder_hero_prompt_candidates(
|
|
1283
|
+
candidates: list[dict[str, str]],
|
|
1284
|
+
onboarding_state: dict[str, Any] | None,
|
|
1285
|
+
) -> list[dict[str, str]]:
|
|
1286
|
+
selected_skill = str(hero_prompt_state(onboarding_state).get("selected_skill") or "")
|
|
1287
|
+
if not selected_skill:
|
|
1288
|
+
return candidates
|
|
1289
|
+
selected = [candidate for candidate in candidates if candidate["skill"] == selected_skill]
|
|
1290
|
+
remaining = [candidate for candidate in candidates if candidate["skill"] != selected_skill]
|
|
1291
|
+
return selected + remaining
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def current_skill_experience(
|
|
1295
|
+
workflow: str,
|
|
1296
|
+
request_mode: str,
|
|
1297
|
+
onboarding_state: dict[str, Any] | None,
|
|
1298
|
+
) -> dict[str, Any]:
|
|
1299
|
+
skill_experience = (onboarding_state or {}).get("skill_experience") or {}
|
|
1300
|
+
entry = skill_experience.get(workflow) if isinstance(skill_experience, dict) else None
|
|
1301
|
+
entry = entry if isinstance(entry, dict) else {}
|
|
1302
|
+
introduced = bool(entry.get("introduced_at"))
|
|
1303
|
+
tried = bool(entry.get("first_tried_at"))
|
|
1304
|
+
dismissed = bool(entry.get("dismissed_at"))
|
|
1305
|
+
return {
|
|
1306
|
+
"skill": workflow,
|
|
1307
|
+
"introduced": introduced,
|
|
1308
|
+
"tried": tried,
|
|
1309
|
+
"dismissed": dismissed,
|
|
1310
|
+
"intro_eligible": workflow in HERO_SKILLS and not introduced and not dismissed,
|
|
1311
|
+
"cta_owner": "onboarding" if request_mode == "guided_onboarding_workflow" else "skill",
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
def final_obligations(
|
|
1316
|
+
request_mode: str,
|
|
1317
|
+
user_context_status: str,
|
|
1318
|
+
source_category_config: dict[str, Any],
|
|
1319
|
+
onboarding_state: dict[str, Any] | None,
|
|
1320
|
+
) -> list[dict[str, Any]]:
|
|
1321
|
+
if request_mode != "ordinary_workflow":
|
|
1322
|
+
return []
|
|
1323
|
+
status = onboarding_status(onboarding_state)
|
|
1324
|
+
if status in {"complete", "quiet"}:
|
|
1325
|
+
return []
|
|
1326
|
+
if user_context_status in {"missing", "unreadable"}:
|
|
1327
|
+
return [
|
|
1328
|
+
{
|
|
1329
|
+
"id": "offer_data_analytics_onboarding_next_step",
|
|
1330
|
+
"timing": "append_after_main_answer",
|
|
1331
|
+
"template": markdown_template_from_onboarding_reference(
|
|
1332
|
+
"Ordinary Workflow Onboarding CTA"
|
|
1333
|
+
),
|
|
1334
|
+
"next_action": {"id": "start_data_analytics_onboarding"},
|
|
1335
|
+
"skip_when": "the response requires a clarification or the user asked for quiet behavior",
|
|
1336
|
+
}
|
|
1337
|
+
]
|
|
1338
|
+
if not core_onboarding_complete(source_category_config, onboarding_state):
|
|
1339
|
+
return [
|
|
1340
|
+
{
|
|
1341
|
+
"id": "complete_data_analytics_core_onboarding",
|
|
1342
|
+
"timing": "append_after_main_answer",
|
|
1343
|
+
"template": markdown_template_from_onboarding_reference(
|
|
1344
|
+
"Active Core Onboarding Reminder"
|
|
1345
|
+
),
|
|
1346
|
+
"requirement": "core onboarding is not complete",
|
|
1347
|
+
"next_action": next_onboarding_action(source_category_config, onboarding_state),
|
|
1348
|
+
"skip_when": "the response requires a clarification or the user asked for quiet behavior",
|
|
1349
|
+
}
|
|
1350
|
+
]
|
|
1351
|
+
return []
|
|
1352
|
+
|
|
1353
|
+
|
|
1354
|
+
def conditional_guidance(request_mode: str, user_context_status: str) -> list[dict[str, Any]]:
|
|
1355
|
+
if request_mode != "ordinary_workflow":
|
|
1356
|
+
return []
|
|
1357
|
+
if user_context_status not in {"missing", "unreadable"}:
|
|
1358
|
+
return []
|
|
1359
|
+
return [
|
|
1360
|
+
{
|
|
1361
|
+
"id": "context_gap_note",
|
|
1362
|
+
"template": markdown_template_from_onboarding_reference("Context Gap Note"),
|
|
1363
|
+
"potential_gaps": [
|
|
1364
|
+
"Data Analytics source-routing preferences or semantic-layer registry are missing or unreadable"
|
|
1365
|
+
],
|
|
1366
|
+
}
|
|
1367
|
+
]
|
|
1368
|
+
|
|
1369
|
+
|
|
1370
|
+
def main() -> int:
|
|
1371
|
+
args = parse_args()
|
|
1372
|
+
state_dir = state_dir_from_args(args)
|
|
1373
|
+
user_context_path = state_dir / "user-context.md"
|
|
1374
|
+
onboarding_state_path = state_dir / "onboarding-state.json"
|
|
1375
|
+
|
|
1376
|
+
user_context = read_text_payload(
|
|
1377
|
+
user_context_path,
|
|
1378
|
+
max_context_bytes=args.max_context_bytes,
|
|
1379
|
+
sections=args.section,
|
|
1380
|
+
)
|
|
1381
|
+
onboarding_state = read_json_payload(
|
|
1382
|
+
onboarding_state_path,
|
|
1383
|
+
max_context_bytes=args.max_context_bytes,
|
|
1384
|
+
)
|
|
1385
|
+
onboarding_data = (
|
|
1386
|
+
onboarding_state.get("data") if onboarding_state["status"] == "present" else None
|
|
1387
|
+
)
|
|
1388
|
+
source_category_config = load_source_category_config()
|
|
1389
|
+
user_context_text = payload_text(user_context)
|
|
1390
|
+
saved_source_preferences = extract_saved_source_preferences(
|
|
1391
|
+
user_context_text,
|
|
1392
|
+
source_category_config,
|
|
1393
|
+
)
|
|
1394
|
+
connector_confirmation = normalized_connector_confirmation(
|
|
1395
|
+
source_category_config, onboarding_data
|
|
1396
|
+
)
|
|
1397
|
+
connector_setup_summary = build_connector_setup_summary(source_category_config, onboarding_data)
|
|
1398
|
+
hero_prompt_candidates = reorder_hero_prompt_candidates(
|
|
1399
|
+
build_hero_prompt_candidates(user_context_text, onboarding_data),
|
|
1400
|
+
onboarding_data,
|
|
1401
|
+
)
|
|
1402
|
+
task_list = progress_task_list(source_category_config, onboarding_data)
|
|
1403
|
+
payload = {
|
|
1404
|
+
"schema": "data_analytics_preflight.v1",
|
|
1405
|
+
"read_only": True,
|
|
1406
|
+
"workflow": args.workflow,
|
|
1407
|
+
"request_mode": args.request_mode,
|
|
1408
|
+
"state": {
|
|
1409
|
+
"state_dir": str(state_dir),
|
|
1410
|
+
"user_context_status": user_context["status"],
|
|
1411
|
+
"onboarding_state_status": onboarding_state["status"],
|
|
1412
|
+
},
|
|
1413
|
+
"files": {
|
|
1414
|
+
"user_context": public_file_payload(user_context, include_content=False),
|
|
1415
|
+
"onboarding_state": public_file_payload(onboarding_state, include_content=False),
|
|
1416
|
+
},
|
|
1417
|
+
"current_skill_experience": current_skill_experience(
|
|
1418
|
+
args.workflow,
|
|
1419
|
+
args.request_mode,
|
|
1420
|
+
onboarding_data,
|
|
1421
|
+
),
|
|
1422
|
+
"context": {
|
|
1423
|
+
"user_context": summarize_user_context(user_context_text, source_category_config),
|
|
1424
|
+
"source_preferences": saved_source_preferences,
|
|
1425
|
+
"source_category_config": source_category_config,
|
|
1426
|
+
"connector_confirmation": connector_confirmation,
|
|
1427
|
+
"connector_setup_summary": connector_setup_summary,
|
|
1428
|
+
"semantic_layers": semantic_layer_registry(user_context_text),
|
|
1429
|
+
"hero_prompt_candidates": hero_prompt_candidates,
|
|
1430
|
+
"primary_hero_prompt": hero_prompt_candidates[0] if hero_prompt_candidates else None,
|
|
1431
|
+
"extra_hero_prompt_candidates": hero_prompt_candidates[1:3],
|
|
1432
|
+
},
|
|
1433
|
+
"control": {
|
|
1434
|
+
"response_mode": args.request_mode,
|
|
1435
|
+
"final_obligations": final_obligations(
|
|
1436
|
+
args.request_mode,
|
|
1437
|
+
user_context["status"],
|
|
1438
|
+
source_category_config,
|
|
1439
|
+
onboarding_data,
|
|
1440
|
+
),
|
|
1441
|
+
"conditional_guidance": conditional_guidance(args.request_mode, user_context["status"]),
|
|
1442
|
+
"onboarding_progress": {
|
|
1443
|
+
"status": onboarding_status(onboarding_data),
|
|
1444
|
+
"core_onboarding": {
|
|
1445
|
+
"complete": core_onboarding_complete(source_category_config, onboarding_data),
|
|
1446
|
+
"remaining_step_ids": [
|
|
1447
|
+
item["id"] for item in task_list if item["status"] != "completed"
|
|
1448
|
+
],
|
|
1449
|
+
"next_action": next_onboarding_action(source_category_config, onboarding_data),
|
|
1450
|
+
},
|
|
1451
|
+
"task_list": task_list,
|
|
1452
|
+
},
|
|
1453
|
+
},
|
|
1454
|
+
}
|
|
1455
|
+
print(json.dumps(payload, indent=2))
|
|
1456
|
+
return 0
|
|
1457
|
+
|
|
1458
|
+
|
|
1459
|
+
if __name__ == "__main__":
|
|
1460
|
+
sys.exit(main())
|