dataface 0.1.2__py3-none-any.whl
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.
- d3_format/__init__.py +14 -0
- d3_format/errors.py +19 -0
- d3_format/format.py +551 -0
- d3_format/spec.py +159 -0
- dataface/DATAFACE_SYNTAX.md +1135 -0
- dataface/__init__.py +93 -0
- dataface/_docs_site.py +20 -0
- dataface/_install_hint.py +26 -0
- dataface/agent_api/__init__.py +79 -0
- dataface/agent_api/_init_templates/__init__.py +0 -0
- dataface/agent_api/_init_templates/agents_dft_snippet.md +26 -0
- dataface/agent_api/_init_templates/dataface.yml +15 -0
- dataface/agent_api/_init_templates/faces-dataface.yml +144 -0
- dataface/agent_api/_init_templates/index.md +24 -0
- dataface/agent_api/_paths.py +118 -0
- dataface/agent_api/_project_agents_md.py +43 -0
- dataface/agent_api/_session_store.py +486 -0
- dataface/agent_api/_state.py +28 -0
- dataface/agent_api/chat.py +221 -0
- dataface/agent_api/dashboards.py +257 -0
- dataface/agent_api/describe.py +366 -0
- dataface/agent_api/describe_query.py +120 -0
- dataface/agent_api/docs/__init__.py +25 -0
- dataface/agent_api/docs/_loader.py +292 -0
- dataface/agent_api/docs/yaml-reference.md +2757 -0
- dataface/agent_api/file_refs.py +118 -0
- dataface/agent_api/init.py +126 -0
- dataface/agent_api/inspect.py +128 -0
- dataface/agent_api/mcp_install.py +170 -0
- dataface/agent_api/query.py +274 -0
- dataface/agent_api/schema.py +658 -0
- dataface/agent_api/schema_search.py +284 -0
- dataface/agent_api/search.py +270 -0
- dataface/agent_api/skill_install.py +141 -0
- dataface/agent_api/skill_render.py +90 -0
- dataface/agent_api/skills.py +293 -0
- dataface/agent_api/surface_aliases.yaml +128 -0
- dataface/agent_api/validate.py +175 -0
- dataface/agent_api/validate_query.py +84 -0
- dataface/ai/__init__.py +39 -0
- dataface/ai/agent.py +139 -0
- dataface/ai/context.py +45 -0
- dataface/ai/events.py +62 -0
- dataface/ai/external_mcp.py +610 -0
- dataface/ai/generate_sql.py +96 -0
- dataface/ai/llm.py +403 -0
- dataface/ai/mcp/__init__.py +51 -0
- dataface/ai/mcp/server.py +289 -0
- dataface/ai/memories.py +85 -0
- dataface/ai/prompts.py +177 -0
- dataface/ai/schema_context.py +138 -0
- dataface/ai/skills/before-after-comparison/SKILL.md +102 -0
- dataface/ai/skills/before-after-comparison/examples/before-after-comparison.yml +24 -0
- dataface/ai/skills/dashboard-build/SKILL.md +212 -0
- dataface/ai/skills/dashboard-build/examples/_smoke.yml +15 -0
- dataface/ai/skills/dashboard-design/SKILL.md +182 -0
- dataface/ai/skills/dashboard-review/SKILL.md +113 -0
- dataface/ai/skills/dashboard-structural-review/SKILL.md +173 -0
- dataface/ai/skills/dashboard-visual-review/SKILL.md +139 -0
- dataface/ai/skills/dataface-mcp-setup/SKILL.md +177 -0
- dataface/ai/skills/dataface-troubleshooting/SKILL.md +225 -0
- dataface/ai/skills/drill-down-link/SKILL.md +112 -0
- dataface/ai/skills/drill-down-link/examples/drill-down-link.yml +27 -0
- dataface/ai/skills/faceted-small-multiples/SKILL.md +116 -0
- dataface/ai/skills/faceted-small-multiples/examples/faceted-small-multiples.yml +33 -0
- dataface/ai/skills/filter-bar-with-variables/SKILL.md +105 -0
- dataface/ai/skills/filter-bar-with-variables/examples/filter-bar-with-variables.yml +49 -0
- dataface/ai/skills/kpi-row/SKILL.md +101 -0
- dataface/ai/skills/kpi-row/examples/kpi-row.yml +55 -0
- dataface/ai/skills/report-design/SKILL.md +184 -0
- dataface/ai/skills/single-metric-bignum/SKILL.md +90 -0
- dataface/ai/skills/single-metric-bignum/examples/single-metric-bignum.yml +27 -0
- dataface/ai/skills/table-heavy-ops-dashboard/SKILL.md +114 -0
- dataface/ai/skills/table-heavy-ops-dashboard/examples/table-heavy-ops-dashboard.yml +48 -0
- dataface/ai/skills/time-series-trend/SKILL.md +93 -0
- dataface/ai/skills/time-series-trend/examples/time-series-trend.yml +26 -0
- dataface/ai/skills/top-n-with-detail/SKILL.md +98 -0
- dataface/ai/skills/top-n-with-detail/examples/top-n-with-detail.yml +45 -0
- dataface/ai/skills/two-by-two-grid-overview/SKILL.md +78 -0
- dataface/ai/skills/two-by-two-grid-overview/examples/two-by-two-grid-overview.yml +64 -0
- dataface/ai/tool_schemas.py +132 -0
- dataface/ai/tools/__init__.py +312 -0
- dataface/ai/yaml_utils.py +57 -0
- dataface/cli/__init__.py +3 -0
- dataface/cli/_console.py +48 -0
- dataface/cli/_error_format.py +83 -0
- dataface/cli/_extras.py +190 -0
- dataface/cli/_json_output.py +8 -0
- dataface/cli/_parsing.py +17 -0
- dataface/cli/_version_info.py +56 -0
- dataface/cli/commands/__init__.py +3 -0
- dataface/cli/commands/_agent_input.py +205 -0
- dataface/cli/commands/_agent_server.py +115 -0
- dataface/cli/commands/chat.py +645 -0
- dataface/cli/commands/describe.py +107 -0
- dataface/cli/commands/docs.py +131 -0
- dataface/cli/commands/extension.py +179 -0
- dataface/cli/commands/init.py +240 -0
- dataface/cli/commands/inspect.py +94 -0
- dataface/cli/commands/mcp_init.py +167 -0
- dataface/cli/commands/query.py +386 -0
- dataface/cli/commands/render.py +291 -0
- dataface/cli/commands/schema.py +411 -0
- dataface/cli/commands/search.py +49 -0
- dataface/cli/commands/serve.py +114 -0
- dataface/cli/commands/skills.py +133 -0
- dataface/cli/commands/skills_init.py +161 -0
- dataface/cli/commands/validate.py +63 -0
- dataface/cli/main.py +1501 -0
- dataface/core/__init__.py +75 -0
- dataface/core/compile/__init__.py +244 -0
- dataface/core/compile/_jinja_helpers.py +78 -0
- dataface/core/compile/channel.py +222 -0
- dataface/core/compile/chart_focus.py +101 -0
- dataface/core/compile/chart_resolved.py +169 -0
- dataface/core/compile/chart_type_detection.py +489 -0
- dataface/core/compile/chart_update.py +261 -0
- dataface/core/compile/colors.py +64 -0
- dataface/core/compile/compiler.py +904 -0
- dataface/core/compile/config.py +823 -0
- dataface/core/compile/custom_chart_types.py +208 -0
- dataface/core/compile/data_table_attachment.py +1287 -0
- dataface/core/compile/detect.py +110 -0
- dataface/core/compile/errors.py +302 -0
- dataface/core/compile/filter_injection.py +319 -0
- dataface/core/compile/introspection.py +527 -0
- dataface/core/compile/jinja.py +511 -0
- dataface/core/compile/labels_env.py +52 -0
- dataface/core/compile/markdown.py +154 -0
- dataface/core/compile/meta.py +388 -0
- dataface/core/compile/models/__init__.py +0 -0
- dataface/core/compile/models/chart/__init__.py +0 -0
- dataface/core/compile/models/chart/authored.py +2137 -0
- dataface/core/compile/models/chart/compiled.py +398 -0
- dataface/core/compile/models/config.py +347 -0
- dataface/core/compile/models/face/__init__.py +0 -0
- dataface/core/compile/models/face/authored.py +659 -0
- dataface/core/compile/models/face/compiled.py +522 -0
- dataface/core/compile/models/factories.py +201 -0
- dataface/core/compile/models/markers.py +40 -0
- dataface/core/compile/models/palette.py +36 -0
- dataface/core/compile/models/primitives.py +415 -0
- dataface/core/compile/models/query/__init__.py +0 -0
- dataface/core/compile/models/query/authored.py +246 -0
- dataface/core/compile/models/query/compiled.py +710 -0
- dataface/core/compile/models/refs.py +137 -0
- dataface/core/compile/models/source.py +611 -0
- dataface/core/compile/models/style/__init__.py +0 -0
- dataface/core/compile/models/style/authored.py +481 -0
- dataface/core/compile/models/style/compiled.py +3399 -0
- dataface/core/compile/models/style/merged.py +1682 -0
- dataface/core/compile/models/theme.py +362 -0
- dataface/core/compile/models/variable/__init__.py +0 -0
- dataface/core/compile/models/variable/authored.py +254 -0
- dataface/core/compile/models/vega_lite/__init__.py +0 -0
- dataface/core/compile/models/vega_lite/config.py +510 -0
- dataface/core/compile/models/vega_lite/contracts.py +171 -0
- dataface/core/compile/normalize_charts.py +494 -0
- dataface/core/compile/normalize_layout.py +1000 -0
- dataface/core/compile/normalize_queries.py +297 -0
- dataface/core/compile/normalize_variables.py +489 -0
- dataface/core/compile/normalizer.py +543 -0
- dataface/core/compile/palette.py +1100 -0
- dataface/core/compile/parameterized.py +658 -0
- dataface/core/compile/parser.py +228 -0
- dataface/core/compile/schema.py +20 -0
- dataface/core/compile/schema_renderers/__init__.py +0 -0
- dataface/core/compile/schema_renderers/json_schema.py +163 -0
- dataface/core/compile/schema_renderers/prompt.py +152 -0
- dataface/core/compile/schema_renderers/vscode_schema.py +301 -0
- dataface/core/compile/sizing.py +2126 -0
- dataface/core/compile/sources.py +518 -0
- dataface/core/compile/sql_authoring_lint.py +56 -0
- dataface/core/compile/style_cascade.py +471 -0
- dataface/core/compile/typography.py +299 -0
- dataface/core/compile/validator.py +301 -0
- dataface/core/compile/variables.py +53 -0
- dataface/core/compile/vega_config.py +98 -0
- dataface/core/compile/vega_lite/__init__.py +6 -0
- dataface/core/compile/vega_lite/validation.py +95 -0
- dataface/core/compile/yaml_error_formatter.py +838 -0
- dataface/core/connections.py +38 -0
- dataface/core/dashboard.py +358 -0
- dataface/core/defaults/default_config.yml +101 -0
- dataface/core/defaults/palettes/categorical/category-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/category-10-light.yml +43 -0
- dataface/core/defaults/palettes/categorical/category-10.yml +31 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-blue.yml +22 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-brown.yml +29 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-green.yml +20 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-orange.yml +21 -0
- dataface/core/defaults/palettes/categorical/category-6-tonal-purple.yml +20 -0
- dataface/core/defaults/palettes/categorical/editorial-10-dark.yml +32 -0
- dataface/core/defaults/palettes/categorical/editorial-10.yml +40 -0
- dataface/core/defaults/palettes/categorical/hero-6.yml +17 -0
- dataface/core/defaults/palettes/categorical/single-blue.yml +11 -0
- dataface/core/defaults/palettes/categorical/tableau.yml +20 -0
- dataface/core/defaults/palettes/data/xkcd_colors.json +3803 -0
- dataface/core/defaults/palettes/diverging/blue-red.yml +25 -0
- dataface/core/defaults/palettes/diverging/coolwarm.yml +24 -0
- dataface/core/defaults/palettes/diverging/crimson-green.yml +23 -0
- dataface/core/defaults/palettes/diverging/orange-teal.yml +23 -0
- dataface/core/defaults/palettes/diverging/sunset.yml +24 -0
- dataface/core/defaults/palettes/scaffold/dft-creams.yml +38 -0
- dataface/core/defaults/palettes/scaffold/dft-grays.yml +53 -0
- dataface/core/defaults/palettes/sequential/amber.yml +22 -0
- dataface/core/defaults/palettes/sequential/blue.yml +22 -0
- dataface/core/defaults/palettes/sequential/brown.yml +22 -0
- dataface/core/defaults/palettes/sequential/gray.yml +22 -0
- dataface/core/defaults/palettes/sequential/green.yml +22 -0
- dataface/core/defaults/palettes/sequential/purple.yml +22 -0
- dataface/core/defaults/palettes/sequential/rust.yml +22 -0
- dataface/core/defaults/palettes/sequential/teal.yml +22 -0
- dataface/core/defaults/palettes/tone/negative.yml +32 -0
- dataface/core/defaults/palettes/tone/positive.yml +22 -0
- dataface/core/defaults/palettes/tone/warning.yml +22 -0
- dataface/core/defaults/themes/_base.yaml +786 -0
- dataface/core/defaults/themes/bi.yaml +16 -0
- dataface/core/defaults/themes/carbong100.yaml +41 -0
- dataface/core/defaults/themes/cream.yaml +122 -0
- dataface/core/defaults/themes/dark.yaml +40 -0
- dataface/core/defaults/themes/diagnostics-title-angle-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-baseline.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-center.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-color-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-font-extreme.yaml +25 -0
- dataface/core/defaults/themes/diagnostics-title-left.yaml +8 -0
- dataface/core/defaults/themes/diagnostics-title-offset-extreme.yaml +9 -0
- dataface/core/defaults/themes/diagnostics-title-size-extreme.yaml +24 -0
- dataface/core/defaults/themes/diagnostics-title-weight-extreme.yaml +24 -0
- dataface/core/defaults/themes/editorial.yaml +147 -0
- dataface/core/defaults/themes/light.yaml +30 -0
- dataface/core/defaults/themes/looker.yaml +17 -0
- dataface/core/defaults/themes/stark.yaml +134 -0
- dataface/core/errors/__init__.py +67 -0
- dataface/core/errors/codes_compile.py +56 -0
- dataface/core/errors/codes_execute.py +177 -0
- dataface/core/errors/codes_render.py +106 -0
- dataface/core/errors/codes_unknown.py +15 -0
- dataface/core/errors/hints.py +74 -0
- dataface/core/errors/registry.py +42 -0
- dataface/core/errors/structured.py +92 -0
- dataface/core/execute/__init__.py +91 -0
- dataface/core/execute/adapters/__init__.py +49 -0
- dataface/core/execute/adapters/adapter_registry.py +400 -0
- dataface/core/execute/adapters/base.py +245 -0
- dataface/core/execute/adapters/csv_adapter.py +239 -0
- dataface/core/execute/adapters/dbt_adapter.py +283 -0
- dataface/core/execute/adapters/dbt_adapter_factory.py +212 -0
- dataface/core/execute/adapters/dbt_macro_loader.py +95 -0
- dataface/core/execute/adapters/dbt_utils.py +150 -0
- dataface/core/execute/adapters/http_adapter.py +224 -0
- dataface/core/execute/adapters/metricflow_adapter.py +94 -0
- dataface/core/execute/adapters/schema_resolver_adapter.py +144 -0
- dataface/core/execute/adapters/sql_adapter.py +710 -0
- dataface/core/execute/adapters/values_adapter.py +58 -0
- dataface/core/execute/batch.py +744 -0
- dataface/core/execute/cache_backend.py +135 -0
- dataface/core/execute/cache_keys.py +66 -0
- dataface/core/execute/dbt_jinja.py +21 -0
- dataface/core/execute/dialects/__init__.py +121 -0
- dataface/core/execute/dialects/athena.py +75 -0
- dataface/core/execute/dialects/base.py +302 -0
- dataface/core/execute/dialects/bigquery.py +38 -0
- dataface/core/execute/dialects/databricks.py +68 -0
- dataface/core/execute/dialects/duckdb.py +35 -0
- dataface/core/execute/dialects/mysql.py +68 -0
- dataface/core/execute/dialects/postgres.py +39 -0
- dataface/core/execute/dialects/redshift.py +12 -0
- dataface/core/execute/dialects/snowflake.py +51 -0
- dataface/core/execute/dialects/sqlserver.py +92 -0
- dataface/core/execute/duckdb_cache.py +712 -0
- dataface/core/execute/duckdb_config.py +26 -0
- dataface/core/execute/errors.py +213 -0
- dataface/core/execute/executor.py +1249 -0
- dataface/core/execute/parallel.py +162 -0
- dataface/core/execute/setup_sql.py +58 -0
- dataface/core/execute/source_registry.py +72 -0
- dataface/core/execute/source_resolver.py +255 -0
- dataface/core/execute/sql_guard.py +387 -0
- dataface/core/execute/sql_literals.py +199 -0
- dataface/core/fonts.py +52 -0
- dataface/core/inspect/__init__.py +32 -0
- dataface/core/inspect/cache_factory.py +98 -0
- dataface/core/inspect/db_types.py +162 -0
- dataface/core/inspect/dbt_schema.py +96 -0
- dataface/core/inspect/defaults.yml +37 -0
- dataface/core/inspect/fanout_risk.py +109 -0
- dataface/core/inspect/manifest_utils.py +77 -0
- dataface/core/inspect/partials/categorical.yml +40 -0
- dataface/core/inspect/partials/date.yml +40 -0
- dataface/core/inspect/partials/numeric.yml +55 -0
- dataface/core/inspect/partition_types.py +38 -0
- dataface/core/inspect/query_validator.py +975 -0
- dataface/core/inspect/renderer.py +354 -0
- dataface/core/inspect/resolver.py +808 -0
- dataface/core/inspect/search.py +461 -0
- dataface/core/inspect/sources/__init__.py +32 -0
- dataface/core/inspect/sources/dbt.py +738 -0
- dataface/core/inspect/sources/duckdb_utils.py +66 -0
- dataface/core/inspect/templates/__init__.py +1 -0
- dataface/core/inspect/templates/categorical_column.yml +196 -0
- dataface/core/inspect/templates/charts.yml +109 -0
- dataface/core/inspect/templates/date_column.yml +248 -0
- dataface/core/inspect/templates/model.yml +138 -0
- dataface/core/inspect/templates/numeric_column.yml +261 -0
- dataface/core/inspect/templates/quality.yml +80 -0
- dataface/core/inspect/templates/string_column.yml +263 -0
- dataface/core/project_roots.py +165 -0
- dataface/core/render/__init__.py +87 -0
- dataface/core/render/board_links.py +176 -0
- dataface/core/render/chart/__init__.py +27 -0
- dataface/core/render/chart/arc_attached_table.py +251 -0
- dataface/core/render/chart/artifacts.py +16 -0
- dataface/core/render/chart/callout.py +225 -0
- dataface/core/render/chart/decisions.py +358 -0
- dataface/core/render/chart/geo.py +700 -0
- dataface/core/render/chart/kpi.py +916 -0
- dataface/core/render/chart/labels.py +76 -0
- dataface/core/render/chart/pipeline.py +818 -0
- dataface/core/render/chart/presentation.py +36 -0
- dataface/core/render/chart/profile.py +3438 -0
- dataface/core/render/chart/render_single.py +347 -0
- dataface/core/render/chart/renderers.py +193 -0
- dataface/core/render/chart/rendering.py +565 -0
- dataface/core/render/chart/serialization.py +90 -0
- dataface/core/render/chart/spark.py +496 -0
- dataface/core/render/chart/spark_bar.py +370 -0
- dataface/core/render/chart/spec_builders.py +154 -0
- dataface/core/render/chart/standard_renderer.py +2645 -0
- dataface/core/render/chart/table.py +2957 -0
- dataface/core/render/chart/table_support.py +1452 -0
- dataface/core/render/chart/tick_values.py +66 -0
- dataface/core/render/chart/time_unit_detect.py +809 -0
- dataface/core/render/chart/title_overflow.py +157 -0
- dataface/core/render/chart/type_inference.py +122 -0
- dataface/core/render/chart/validation.py +99 -0
- dataface/core/render/chart/vega_lite.py +125 -0
- dataface/core/render/chart/vega_lite_types.py +268 -0
- dataface/core/render/chart/vl_field_maps.py +346 -0
- dataface/core/render/chart_interactivity.py +24 -0
- dataface/core/render/control_registry.py +287 -0
- dataface/core/render/converters/__init__.py +24 -0
- dataface/core/render/converters/chart.py +276 -0
- dataface/core/render/converters/html.py +98 -0
- dataface/core/render/converters/pdf.py +40 -0
- dataface/core/render/converters/png.py +41 -0
- dataface/core/render/errors.py +144 -0
- dataface/core/render/face_api.py +160 -0
- dataface/core/render/faces.py +1194 -0
- dataface/core/render/font_measurement.py +48 -0
- dataface/core/render/font_support.py +197 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSansTabular-Regular.woff2 +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleProportional-Regular.ttf +0 -0
- dataface/core/render/fonts/DFTSerifOldstyleTabular-Regular.ttf +0 -0
- dataface/core/render/fonts/InterVariable.ttf +0 -0
- dataface/core/render/fonts/InterVariable.woff2 +0 -0
- dataface/core/render/fonts/NOTO_COLOR_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NOTO_EMOJI_LICENSE.txt +93 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoColorEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.ttf +0 -0
- dataface/core/render/fonts/NotoEmoji-Regular.woff2 +0 -0
- dataface/core/render/fonts/SOURCE_CODE_PRO_LICENSE.txt +93 -0
- dataface/core/render/fonts/SOURCE_SERIF_4_LICENSE.txt +98 -0
- dataface/core/render/fonts/SourceCodePro-Regular.ttf +0 -0
- dataface/core/render/fonts/SourceSerif4-Regular.ttf +0 -0
- dataface/core/render/fonts/_emoji_font_face.css +43 -0
- dataface/core/render/fonts/source-serif-4-variable-latin.woff2 +0 -0
- dataface/core/render/format_utils.py +329 -0
- dataface/core/render/geo_defaults.yml +28 -0
- dataface/core/render/json_format.py +146 -0
- dataface/core/render/layout_sizing.py +865 -0
- dataface/core/render/layouts.py +541 -0
- dataface/core/render/markdown_defaults.yml +16 -0
- dataface/core/render/missing_vars_prompt.py +79 -0
- dataface/core/render/placeholder.py +389 -0
- dataface/core/render/render_result.py +14 -0
- dataface/core/render/renderer.py +467 -0
- dataface/core/render/script_embedding.py +16 -0
- dataface/core/render/svg_utils.py +212 -0
- dataface/core/render/template_loader.py +69 -0
- dataface/core/render/templates/controls/_styles.css +606 -0
- dataface/core/render/templates/controls/checkbox.html +16 -0
- dataface/core/render/templates/controls/date.html +16 -0
- dataface/core/render/templates/controls/number.html +19 -0
- dataface/core/render/templates/controls/readonly.html +9 -0
- dataface/core/render/templates/controls/select.html +21 -0
- dataface/core/render/templates/controls/slider.html +22 -0
- dataface/core/render/templates/controls/text.html +16 -0
- dataface/core/render/templates/scripts/chart_interactivity.js +191 -0
- dataface/core/render/templates/scripts/variables.js +976 -0
- dataface/core/render/templates/svg/grid_pattern.svg +3 -0
- dataface/core/render/templates/svg/styles.css +51 -0
- dataface/core/render/terminal.py +311 -0
- dataface/core/render/terminal_charts.py +563 -0
- dataface/core/render/terminal_defaults.yml +2 -0
- dataface/core/render/terminal_layouts.py +299 -0
- dataface/core/render/terminal_text.py +31 -0
- dataface/core/render/text/__init__.py +1 -0
- dataface/core/render/text/case.py +113 -0
- dataface/core/render/text_format.py +129 -0
- dataface/core/render/utils.py +106 -0
- dataface/core/render/variable_controls.py +946 -0
- dataface/core/render/variable_input_refinement.py +140 -0
- dataface/core/render/warnings/__init__.py +15 -0
- dataface/core/render/warnings/bar_color_1_to_1_with_x.py +80 -0
- dataface/core/render/warnings/base.py +44 -0
- dataface/core/render/warnings/fanout_risk.py +15 -0
- dataface/core/render/warnings/from_query_diagnostic.py +56 -0
- dataface/core/render/warnings/missing_join_predicate.py +13 -0
- dataface/core/render/warnings/query_parse_error.py +14 -0
- dataface/core/render/warnings/query_returned_zero_rows.py +42 -0
- dataface/core/render/warnings/reaggregation.py +14 -0
- dataface/core/render/warnings/registry.py +45 -0
- dataface/core/render/warnings/suppression.py +46 -0
- dataface/core/render/warnings/temporal_single_point.py +63 -0
- dataface/core/render/warnings/unreferenced_chart.py +15 -0
- dataface/core/render/warnings/y_encoding_mostly_null.py +76 -0
- dataface/core/render/yaml_format.py +167 -0
- dataface/core/resolve_face.py +195 -0
- dataface/core/schema/__init__.py +0 -0
- dataface/core/schema/guidance.py +151 -0
- dataface/core/scoped_paths.py +59 -0
- dataface/core/serve/__init__.py +14 -0
- dataface/core/serve/bootstrap.py +39 -0
- dataface/core/serve/embedded.py +57 -0
- dataface/core/serve/port.py +129 -0
- dataface/core/serve/server.py +938 -0
- dataface/core/serve/templates/__init__.py +0 -0
- dataface/core/serve/templates/directory.yml +6 -0
- dataface/core/serve/templates/error.html.j2 +217 -0
- dataface/core/utils.py +121 -0
- dataface/core/validate.py +64 -0
- dataface/integrations/__init__.py +0 -0
- dataface/integrations/highlighting.py +351 -0
- dataface/integrations/markdown.py +537 -0
- dataface/py.typed +0 -0
- dataface-0.1.2.dist-info/METADATA +375 -0
- dataface-0.1.2.dist-info/RECORD +455 -0
- dataface-0.1.2.dist-info/WHEEL +4 -0
- dataface-0.1.2.dist-info/entry_points.txt +2 -0
- dataface-0.1.2.dist-info/licenses/LICENSE +202 -0
- mdsvg/__init__.py +168 -0
- mdsvg/fonts.py +656 -0
- mdsvg/images.py +299 -0
- mdsvg/parser.py +629 -0
- mdsvg/playground.py +284 -0
- mdsvg/py.typed +2 -0
- mdsvg/renderer.py +1623 -0
- mdsvg/style.py +355 -0
- mdsvg/types.py +200 -0
- mdsvg/utils.py +86 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Expand ``{{ s_<key> }}`` macros in wheel skill bodies.
|
|
2
|
+
|
|
3
|
+
Skill markdown is authored once with ``{{ s_<key> }}`` tokens. On the MCP
|
|
4
|
+
surface the ``mcp`` side of the alias renders; on the CLI surface the ``dft``
|
|
5
|
+
side renders. The alias table lives in ``surface_aliases.yaml`` next to this
|
|
6
|
+
module so every wheel skill and every surface points at the same source of
|
|
7
|
+
truth.
|
|
8
|
+
|
|
9
|
+
Skills that should never appear on one surface (e.g. ``dataface-mcp-setup`` is
|
|
10
|
+
CLI-only) declare ``surfaces:`` in their frontmatter; the registry hides them
|
|
11
|
+
from the wrong surface entirely. Skills shown on both surfaces use ``{{ s_key }}``
|
|
12
|
+
wherever they reference a tool/command so the prose reads naturally to both
|
|
13
|
+
audiences.
|
|
14
|
+
|
|
15
|
+
Face-YAML template tokens such as ``{{ region }}`` (no ``s_`` prefix) pass
|
|
16
|
+
through untouched — the regex only matches the ``s_`` prefix.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from functools import cache
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Literal
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
SkillSurface = Literal["mcp", "cli"]
|
|
29
|
+
|
|
30
|
+
_ALIASES_PATH = Path(__file__).resolve().parent / "surface_aliases.yaml"
|
|
31
|
+
|
|
32
|
+
# Matches `{{ s_<key> }}`. Keys are lowercase ASCII letters, digits,
|
|
33
|
+
# underscores; must start with a letter. Plain `{{ variable }}` tokens
|
|
34
|
+
# (face-YAML template tokens) do NOT match because they lack the `s_` prefix.
|
|
35
|
+
_MACRO_RE = re.compile(r"\{\{\s*s_([a-z][a-z0-9_]*)\s*\}\}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@cache
|
|
39
|
+
def _aliases() -> dict[str, dict[str, str]]:
|
|
40
|
+
"""Load and validate ``surface_aliases.yaml`` once per process."""
|
|
41
|
+
raw = yaml.safe_load(_ALIASES_PATH.read_text(encoding="utf-8")) or {}
|
|
42
|
+
if not isinstance(raw, dict):
|
|
43
|
+
raise ValueError(f"{_ALIASES_PATH}: top level must be a mapping")
|
|
44
|
+
out: dict[str, dict[str, str]] = {}
|
|
45
|
+
for key, entry in raw.items():
|
|
46
|
+
if not isinstance(entry, dict):
|
|
47
|
+
raise ValueError(f"{_ALIASES_PATH}: entry {key!r} must be a mapping")
|
|
48
|
+
unknown = set(entry) - {"mcp", "dft"}
|
|
49
|
+
if unknown:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"{_ALIASES_PATH}: entry {key!r} has unknown keys {sorted(unknown)!r}"
|
|
52
|
+
)
|
|
53
|
+
mcp = entry.get("mcp", "")
|
|
54
|
+
dft = entry.get("dft", "")
|
|
55
|
+
if not isinstance(mcp, str) or not isinstance(dft, str):
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"{_ALIASES_PATH}: entry {key!r} must have string `mcp` and `dft` values"
|
|
58
|
+
)
|
|
59
|
+
out[key] = {"mcp": mcp, "dft": dft}
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MissingSurfaceAlias(KeyError):
|
|
64
|
+
"""Raised when a SKILL.md references a macro key that has no alias entry."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def render_skill_body(body: str, *, surface: SkillSurface) -> str:
|
|
68
|
+
"""Return ``body`` with ``{{ s_key }}`` macros expanded for ``surface``.
|
|
69
|
+
|
|
70
|
+
Each ``{{ s_key }}`` resolves to the ``mcp`` or ``dft`` side of the alias
|
|
71
|
+
table based on the active surface. Unknown keys raise ``MissingSurfaceAlias``
|
|
72
|
+
so a typo fails the surface fan-out test rather than shipping an unexpanded
|
|
73
|
+
token into an agent's context window.
|
|
74
|
+
|
|
75
|
+
Face-YAML template tokens (``{{ variable }}`` without ``s_`` prefix) pass
|
|
76
|
+
through untouched.
|
|
77
|
+
"""
|
|
78
|
+
aliases = _aliases()
|
|
79
|
+
side = "mcp" if surface == "mcp" else "dft"
|
|
80
|
+
|
|
81
|
+
def repl(match: re.Match[str]) -> str:
|
|
82
|
+
key = match.group(1)
|
|
83
|
+
if key not in aliases:
|
|
84
|
+
raise MissingSurfaceAlias(
|
|
85
|
+
f"surface_aliases.yaml has no entry for {key!r} "
|
|
86
|
+
f"(referenced as {{{{ s_{key} }}}})"
|
|
87
|
+
)
|
|
88
|
+
return aliases[key][side]
|
|
89
|
+
|
|
90
|
+
return _MACRO_RE.sub(repl, body)
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""dataface.agent_api.skills — registry of agent recipes packaged with the wheel.
|
|
2
|
+
|
|
3
|
+
A skill is a directory under ``dataface/ai/skills/<name>/`` containing a
|
|
4
|
+
``SKILL.md`` file with frontmatter (``name``, ``description``, ``kind``, optional
|
|
5
|
+
``metadata``, optional ``surfaces``) and an optional ``examples/`` directory of
|
|
6
|
+
face YAML files exercised by the snapshot test in
|
|
7
|
+
``dataface/tests/agent_api/test_skills.py``.
|
|
8
|
+
|
|
9
|
+
``kind`` is ``"workflow"`` (end-to-end agent playbooks like ``dashboard-build``)
|
|
10
|
+
or ``"pattern"`` (layout recipes like ``kpi-row``). Required: the loader
|
|
11
|
+
rejects a SKILL.md that omits it so the genre stays self-documenting as new
|
|
12
|
+
skills land.
|
|
13
|
+
|
|
14
|
+
``surfaces`` is an optional list of ``"mcp"`` / ``"cli"`` declaring where the
|
|
15
|
+
skill should be exposed. Default is both. ``dataface-mcp-setup`` is CLI-only
|
|
16
|
+
because shipping setup instructions to an already-connected MCP agent is
|
|
17
|
+
worse than useless.
|
|
18
|
+
|
|
19
|
+
Skill bodies are authored once with ``{{ s_<key> }}`` macros
|
|
20
|
+
(see ``skill_render.py`` and ``surface_aliases.yaml``); the registry
|
|
21
|
+
renders them on the requested surface before returning.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from functools import cache, cached_property
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Literal
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
32
|
+
|
|
33
|
+
from dataface.agent_api.skill_render import SkillSurface, render_skill_body
|
|
34
|
+
|
|
35
|
+
_SKILLS_DIR = Path(__file__).resolve().parent.parent / "ai" / "skills"
|
|
36
|
+
|
|
37
|
+
SkillKind = Literal["workflow", "pattern"]
|
|
38
|
+
_VALID_KINDS: tuple[SkillKind, ...] = ("workflow", "pattern")
|
|
39
|
+
_VALID_SURFACES: tuple[SkillSurface, ...] = ("mcp", "cli")
|
|
40
|
+
_ALL_SURFACES: frozenset[SkillSurface] = frozenset(_VALID_SURFACES)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SkillExample(BaseModel):
|
|
44
|
+
path: Path
|
|
45
|
+
name: str # filename without extension
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SkillMetadata(BaseModel):
|
|
49
|
+
author: str | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Skill(BaseModel):
|
|
53
|
+
name: str
|
|
54
|
+
description: str
|
|
55
|
+
kind: SkillKind
|
|
56
|
+
directory: Path
|
|
57
|
+
body: str
|
|
58
|
+
surfaces: frozenset[SkillSurface] = Field(
|
|
59
|
+
default=_ALL_SURFACES,
|
|
60
|
+
description="Surfaces (`mcp`, `cli`) on which this skill is exposed.",
|
|
61
|
+
)
|
|
62
|
+
rendered_for: SkillSurface | None = Field(
|
|
63
|
+
default=None,
|
|
64
|
+
description=(
|
|
65
|
+
"Surface this body was rendered for. ``None`` on the raw parsed "
|
|
66
|
+
"skill (macros still present); set by ``list_skills`` / "
|
|
67
|
+
"``get_skill`` / ``search_skills`` to the surface they rendered."
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
examples: list[SkillExample] = Field(
|
|
71
|
+
default_factory=list, description="Example face YAML entries for this skill."
|
|
72
|
+
)
|
|
73
|
+
metadata: SkillMetadata = Field(
|
|
74
|
+
default_factory=SkillMetadata,
|
|
75
|
+
description="Parsed SKILL.md frontmatter metadata.",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@computed_field # type: ignore[prop-decorator]
|
|
79
|
+
@cached_property
|
|
80
|
+
def has_examples(self) -> bool:
|
|
81
|
+
return bool(self.examples)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class SkillList(BaseModel):
|
|
85
|
+
success: bool = True
|
|
86
|
+
skills: list[Skill] = Field(
|
|
87
|
+
default_factory=list, description="Available skills parsed from disk."
|
|
88
|
+
)
|
|
89
|
+
errors: list[str] = Field(
|
|
90
|
+
default_factory=list, description="Errors encountered while loading skills."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SkillSearchHit(BaseModel):
|
|
95
|
+
name: str
|
|
96
|
+
description: str
|
|
97
|
+
kind: SkillKind
|
|
98
|
+
has_examples: bool
|
|
99
|
+
score: float
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class SkillSearchResult(BaseModel):
|
|
103
|
+
success: bool = True
|
|
104
|
+
query: str
|
|
105
|
+
hits: list[SkillSearchHit] = Field(default_factory=list)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SkillNotFound(Exception): ...
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class GetSkillArgs(BaseModel):
|
|
112
|
+
"""Input to get_skill: fetch one skill by name."""
|
|
113
|
+
|
|
114
|
+
name: str = Field(..., description="Skill directory name (kebab-case)")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class SearchSkillsArgs(BaseModel):
|
|
118
|
+
"""Input to search_skills: substring search across names, descriptions, and bodies."""
|
|
119
|
+
|
|
120
|
+
query: str = Field(..., description="Substring query (case-insensitive)")
|
|
121
|
+
limit: int = Field(10, ge=1, le=25, description="Max hits to return")
|
|
122
|
+
|
|
123
|
+
model_config = ConfigDict(extra="forbid")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _parse_surfaces(value: object, skill_md: Path) -> frozenset[SkillSurface]:
|
|
127
|
+
"""Validate the optional ``surfaces:`` frontmatter list."""
|
|
128
|
+
if value is None:
|
|
129
|
+
return _ALL_SURFACES
|
|
130
|
+
if not isinstance(value, list) or not value:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"{skill_md}: `surfaces` must be a non-empty list (got {value!r})"
|
|
133
|
+
)
|
|
134
|
+
out: set[SkillSurface] = set()
|
|
135
|
+
for item in value:
|
|
136
|
+
if item not in _VALID_SURFACES:
|
|
137
|
+
raise ValueError(
|
|
138
|
+
f"{skill_md}: `surfaces` entry {item!r} not in {_VALID_SURFACES}"
|
|
139
|
+
)
|
|
140
|
+
out.add(item)
|
|
141
|
+
return frozenset(out)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _parse_skill_dir(directory: Path) -> Skill:
|
|
145
|
+
"""Parse a single skill directory into a Skill. Raises ValueError on bad data."""
|
|
146
|
+
skill_md = directory / "SKILL.md"
|
|
147
|
+
raw = skill_md.read_text(encoding="utf-8")
|
|
148
|
+
|
|
149
|
+
# Split frontmatter: expect leading --- block
|
|
150
|
+
if not raw.startswith("---"):
|
|
151
|
+
raise ValueError(f"{skill_md}: missing frontmatter (no leading ---)")
|
|
152
|
+
parts = raw.split("---", 2)
|
|
153
|
+
if len(parts) < 3:
|
|
154
|
+
raise ValueError(f"{skill_md}: malformed frontmatter")
|
|
155
|
+
frontmatter = yaml.safe_load(parts[1]) or {}
|
|
156
|
+
body = parts[2].lstrip("\n")
|
|
157
|
+
|
|
158
|
+
name = frontmatter.get("name")
|
|
159
|
+
if not name:
|
|
160
|
+
raise ValueError(f"{skill_md}: frontmatter missing 'name'")
|
|
161
|
+
if name != directory.name:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"{skill_md}: frontmatter name {name!r} does not match directory name {directory.name!r}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
description = frontmatter.get("description", "")
|
|
167
|
+
if not description:
|
|
168
|
+
raise ValueError(f"{skill_md}: frontmatter missing 'description'")
|
|
169
|
+
|
|
170
|
+
kind = frontmatter.get("kind")
|
|
171
|
+
if not kind:
|
|
172
|
+
raise ValueError(
|
|
173
|
+
f"{skill_md}: frontmatter missing 'kind' (must be one of {_VALID_KINDS})"
|
|
174
|
+
)
|
|
175
|
+
if kind not in _VALID_KINDS:
|
|
176
|
+
raise ValueError(
|
|
177
|
+
f"{skill_md}: invalid kind {kind!r}; must be one of {_VALID_KINDS}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
meta_raw = frontmatter.get("metadata") or {}
|
|
181
|
+
metadata = SkillMetadata(author=meta_raw.get("author"))
|
|
182
|
+
|
|
183
|
+
surfaces = _parse_surfaces(frontmatter.get("surfaces"), skill_md)
|
|
184
|
+
|
|
185
|
+
examples: list[SkillExample] = []
|
|
186
|
+
examples_dir = directory / "examples"
|
|
187
|
+
if examples_dir.is_dir():
|
|
188
|
+
for yml_path in sorted(examples_dir.glob("*.yml")):
|
|
189
|
+
examples.append(SkillExample(path=yml_path, name=yml_path.stem))
|
|
190
|
+
|
|
191
|
+
return Skill(
|
|
192
|
+
name=name,
|
|
193
|
+
description=description.strip(),
|
|
194
|
+
kind=kind,
|
|
195
|
+
directory=directory,
|
|
196
|
+
body=body,
|
|
197
|
+
surfaces=surfaces,
|
|
198
|
+
examples=examples,
|
|
199
|
+
metadata=metadata,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def all_skill_names() -> frozenset[str]:
|
|
204
|
+
"""Every skill directory name shipped in the wheel."""
|
|
205
|
+
return frozenset(_load_all())
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@cache
|
|
209
|
+
def _load_all() -> dict[str, Skill]:
|
|
210
|
+
"""Walk _SKILLS_DIR and parse every <name>/SKILL.md. Cached for process lifetime."""
|
|
211
|
+
skills: dict[str, Skill] = {}
|
|
212
|
+
for skill_md in sorted(_SKILLS_DIR.glob("*/SKILL.md")):
|
|
213
|
+
directory = skill_md.parent
|
|
214
|
+
skill = _parse_skill_dir(directory)
|
|
215
|
+
skills[skill.name] = skill
|
|
216
|
+
return skills
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _render_for_surface(skill: Skill, surface: SkillSurface) -> Skill:
|
|
220
|
+
"""Return a shallow copy of ``skill`` with body + ``rendered_for`` set."""
|
|
221
|
+
return skill.model_copy(
|
|
222
|
+
update={
|
|
223
|
+
"body": render_skill_body(skill.body, surface=surface),
|
|
224
|
+
"rendered_for": surface,
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def list_skills(*, surface: SkillSurface = "mcp") -> SkillList:
|
|
230
|
+
"""Return every skill exposed on ``surface``, with bodies rendered for it."""
|
|
231
|
+
skills = [
|
|
232
|
+
_render_for_surface(s, surface)
|
|
233
|
+
for s in _load_all().values()
|
|
234
|
+
if surface in s.surfaces
|
|
235
|
+
]
|
|
236
|
+
skills.sort(key=lambda s: s.name)
|
|
237
|
+
return SkillList(skills=skills)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_skill(name: str, *, surface: SkillSurface = "mcp") -> Skill:
|
|
241
|
+
"""Return one skill by directory name, rendered for ``surface``.
|
|
242
|
+
|
|
243
|
+
Raises ``SkillNotFound`` when the skill does not exist *or* is not exposed
|
|
244
|
+
on the requested surface (e.g. ``dataface-mcp-setup`` is CLI-only and is
|
|
245
|
+
invisible to MCP callers).
|
|
246
|
+
"""
|
|
247
|
+
skills = _load_all()
|
|
248
|
+
skill = skills.get(name)
|
|
249
|
+
if skill is None or surface not in skill.surfaces:
|
|
250
|
+
raise SkillNotFound(
|
|
251
|
+
f"Unknown skill: {name!r}. Run `dft skills` to list available."
|
|
252
|
+
)
|
|
253
|
+
return _render_for_surface(skill, surface)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def search_skills(
|
|
257
|
+
query: str, *, limit: int = 10, surface: SkillSurface = "mcp"
|
|
258
|
+
) -> SkillSearchResult:
|
|
259
|
+
"""Substring search across name (1.0), description (0.8), and rendered body (0.5).
|
|
260
|
+
|
|
261
|
+
Honors the per-skill ``surfaces`` frontmatter — a skill that isn't exposed
|
|
262
|
+
on ``surface`` is invisible to search on that surface. Body matches use the
|
|
263
|
+
surface-rendered body so macro expansion doesn't leak macro suffixes into
|
|
264
|
+
relevance ranking.
|
|
265
|
+
"""
|
|
266
|
+
q = query.strip().lower()
|
|
267
|
+
if not q:
|
|
268
|
+
raise ValueError("query must be a non-empty string")
|
|
269
|
+
|
|
270
|
+
hits: list[SkillSearchHit] = []
|
|
271
|
+
for skill in _load_all().values():
|
|
272
|
+
if surface not in skill.surfaces:
|
|
273
|
+
continue
|
|
274
|
+
if q in skill.name.lower():
|
|
275
|
+
score = 1.0
|
|
276
|
+
elif q in skill.description.lower():
|
|
277
|
+
score = 0.8
|
|
278
|
+
elif q in render_skill_body(skill.body, surface=surface).lower():
|
|
279
|
+
score = 0.5
|
|
280
|
+
else:
|
|
281
|
+
continue
|
|
282
|
+
hits.append(
|
|
283
|
+
SkillSearchHit(
|
|
284
|
+
name=skill.name,
|
|
285
|
+
description=skill.description,
|
|
286
|
+
kind=skill.kind,
|
|
287
|
+
has_examples=skill.has_examples,
|
|
288
|
+
score=score,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
hits.sort(key=lambda h: (-h.score, h.name))
|
|
293
|
+
return SkillSearchResult(query=query, hits=hits[:limit])
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Surface aliases for skill macro expansion.
|
|
2
|
+
#
|
|
3
|
+
# Authoring convention:
|
|
4
|
+
#
|
|
5
|
+
# {{ s_<key> }} renders as the `mcp` side on MCP surface, `dft` side on CLI
|
|
6
|
+
#
|
|
7
|
+
# Common patterns inside a SKILL.md:
|
|
8
|
+
#
|
|
9
|
+
# "Use `{{ s_validate_dashboard }}` after every edit."
|
|
10
|
+
# → MCP agent reads: `validate_dashboard`
|
|
11
|
+
# → CLI agent reads: `dft validate`
|
|
12
|
+
#
|
|
13
|
+
# "{{ s_render_png_example }}"
|
|
14
|
+
# → Whole code block flips between surfaces.
|
|
15
|
+
#
|
|
16
|
+
# Each top-level key here maps to an `mcp:` and `dft:` value. Either side may
|
|
17
|
+
# be an empty string when the content is one-sided.
|
|
18
|
+
#
|
|
19
|
+
# Add a new key here, then reference it as `{{ s_<key> }}` in any wheel
|
|
20
|
+
# SKILL.md. Face-YAML template tokens (`{{ region }}` etc.) are not macros —
|
|
21
|
+
# they have no `s_` prefix and pass through untouched. Test coverage lives in
|
|
22
|
+
# `dataface/tests/agent_api/test_skill_render.py` and the surface fan-out test
|
|
23
|
+
# in `dataface/tests/agent_api/test_skills.py`.
|
|
24
|
+
|
|
25
|
+
# ---- Inline tool references (one-liners inside prose) ----------------------
|
|
26
|
+
|
|
27
|
+
validate_dashboard:
|
|
28
|
+
mcp: "validate_dashboard"
|
|
29
|
+
dft: "dft validate"
|
|
30
|
+
|
|
31
|
+
execute_query:
|
|
32
|
+
mcp: "execute_query"
|
|
33
|
+
dft: "dft query"
|
|
34
|
+
|
|
35
|
+
render_dashboard:
|
|
36
|
+
mcp: "render_dashboard"
|
|
37
|
+
dft: "dft render"
|
|
38
|
+
|
|
39
|
+
query_face:
|
|
40
|
+
mcp: "query_face"
|
|
41
|
+
dft: "dft query --face"
|
|
42
|
+
|
|
43
|
+
describe_query:
|
|
44
|
+
mcp: "describe_query"
|
|
45
|
+
dft: "dft query --describe"
|
|
46
|
+
|
|
47
|
+
schema:
|
|
48
|
+
mcp: "schema"
|
|
49
|
+
dft: "dft schema"
|
|
50
|
+
|
|
51
|
+
schema_search:
|
|
52
|
+
mcp: "schema_search"
|
|
53
|
+
dft: "dft schema -s"
|
|
54
|
+
|
|
55
|
+
# ---- Inline skill-pointer references --------------------------------------
|
|
56
|
+
|
|
57
|
+
skill_build:
|
|
58
|
+
mcp: "get_skill('dashboard-build')"
|
|
59
|
+
dft: "dft skills dashboard-build"
|
|
60
|
+
|
|
61
|
+
skill_design_dashboard:
|
|
62
|
+
mcp: "get_skill('dashboard-design')"
|
|
63
|
+
dft: "dft skills dashboard-design"
|
|
64
|
+
|
|
65
|
+
skill_design_report:
|
|
66
|
+
mcp: "get_skill('report-design')"
|
|
67
|
+
dft: "dft skills report-design"
|
|
68
|
+
|
|
69
|
+
skill_review:
|
|
70
|
+
mcp: "get_skill('dashboard-review')"
|
|
71
|
+
dft: "dft skills dashboard-review"
|
|
72
|
+
|
|
73
|
+
skill_troubleshooting:
|
|
74
|
+
mcp: "get_skill('dataface-troubleshooting')"
|
|
75
|
+
dft: "dft skills dataface-troubleshooting"
|
|
76
|
+
|
|
77
|
+
# ---- Sentence-level shared phrases (same intent, different vocabulary) ----
|
|
78
|
+
|
|
79
|
+
render_to_verify:
|
|
80
|
+
mcp: "Call `render_dashboard` to see the visual result — queries are cached after first execution, so re-rendering is nearly instant."
|
|
81
|
+
dft: "Run `dft render <path>` to see the visual result — queries are cached after first execution, so re-rendering is nearly instant."
|
|
82
|
+
|
|
83
|
+
explore_schema_first:
|
|
84
|
+
mcp: "Use `schema` to drill source → schema → table → column, and `schema_search` for keyword search across the corpus."
|
|
85
|
+
dft: "Use `dft schema` to drill source → schema → table → column, and `dft schema -s <kw>` for keyword search across the corpus."
|
|
86
|
+
|
|
87
|
+
# ---- Discovery / footer blocks (whole-paragraph, both sides populated) ----
|
|
88
|
+
|
|
89
|
+
docs_discovery:
|
|
90
|
+
mcp: |
|
|
91
|
+
YAML field reference lives behind the `docs` tool (overview),
|
|
92
|
+
`docs(topic=…)` (one section in depth), and `docs(search=…)` (full-text
|
|
93
|
+
search).
|
|
94
|
+
dft: |
|
|
95
|
+
YAML field reference is `dft docs` (overview), `dft docs <topic>` (one
|
|
96
|
+
section in depth), and `dft docs -s "<query>"` (full-text search).
|
|
97
|
+
|
|
98
|
+
# ---- Code-block examples (whole fenced block, surface-specific) -----------
|
|
99
|
+
|
|
100
|
+
render_png_example:
|
|
101
|
+
mcp: |
|
|
102
|
+
```text
|
|
103
|
+
render_dashboard(path="faces/finance/revenue-overview.yml", format="png")
|
|
104
|
+
```
|
|
105
|
+
dft: |
|
|
106
|
+
```bash
|
|
107
|
+
dft render faces/finance/revenue-overview.yml --format png
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
validate_example:
|
|
111
|
+
mcp: |
|
|
112
|
+
```text
|
|
113
|
+
validate_dashboard(path="faces/finance/revenue-overview.yml")
|
|
114
|
+
```
|
|
115
|
+
dft: |
|
|
116
|
+
```bash
|
|
117
|
+
dft validate faces/finance/revenue-overview.yml
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
# ---- Cross-skill pointers (single-line footers) ---------------------------
|
|
121
|
+
|
|
122
|
+
yaml_reference_footer:
|
|
123
|
+
mcp: "Full YAML reference is the `docs` tool — call `docs` for the overview, `docs(topic=…)` for one section, `docs(search=…)` for full-text search."
|
|
124
|
+
dft: "Full YAML reference is `dft docs` — `dft docs` for the overview, `dft docs <topic>` for one section, `dft docs -s \"<query>\"` for full-text search."
|
|
125
|
+
|
|
126
|
+
review_pointer:
|
|
127
|
+
mcp: "Run `get_skill('dashboard-review')` before declaring a face done."
|
|
128
|
+
dft: "Run `dft skills dashboard-review` before declaring a face done."
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Typed validate verb — fast YAML schema + cross-reference validation.
|
|
2
|
+
|
|
3
|
+
No warehouse connection, no query execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
from dataface.agent_api._paths import (
|
|
13
|
+
no_project_hint,
|
|
14
|
+
project_root_for,
|
|
15
|
+
resolve_scoped_path,
|
|
16
|
+
)
|
|
17
|
+
from dataface.core.compile.errors import DatafaceError
|
|
18
|
+
from dataface.core.errors import DF_UNKNOWN_INTERNAL, StructuredError
|
|
19
|
+
from dataface.core.render.warnings.base import RenderWarning
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidateDashboardArgs(BaseModel):
|
|
23
|
+
"""Validate a face YAML file without executing queries.
|
|
24
|
+
|
|
25
|
+
Runs YAML parse, Pydantic schema validation, and cross-reference checks
|
|
26
|
+
(chart → query resolution, variable resolution, partial-include expansion).
|
|
27
|
+
Fast — no warehouse connection, no query execution. Use this as the inner
|
|
28
|
+
edit-loop verb: edit, validate, fix, repeat. Prefer render_dashboard for a
|
|
29
|
+
full compile + execute validation.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
path: Path = Field(
|
|
33
|
+
..., description="Path to face YAML file (relative to project_dir or cwd)"
|
|
34
|
+
)
|
|
35
|
+
project_dir: Path | None = Field(
|
|
36
|
+
None, description="Project root for resolving relative paths"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ValidateResult(BaseModel):
|
|
41
|
+
success: bool
|
|
42
|
+
path: Path
|
|
43
|
+
errors: list[StructuredError] = Field(
|
|
44
|
+
default_factory=list, description="Validation errors found in the face file."
|
|
45
|
+
)
|
|
46
|
+
warnings: list[RenderWarning] = Field(
|
|
47
|
+
default_factory=list,
|
|
48
|
+
description=(
|
|
49
|
+
"Non-fatal warnings as RenderWarning objects with stable codes. "
|
|
50
|
+
"Each warning carries code, message, and an optional fix hint."
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def validate_paths(
|
|
56
|
+
paths: list[Path] | None,
|
|
57
|
+
project_dir: Path | None = None,
|
|
58
|
+
) -> list[ValidateResult]:
|
|
59
|
+
"""Validate N face files / directories, or default ``faces/`` when empty.
|
|
60
|
+
|
|
61
|
+
Each argv path expands like the single-path verb did: a file validates
|
|
62
|
+
directly, a directory walks recursively (skipping ``_*.yml`` partials and
|
|
63
|
+
ejected inspect-template directories). Empty / ``None`` defaults to
|
|
64
|
+
``<project_root>/faces``. Results are concatenated in argv order.
|
|
65
|
+
"""
|
|
66
|
+
if not paths:
|
|
67
|
+
return _validate_one_path(None, project_dir=project_dir)
|
|
68
|
+
out: list[ValidateResult] = []
|
|
69
|
+
for p in paths:
|
|
70
|
+
out.extend(_validate_one_path(p, project_dir=project_dir))
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _validate_one_path(
|
|
75
|
+
path: Path | None,
|
|
76
|
+
project_dir: Path | None = None,
|
|
77
|
+
) -> list[ValidateResult]:
|
|
78
|
+
"""Per-argv expansion: ``None`` → faces/, file → [one], dir → walk."""
|
|
79
|
+
# WHY: dataface.core.inspect.manifest_utils triggers the inspect package
|
|
80
|
+
# __init__, which eagerly imports TableInspector + grain/quality/semantic
|
|
81
|
+
# detectors. Keep this lazy so `dft --help` doesn't pay that startup cost.
|
|
82
|
+
from dataface.core.inspect.manifest_utils import INSPECT_TEMPLATE_MANIFEST
|
|
83
|
+
|
|
84
|
+
raw_path = path if path is not None else project_root_for(project_dir) / "faces"
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
resolved = resolve_scoped_path(raw_path, project_dir)
|
|
88
|
+
except ValueError as exc:
|
|
89
|
+
return [
|
|
90
|
+
ValidateResult(
|
|
91
|
+
success=False,
|
|
92
|
+
path=raw_path,
|
|
93
|
+
errors=[
|
|
94
|
+
DatafaceError.from_code(
|
|
95
|
+
DF_UNKNOWN_INTERNAL, message=str(exc)
|
|
96
|
+
).to_structured()
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
if not resolved.is_dir():
|
|
102
|
+
return [validate(resolved, project_dir=project_dir)]
|
|
103
|
+
|
|
104
|
+
template_dirs = {m.parent for m in resolved.glob(f"**/{INSPECT_TEMPLATE_MANIFEST}")}
|
|
105
|
+
yaml_files = sorted(
|
|
106
|
+
f
|
|
107
|
+
for f in (list(resolved.glob("**/*.yml")) + list(resolved.glob("**/*.yaml")))
|
|
108
|
+
if not f.name.startswith("_") and f.parent not in template_dirs
|
|
109
|
+
)
|
|
110
|
+
if not yaml_files:
|
|
111
|
+
return [
|
|
112
|
+
ValidateResult(
|
|
113
|
+
success=False,
|
|
114
|
+
path=resolved,
|
|
115
|
+
errors=[
|
|
116
|
+
DatafaceError.from_code(
|
|
117
|
+
DF_UNKNOWN_INTERNAL,
|
|
118
|
+
message=f"No face files found in {resolved}",
|
|
119
|
+
).to_structured()
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
return [validate(f, project_dir=project_dir) for f in yaml_files]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate(path: Path, project_dir: Path | None = None) -> ValidateResult:
|
|
127
|
+
"""Fast YAML schema + cross-reference validation. No warehouse, no execute."""
|
|
128
|
+
from dataface.core.compile.compiler import compile_file
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
resolved = resolve_scoped_path(path, project_dir)
|
|
132
|
+
except ValueError as exc:
|
|
133
|
+
return ValidateResult(
|
|
134
|
+
success=False,
|
|
135
|
+
path=path,
|
|
136
|
+
errors=[
|
|
137
|
+
DatafaceError.from_code(
|
|
138
|
+
DF_UNKNOWN_INTERNAL, message=str(exc)
|
|
139
|
+
).to_structured()
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if not resolved.exists():
|
|
144
|
+
hint = no_project_hint(project_dir)
|
|
145
|
+
return ValidateResult(
|
|
146
|
+
success=False,
|
|
147
|
+
path=path,
|
|
148
|
+
errors=[
|
|
149
|
+
DatafaceError.from_code(
|
|
150
|
+
DF_UNKNOWN_INTERNAL,
|
|
151
|
+
message=f"File not found: {resolved}{hint}",
|
|
152
|
+
).to_structured()
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
result = compile_file(resolved)
|
|
158
|
+
except OSError as exc:
|
|
159
|
+
return ValidateResult(
|
|
160
|
+
success=False,
|
|
161
|
+
path=resolved,
|
|
162
|
+
errors=[
|
|
163
|
+
DatafaceError.from_code(
|
|
164
|
+
DF_UNKNOWN_INTERNAL, message=str(exc)
|
|
165
|
+
).to_structured(file=str(resolved))
|
|
166
|
+
],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
errors = list(result.errors)
|
|
170
|
+
return ValidateResult(
|
|
171
|
+
success=len(errors) == 0,
|
|
172
|
+
path=resolved,
|
|
173
|
+
errors=errors,
|
|
174
|
+
warnings=list(result.warnings),
|
|
175
|
+
)
|