osi-semantic-authoring
GitHub定义OSI核心语义模型编写规范,明确字段角色(维度、键、度量)的声明规则及结构约束,指导生成符合标准的语义文档。
Trigger Scenarios
Install
npx skills add Datus-ai/Datus-agent --skill osi-semantic-authoring -g -y
SKILL.md
Frontmatter
{
"name": "osi-semantic-authoring",
"tags": [
"semantic-model",
"osi"
],
"version": "2.3.0",
"description": "OSI core schema semantic model authoring specification — field roles, structural keys, Datus extension hints, relationships, and validation",
"allowed_agents": [
"gen_semantic_model",
"gen_metrics"
],
"user_invocable": false,
"disable_model_invocation": false
}
OSI Semantic Authoring
Describe tables as strict OSI (Open Semantic Interchange) core schema documents plus Datus business hints.
CRITICAL BOUNDARY: You author OSI core semantics only. You do NOT write MetricFlow data_source:, measures:, identifiers:, agg_time_dimension, create_metric, or any execution-engine YAML. The Datus OSI compiler lowers OSI core documents to the configured backend.
The OSI expression dialect is shown in the system prompt Workspace section. Plan the target model name and file with plan_osi_semantic_model_target after identifying the fact and dimension tables; use the returned values exactly (<osi_dialect> below stands for that dialect).
Field roles — the three-way decision
OSI core separates three column roles by structure, not by type labels:
| Role | How it is declared | Examples |
|---|---|---|
| Dimension — used for GROUP BY / filtering | A field with a dimension: block |
code columns, names, statuses, dates |
| Key — identifies rows / joins | Dataset primary_key / unique_keys, relationship from_columns/to_columns. NOT a field type. |
declared PK columns, FK join columns |
| Measure source — only aggregated | A field without a dimension: block (just name/expression/description) |
balances, amounts, quantities, precomputed rates |
The presence of the dimension: block IS the dimension declaration. A field without the block is a plain row-level expression: it documents the column and may back metric expressions, but it is not exposed for grouping (get_dimensions will not list it). NEVER write dimension: {is_time: false} to mean "this is not a time column" — omit the entire block for non-dimension fields.
What you produce
One valid OSI core document for the current business domain / semantic model scope. The authoritative document shape — object structures, field lists, version, and the Datus execution subset notes — is the OSI Core Authoring Specification section of the system prompt; author against it exactly. This skill adds the decision rules the specification cannot express: which columns become which role, and how to fix validation conflicts.
Authoring rules
- Root schema is fixed. Root keys are only
versionandsemantic_model.semantic_modelis a list. Do NOT write top-leveldatasets:,relationships:, ormetrics:. - Use OSI core dataset shape. Dataset
sourceis a string, not{table: ...}. Dataset columns arefields, notdimensions. Field expressions areexpression.dialects[], notexpr. Use the exact OSI expression dialect from the system prompt in everyexpression.dialects[].dialect. - Datus-only hints go into
custom_extensions. The only field-level hint istime_granularity(on the time field). Dataset-level:source_type: "query"for query sources. Relationships use the native OSI corenameas the stable joined-dimension path prefix; do not add a relationship alias extension. Do NOT emit fieldtypehints — roles are expressed structurally per the table above. - Semantic model boundary. One OSI
semantic_modelrepresents the current business domain. Put all related logical datasets needed by the provided SQL history in this semantic model, with relationships declared once under the semantic model object. - Canonical logical datasets. For the same source and row grain, create one canonical dataset that metrics can reference by name. Create a separate dataset only when the logical row grain or fixed business scope is genuinely different.
- Dataset
descriptionandai_contextare required.description: one concise human sentence with the business entity and row grain.ai_context.instructions: when to use the dataset, the row grain (spell out the full grain explicitly — this is where grain lives when no primary key is declared), the primary time field, important row-selection columns, relationship caveats. - Separate physical primary keys from verified logical keys.
- Write
primary_keyONLY when source metadata or an explicit data contract declares it: aPRIMARY KEYin DDL, orpk: truecolumns indescribe_table. Historical SQL and profiling must never manufacture a physical primary key. - Transcribe declared unique constraints/indexes directly into
unique_keys. - A request-SQL JOIN, ETL pattern, column name, or stated row grain may propose an ordered candidate logical key. It is not yet a key. Submit every candidate you intend to author in one
validate_semantic_key_candidates(candidates=[...])call. Add an ordered list as oneunique_keysentry only when its result hasverification_scope: full_table,is_valid_logical_key: true, andrecommended_osi_declaration: unique_keys. The scan covers rows visible under the current policy context; if row-level policy limits that context, require an explicit data contract or an unrestricted verification before declaring a global key. Mention the verification evidence inai_context.instructions. - If the table is empty, any key component is NULL, any duplicate group exists, or verification fails/cannot run, omit the candidate from both
primary_keyandunique_keys. Do not validate only one snapshot/partition and generalize it to the whole table. - ClickHouse
PRIMARY KEY/ORDER BYand StarRocksDUPLICATE KEYare sort keys, not uniqueness. Never transcribe them. A StarRocksPRIMARY KEYtable model is a true upsert key and may be transcribed.
- Write
- Field selection — decide by role, not by listing every column:
- Code / name / status / label columns the SQL groups or filters by → field with
dimension: {}block. - Use SQL
GROUP BYevidence and final dimension outputs to author reusable dimensions needed by the selected model. - The primary date/time column → field with
dimension: {is_time: true}plus{"time_granularity":"day|week|month|quarter|year"}hint. Point it at a real date/time column, never a numeric surrogate key. - Every physical column referenced by a requested metric expression must be declared in the owning dataset's
fields. This includes aggregate arguments,COUNT(DISTINCT ...)business keys,CASEpredicates, and arithmetic operands. Use a field with adimension:block when the SQL groups or filters by it; when it is only an aggregation input, use a plain field withoutdimension:. A declaration inprimary_key,unique_keys, or a relationship does not replace the field declaration required by metric compilation. - Columns whose comments/usage indicate measured quantities (balance, amount, quantity and their equivalents in the comment language) and that are only aggregated → field without a
dimension:block (name/expression/description only); metric expressions reference them by physical column name. - Precomputed ratio columns (rate, ratio, percent and equivalents) → field without a
dimension:block; note in itsdescriptionthat the metrics workflow recomputes weighted ratios from the numerator/denominator columns instead of aggregating this column. - Declared key columns (rule 7) still live in
primary_key/unique_keys/relationships, but also declare them as fields whenever the provided SQL uses them. For example,ac_codeinCOUNT(DISTINCT ac_code)is a plain field withoutdimension:, while a key used inGROUP BYis a field withdimension: {}. - Columns no provided SQL uses and that carry no key/time role → omit.
- Populate
descriptionfor all non-obvious fields from column comments, sample values, and profiler evidence; keep original language, do not translate.
- Code / name / status / label columns the SQL groups or filters by → field with
- Time dimension: exactly one primary time field per dataset. When several date columns exist and the primary one is ambiguous, ASK before generating. Verify
time_granularitywith data: run one query such asSELECT COUNT(DISTINCT <time_col>), MIN(<time_col>), MAX(<time_col>) FROM <table>and derive the snapshot interval (e.g. month-end dates spanning months →month; consecutive dates →day). When the data is indeterminate (a single distinct date), fall back to the table/column comments (e.g. a "monthly statistics" table comment →month), else default today. - Validation conflicts are fixed structurally. If
validate_semanticreports an element lowering to multiple types, follow the structural fix in the message: move the column into a verified key/relationship everywhere, give it adimension:block everywhere, or drop thedimension:block in datasets that only aggregate it. Never bounce a column between roles across validation attempts, and never falsify keys to silence the validator. - Relationships live inside the semantic model object, never inside a dataset. Use OSI core fields
from,to,from_columns,to_columns. The lists may contain one or more columns; they must have equal lengths and their order defines component correspondence.to_columnsmust exactly equal the target dataset's completeprimary_keyor one completeunique_keysentry — never join to a subset of a composite key. Collect every unverified target candidate returned byinspect_semantic_sourcesand verify all candidates you intend to use in onevalidate_semantic_key_candidatescall before authoring relationships. Do NOT use non-core fields such asfrom_dataset,from_identifier,join_on,from_column, orto_column. - Do NOT add metrics in the semantic-model step. Metrics are added by the metrics workflow under
semantic_model[0].metrics. - Preserve literal values and column names exactly; do not invent columns. Keep column comments in their original language — do not translate.
- Model durable query-backed results when requested. The request SQL is evidence, not a required dataset definition. When the user establishes the result itself as a durable reusable asset or explicitly asks for faithful one-query reproduction, create or update a query-backed dataset. Mark it with the DATUS query-source extension and expose only reusable final output fields.
- Use narrow dataset mutation tools. After planning the target, call
upsert_osi_datasetswith the first non-empty dataset batch; it creates a missing target as a complete valid document. For an existing model, use the same upsert so unrelated datasets, relationships, metrics, and model metadata are preserved. For an explicit request to remove named datasets, calldelete_osi_datasets; missing names are a successful no-op so retrying can reconcile stale Knowledge Base rows. Do not infer cascade deletions of relationships or metrics: validate the resulting model and use the errors plus user intent to decide whether to edit relationships, restore or replace a dataset, or require dependent metric changes throughgen_metrics. Useedit_fileonly for relationships or model metadata that the dataset tools do not own. Never create an empty semantic-model shell; upsert a replacement before deleting the last dataset.
Worked example — monthly snapshot table
Input (describe_table): branch_loan_quality_monthly — a monthly loan-quality statistics table, no declared primary key, columns: snapshot_date (date), branch_no/assess_dim_code/scope_code (varchar code columns), branch_name (varchar), loan_balance/npl_balance/overdue_balance (numeric balances), npl_rate/overdue_rate (numeric precomputed rates).
Correct field layout:
# no primary_key: the source declares none — the grain ("one row per month per branch per assessment dimension per scope") goes in ai_context.instructions
fields:
- name: snapshot_date
expression: {dialects: [{dialect: <osi_dialect>, expression: snapshot_date}]}
dimension: {is_time: true}
custom_extensions: [{vendor_name: DATUS, data: '{"time_granularity":"month"}'}]
- name: branch_no
expression: {dialects: [{dialect: <osi_dialect>, expression: branch_no}]}
dimension: {}
- name: assess_dim_code
expression: {dialects: [{dialect: <osi_dialect>, expression: assess_dim_code}]}
dimension: {}
- name: scope_code
expression: {dialects: [{dialect: <osi_dialect>, expression: scope_code}]}
dimension: {}
- name: branch_name
expression: {dialects: [{dialect: <osi_dialect>, expression: branch_name}]}
dimension: {}
- name: loan_balance # aggregation-only: field WITHOUT dimension block
expression: {dialects: [{dialect: <osi_dialect>, expression: loan_balance}]}
description: "Loan principal balance; aggregated by metrics"
# npl_balance / overdue_balance: same plain-field shape as loan_balance
- name: npl_rate # precomputed ratio: plain field, never aggregated directly
expression: {dialects: [{dialect: <osi_dialect>, expression: npl_rate}]}
description: "Precomputed row-level NPL ratio; metrics recompute the weighted ratio from npl_balance / loan_balance"
# overdue_rate: same plain-field shape as npl_rate
WRONG (do not do this): declaring loan_balance or npl_rate as fields with a dimension: block (or dimension: {is_time: false}); inventing primary_key: [branch_no, ...] when the DDL declares none; promoting JOIN columns to unique_keys without a passing full-table candidate-key verification; adding {"type":"numeric"} hints.
Workflow notes
- Resolve the target before writing. Priority: an explicit user-provided semantic model name; an existing model containing the core fact table; an inferred business domain; the core fact table as fallback.
- For deletion or other existing-YAML maintenance without an exact model name/file, call
list_existing_osi_semantic_modelsand select from the complete live inventory using dataset name, source, description, and business meaning. Give the unique existing model name to the planner; ask when multiple models remain plausible. - Put the core fact table first in
fact_tables. Pass dimensions separately; dimension tables never participate in naming. - When the resolver returns an existing file, preserve its semantic model name permanently, even when adding dimensions. Read and update that file instead of creating a renamed model.
- For a new resolved target, call
upsert_osi_datasetsdirectly; the first non-empty batch creates the document atomically. For an existing target, read it first and use narrow mutations. - For an existing target, keep its current semantic model name and preserve all unrelated datasets, relationships, and metrics. Add or update datasets with
upsert_osi_datasets, and remove only explicitly requested names withdelete_osi_datasets; never replace the file with a partial document containing only the requested objects. - Use the original SQL only as source evidence. Call
inspect_semantic_sourceswith the physical tables that need modeling, and use the live schema plus SQL usage to decide fields, keys, relationships, and time dimensions. Do not treat planner guesses as frozen schema decisions. - Before validation, cross-check every requested metric expression against the authored datasets and add any missing aggregate, distinct-key, predicate, or arithmetic input as a field with the role defined by rule 8.
- Treat
source_columns/target_columnsreturned by relationship discovery as one ordered composite whenkey_arity > 1. Put every complete target list you intend to use into the single batchvalidate_semantic_key_candidatescall; never validate or author its components independently. - When a critical modeling choice is ambiguous (which column set is the grain, which is the primary time dimension), ASK before generating.
- Call
validate_semantic(scope="semantic_model", semantic_model_name="<planned semantic model name>")without a customcheckssubset after writing or editing the OSI semantic model, and fix errors withedit_fileuntil the adapter's complete default profile passes; treat warnings about "aggregates column X which is also a dimension" as instructions to drop that field'sdimension:block or the field itself. - After validation passes, call
publish_semantic_model(semantic_model_files=[...]). In OSI mode this syncs OSI datasets to the Knowledge Base without using MetricFlow YAML.
Version History
- 8fb79f6 Current 2026-08-20 12:34


