strata-cli 0.1.9 → 0.1.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31a0327b1b8df2f7a982ece6c882cf019322cdf6e649b8cafdf035132424a705
4
- data.tar.gz: 16dbe764a144c7098574900d54fdad599727a65dbfc82fef531b6f63a59e39a7
3
+ metadata.gz: 371199d421786d4bc06748770b1155f59206d49f115eb0d2c664df2a0de139d8
4
+ data.tar.gz: eb1ecac2dc8e40864f5c8ad278353ed8b8924f99fc54a2dad5c356f63dacd06d
5
5
  SHA512:
6
- metadata.gz: 2fa33f77c3082a06a9a7e3e52cbeebd5b12ed4195b621011380ed2c39d124f3ebcd28b1962f8cf1565a999e101c8df7db15ea864dafed4d778211272a5c3f250
7
- data.tar.gz: b1b1250691596a599af3b3193220242fe36f50eb4c7cab83eaac18d3a59795979b4b861c11799a65588e6a077594b2ac396e45a54c4aa216421aaf6846896146
6
+ metadata.gz: 6f65de98ebf7994129da1cc2b6ae16a3591020e799ab61583afb1eea725f69b73fa6e35f3d8e69ebe0540bf8cdfe02f38a2f8d9f5c9e2ca606d0bf81536fa1d1
7
+ data.tar.gz: f6e89761a4ef0fd8586d8a82a4262594f22f95a14f814abc6b48ab2d62e20e840429ee0dbf215dc58f2c8a4d350eace329d7be6528f1a079b2fb17a223cf88c2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.11] - 2026-05-22
4
+
5
+ ### Changed
6
+
7
+ - **dwh dependency**: Require `dwh` `~> 0.4.2` (includes DuckDB initialization fix and Databricks streaming improvements from 0.4.x).
8
+
9
+ ## [0.1.10] - 2026-05-22
10
+
11
+ ### Added
12
+
13
+ - **Agent mode (`--agent`)**: Structured JSON output for automation and agent workflows on read-only commands (`datasource tables`, `datasource meta`, `version`, and others). Interactive `create` flows return machine-readable errors with stable codes when agent mode is requested.
14
+ - **Audit field validation**: Model audit checks allowed field keys, expression shape (string shortcut or mapping with `sql`), and `format` types (string shortcuts and hash forms).
15
+ - **Audit import resolution**: Model checks resolve YAML imports via `YamlImportResolver` before validating table and relationship definitions.
16
+
17
+ ### Changed
18
+
19
+ - **AGENTS.md template**: Updated project init template with semantic-model authoring guidance and `--agent` command usage.
20
+
3
21
  ## [0.1.9] - 2026-05-13
4
22
 
5
23
  ### Added
data/README.md CHANGED
@@ -25,6 +25,30 @@ gem install strata-cli
25
25
 
26
26
  > šŸ’” **Tip:** Run `strata COMMAND --help` for detailed help, options, and examples on any command. The CLI help system provides comprehensive documentation for each command.
27
27
 
28
+ ## Agent mode (`--agent`)
29
+
30
+ Pass `--agent` on any command for structured JSON output and no interactive prompts (where the command supports it).
31
+
32
+ **Supported examples:**
33
+
34
+ ```bash
35
+ strata datasource list --agent
36
+ strata datasource tables my_db --agent
37
+ strata datasource meta my_db orders --agent
38
+ strata table list --agent
39
+ strata audit all --agent
40
+ strata deploy --yes --agent
41
+ ```
42
+
43
+ **Not supported** — these commands are interactive generators; write YAML directly instead:
44
+
45
+ ```bash
46
+ strata create table [TABLE_PATH] # → models/tbl.*.yml
47
+ strata create relation RELATION_PATH # → models/rel.*.yml
48
+ ```
49
+
50
+ Use `strata datasource meta DS_KEY TABLE_NAME --agent` for column metadata when authoring table models.
51
+
28
52
  <details>
29
53
  <summary><strong>Project Management</strong></summary>
30
54
 
@@ -127,7 +151,7 @@ strata datasource exec my_db -f queries/analysis.sql
127
151
  <summary><strong>Model Creation</strong></summary>
128
152
 
129
153
  ### `strata create table [TABLE_PATH]`
130
- Create a semantic table model from a datasource table.
154
+ Create a semantic table model from a datasource table. **Not available with `--agent`** — write `models/tbl.*.yml` directly or use `strata datasource meta` for schema metadata.
131
155
 
132
156
  **Options:** `-d, --datasource DS_KEY`
133
157
 
@@ -142,7 +166,7 @@ strata create table contact/dse.call_center_d # With schema
142
166
  **Process:** Checks table existence → Fetches metadata → AI suggests fields → Interactive editor → Generates model file
143
167
 
144
168
  ### `strata create relation RELATION_PATH`
145
- Create a relation (join) definition file.
169
+ Create a relation (join) definition file. **Not available with `--agent`** — write `models/rel.*.yml` directly.
146
170
 
147
171
  **Options:** `-d, --datasource DS_KEY`
148
172
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "agent_output"
4
+
5
+ module Strata
6
+ module CLI
7
+ module AgentMode
8
+ def self.included(base)
9
+ base.class_option :agent, type: :boolean, default: false,
10
+ desc: "Agent mode: structured output, no interactive prompts"
11
+ end
12
+
13
+ def agent_mode?
14
+ value = options[:agent]
15
+ value = options["agent"] if value.nil?
16
+ value == true
17
+ end
18
+
19
+ def reject_agent_mode!(message, code: "agent_mode_unsupported")
20
+ return unless agent_mode?
21
+
22
+ AgentOutput.emit_error(message, code: code)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Strata
6
+ module CLI
7
+ module AgentOutput
8
+ module_function
9
+
10
+ def emit_json(data)
11
+ $stdout.puts(JSON.generate(data))
12
+ end
13
+
14
+ def emit_error(message, code: "agent_mode_unsupported", **metadata)
15
+ payload = {error: message, code: code}.merge(metadata)
16
+ warn(JSON.generate(payload))
17
+ exit(1)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,136 +1,505 @@
1
1
  # Strata Semantic Model Project
2
2
 
3
- This is a **Strata** semantic model project. It defines the dimensions, measures, and relationships that the Strata BI platform exposes to end users for querying and reporting.
3
+ You are authoring a **semantic model** — version-controlled YAML in this repo (`models/tbl.*.yml`, `models/rel.*.yml`, datasources). That is not one-off SQL, warehouse ETL, or a standalone script.
4
4
 
5
- ## Project Structure
5
+ | Term | Meaning in Strata |
6
+ |------|-------------------|
7
+ | **Semantic model** | What you edit in git — tables, fields, relationships, migrations |
8
+ | **Semantic layer** | What exists on the server **after deploy** — the governed dimensions, measures, and joins users query |
6
9
 
10
+ After **`strata deploy`**, this project’s model becomes (or updates) the **semantic layer** on the Strata server for that branch. The **Strata web app** charts and reports run against that layer, not against raw warehouse tables.
11
+
12
+ ## What this model powers (end-to-end)
13
+
14
+ | Stage | What happens |
15
+ |-------|----------------|
16
+ | **You (agent + human)** | Edit the **semantic model** in `models/`; run `strata audit` |
17
+ | **Human** | `strata deploy` publishes the model to the **Strata server** (per git branch) |
18
+ | **Strata server** | Builds join universes, plans queries, runs SQL against datasources |
19
+ | **Strata web app** | Analysts query the deployed **semantic layer** — charting, reporting, filters, exploration by field **name** |
20
+
21
+ Field and table **names**, joins, and measure definitions affect **live dashboards and saved reports**, not only CLI or git. Breaking naming, duplicates, or joins can break production content after deploy. YAML is consumed by deploy on the server — it is **not** ad-hoc warehouse SQL run in isolation.
22
+
23
+ Read this file end-to-end before creating or editing `models/**/*.yml`. Do not mirror warehouse DDL or ad-hoc SQL style; follow the contracts below.
24
+
25
+ **Do not infer Strata behavior from warehouse SQL or generic BI tools.** Rules in this file are authoritative. Use the [official documentation index](#official-documentation) only when you need depth beyond what is inlined here.
26
+
27
+ ## Critical rules
28
+
29
+ These are non-negotiable. Violations break deploy, query planning, or production reports.
30
+
31
+ 1. **Every field `name` is unique project-wide** — one name = one dimension or measure entity across all `tbl.*.yml` files. Strata has no `table.field` namespaces; bracket references like `[Total Revenue]` resolve by name alone.
32
+ 2. **`many_to_many` join cardinality is not supported.** Use a junction/bridge table with two relationships (`one_to_many` + `many_to_one`) instead.
33
+ 3. **Measures on unrelated detail facts must have distinct names** — e.g. `Store Gross Sales`, `Catalog Gross Sales`, not one `Gross Sales` on three channel facts. See [Naming conventions](#naming-conventions).
34
+ 4. **Dimensions may share names** when the business role is the same; Strata picks among tables at query time. **Measures do not combine that way** — duplicate measure names attach more SQL to the same measure entity (double-count risk).
35
+ 5. **Cross-fact totals use compound measures or blending** — not the same measure name on multiple facts. See [Combined metrics across facts](#combined-metrics-across-facts).
36
+
37
+ ## Unsupported or impossible requirements
38
+
39
+ **If the user asks for something Strata does not support, say so clearly and do not edit `models/**/*.yml` to approximate it.** A broken or misleading semantic model is worse than no change.
40
+
41
+ ### Before writing YAML
42
+
43
+ 1. **Classify the request:** supported in Strata Ā· supported with a different pattern Ā· unsupported Ā· unknown.
44
+ 2. **If unsupported or unknown:** explain the limitation, why a workaround would fail (deploy, double-count, invalid cardinality, invented keys), and offer **compliant alternatives** if any exist.
45
+ 3. **Wait for the user to choose** a supported approach (or to change the requirement) before proposing file changes.
46
+ 4. **Never ship ā€œbest effortā€ YAML** that you expect `audit` or deploy to reject, or that violates [Critical rules](#critical-rules) — fix the design in conversation first.
47
+
48
+ ### What to tell the user
49
+
50
+ Use plain language, for example:
51
+
52
+ - ā€œStrata does not support X. Doing Y in YAML would not work because ā€¦ā€
53
+ - ā€œSupported alternative: … (trade-off: …)ā€
54
+ - ā€œI’m not sure Strata supports X; I won’t change the model until we confirm — see [official documentation](#official-documentation) or your Strata admin.ā€
55
+
56
+ ### Do not do these when blocked
57
+
58
+ - Invent YAML keys or properties not in Strata’s schema (`custom_join`, `blend_measures`, etc.).
59
+ - Use duplicate measure names, fake dimensions, or `many_to_many` joins as workarounds.
60
+ - Paste full ad-hoc report SQL into `expression.sql` and call it a semantic model.
61
+ - Rename or migrate entities to ā€œmakeā€ an unsupported shape fit.
62
+ - Silently implement something that only works in raw warehouse SQL, not in Strata’s planner.
63
+
64
+ ### Common unsupported asks → response pattern
65
+
66
+ | User ask | Why it fails | Compliant direction (if any) |
67
+ |----------|--------------|------------------------------|
68
+ | `many_to_many` join between two tables | Not supported | Junction table + two `rel` entries |
69
+ | One measure name on store + catalog + web facts | One measure entity, double-count risk | Distinct measures + [compound measure](#combined-metrics-across-facts) |
70
+ | ā€œCombine metrics by reusing the same nameā€ | Not how Strata merges facts | Compound measure or blending on shared dimensions |
71
+ | Cross-datasource compound measure | Same datasource required | Model per datasource or separate metrics |
72
+ | Datasource swap migration | Not supported | Rename datasource; human migration plan |
73
+ | Snapshot-style metric on a flow fact without `snapshot_date` | Wrong measure type | Normal additive measure, or proper snapshot table setup |
74
+ | Requirement needs engine feature you cannot verify | Risk of invalid model | Stop; cite docs or ask human — do not guess |
75
+
76
+ When `strata audit all --agent` reports errors you cannot resolve within Strata’s rules, **stop and report the errors to the user** — do not keep patching YAML until the model drifts further from a valid design.
77
+
78
+ ## Order of operations for agents
79
+
80
+ Follow this sequence. Do not skip steps or reorder deploy before audit.
81
+
82
+ | Step | Who | Action |
83
+ |------|-----|--------|
84
+ | 1 | Agent | `strata datasource list --agent` → `tables` → `meta` (discovery) |
85
+ | 2 | Agent | `strata table list --agent` (existing models, if any) |
86
+ | 3 | Agent | Classify tables; check request is [supported](#unsupported-or-impossible-requirements); propose field names and joins (or explain why not) |
87
+ | 4 | Human | Approve proposal (required before any file writes) |
88
+ | 5 | Agent | Write `models/**/*.yml` (and `migrations/*.yml` if renaming — see [Renames and swaps](#renames-swaps-and-production-branch)) |
89
+ | 6 | Agent | `strata audit all --agent` — **required after every model change** |
90
+ | 7 | Human | `strata deploy` on the intended git branch (deploy runs audits again) |
91
+
92
+ **Agents must not run:** `strata deploy`, `strata datasource add`, interactive `strata create table` / `strata create relation` / `strata create migration`, or writing secrets into `.strata`.
93
+
94
+ **Branches:** Model on feature branches; deploy to the matching Strata server branch for staging. Production deploys typically use `production_branch` in `project.yml` (default `main`). Renames and swaps that affect production query names should happen on the production branch — see [Renames and swaps](#renames-swaps-and-production-branch).
95
+
96
+ ## How the semantic layer maps to the warehouse
97
+
98
+ | Concept | In your YAML | Not the same as |
99
+ |--------|----------------|-----------------|
100
+ | Table users query | `name` in `tbl.*.yml` | `physical_name` (warehouse table) |
101
+ | Column in a table | `expression.sql` on each field | Bare column string or `physical_name.column` |
102
+ | Join endpoints | `left` / `right` (logical table names) | Table names inside join `sql` |
103
+ | Join condition | `sql` using `left.` and `right.` | `store_sales.col = date_dim.col` |
104
+ | Second FK to same dimension | New logical `tbl` (role-playing) + join to that name | Two `rel` entries with same `left` + `right` |
105
+
106
+ ## Deploy contracts (required on every change)
107
+
108
+ Every `tbl` field and every `rel` join must satisfy **all** of these before deploy will succeed:
109
+
110
+ 1. **Field `expression`** is a non-empty SQL string shortcut (`expression: order_id`, `expression: sum(amount)`) or a mapping with non-empty `sql`. Use the mapping form when you need `lookup`, `primary_key`, or `array`.
111
+ 2. **Join `sql` uses only `left.` and `right.`** — column names from field definitions on those tables. Never warehouse prefixes (`orders.`, `date_dim.`, `store_sales.`).
112
+ 3. **At most one join per logical table pair** in a branch. If a fact has two FKs to the same physical dimension (e.g. sold date and ship date both → `date_dim`), create **separate logical tables** (role-playing dimensions) and join to each distinct `name`.
113
+ 4. **`left` / `right` in rel files match `name` in `tbl.*.yml` exactly** (same spelling and casing as the table model).
114
+
115
+ Run `strata audit all --agent` after edits; a human runs `strata deploy`.
116
+
117
+ ## CLI for agents
118
+
119
+ Append `--agent` to every `strata` command (structured output, no prompts):
120
+
121
+ ```bash
122
+ strata datasource list --agent
123
+ strata datasource tables MY_DS --agent
124
+ strata datasource meta MY_DS TABLE_NAME --agent
125
+ strata table list --agent
126
+ strata audit all --agent
7
127
  ```
8
- project.yml # Project config (server URL, project ID, production branch)
9
- datasources.yml # Data warehouse connection config (no credentials)
10
- .strata # Local credentials and API keys — gitignored, never commit
11
- models/ # Semantic model definitions
12
- tbl.*.yml # Table models (dimensions + measures)
13
- rel.*.yml # Relationship/join definitions
14
- tests/ # Model tests
128
+
129
+ **Do not run:** `strata create table`, `strata create relation` (interactive generators). Write `models/tbl.*.yml` and `models/rel.*.yml` directly instead.
130
+
131
+ If multiple datasources exist, pass `DS_KEY` on `tables` and `meta`.
132
+
133
+ ## Datasources — human-in-the-loop (secrets)
134
+
135
+ If the CLI returns `no_datasources`, ask the user to run `strata datasource add [ADAPTER]` in a terminal. Do not create `datasources.yml` entries or fill `.strata` credentials yourself.
136
+
137
+ ## Human-in-the-loop before file writes
138
+
139
+ Do **not** silently bulk-write model files. Before any `models/**/*.yml` change:
140
+
141
+ 0. **If the request is unsupported** — follow [Unsupported or impossible requirements](#unsupported-or-impossible-requirements); do not proceed to file writes until the user picks a supported approach.
142
+ 1. **Describe intent** — tables, fields, joins, and role-playing tables if needed (e.g. separate **Catalog Ship Date** when catalog sales has both sold-date and ship-date FKs).
143
+ 2. **Explain semantic impact** — which **dimension** names are intentionally shared vs which **measure** names stay **distinct per fact**; new join paths; any proposed **compound measures** (including **which host table** defines dimensionality); any **migrations** (renames/swaps) and whether work is on **production branch**; which logical tables are created vs updated.
144
+ 3. **Wait for explicit approval**, then write files and run `strata audit all --agent`.
145
+
146
+ ## Workflow
147
+
148
+ Same steps as [Order of operations for agents](#order-of-operations-for-agents). In the proposal (step 3–4), include:
149
+
150
+ - Table classification: **fact**, **dimension**, or **aggregate/summary**
151
+ - **Unique measure name per unrelated detail fact**; shared dimension names only where the role matches
152
+ - `tbl.*.yml` and `rel.*.yml` drafts that satisfy [Deploy contracts](#deploy-contracts-required-on-every-change) and naming rules below
153
+
154
+ ---
155
+
156
+ ## Table models — `models/tbl.<name>.yml`
157
+
158
+ ```yaml
159
+ datasource: "<datasource_key_or_name>"
160
+ name: "<logical_name>" # Required — used in rel left/right and queries
161
+ physical_name: "<warehouse_table>" # Required — actual table in the database
162
+ cost: 10
163
+
164
+ fields:
165
+ - type: dimension
166
+ name: "Order ID"
167
+ data_type: bigint
168
+ expression:
169
+ sql: order_id # Column or expression fragment for this table
170
+ lookup: true # Typical for filterable dimensions
171
+ primary_key: true # Optional — business/surrogate key
172
+
173
+ - type: measure
174
+ name: "Store Revenue" # Unique per fact — not reused on other fact tables
175
+ data_type: decimal
176
+ expression:
177
+ sql: sum(ss_sales_price) # Aggregation required for measures
15
178
  ```
16
179
 
17
- ## CLI Reference
180
+ **Field rules**
18
181
 
19
- Use the `strata` CLI to manage this project. All commands should be run from the project root.
182
+ - `data_type`: `string`, `integer`, `bigint`, `decimal`, `date`, `date_time`, `boolean` (map from `normalized_data_type` in `datasource meta`).
183
+ - `expression` keys: `sql` (required), `lookup`, `primary_key`, `array` (optional booleans).
184
+ - Optional: `description`, `grains` (date/time), `format`, `synonyms`, `imports` of other tbl files.
20
185
 
21
- ### Inspect
186
+ **Date/time grains** (optional):
22
187
 
23
- ```bash
24
- strata table list # List all defined semantic tables
25
- strata datasource list # List configured datasources
26
- strata datasource tables [DS_KEY] # Browse physical tables in a datasource
27
- strata datasource meta DS_KEY TABLE_NAME # Show columns for a physical table
188
+ ```yaml
189
+ grains: [day, week, month, quarter, year]
28
190
  ```
29
191
 
30
- ### Create
192
+ ---
31
193
 
32
- ```bash
33
- strata create table [TABLE_PATH] # AI-assisted semantic table creation
34
- # TABLE_PATH examples:
35
- # orders → models/tbl.orders.yml
36
- # sales/orders → models/sales/tbl.orders.yml
37
- # dw.fact_orders → models/tbl.dw.fact_orders.yml (schema-prefixed)
194
+ ## Relationship models — `models/rel.<name>.yml`
195
+
196
+ ```yaml
197
+ datasource: "<datasource_key_or_name>"
38
198
 
39
- strata create relation RELATION_NAME # Create a join definition (rel.*.yml)
199
+ orders_to_customers:
200
+ left: "Orders" # Must match tbl name — many side for many_to_one
201
+ right: "Customers" # Must match tbl name — one side
202
+ sql: "left.customer_id = right.id"
203
+ cardinality: many_to_one
40
204
  ```
41
205
 
42
- `strata create table` fetches column metadata from the datasource, generates field definitions via AI, then opens an interactive editor to review and confirm before writing the YAML file.
206
+ **Join rules**
43
207
 
44
- ### Validate & Deploy
208
+ - YAML `left` / `right`: logical table `name` values from `tbl.*.yml`.
209
+ - `sql`: equality using **`left.<column>`** and **`right.<column>`** only — use the same column names as in each table’s field `expression.sql`.
210
+ - `cardinality`: `many_to_one`, `one_to_many`, or `one_to_one` (as appropriate).
211
+ - Optional: `join: inner` (default) or `left` / `right`.
45
212
 
46
- ```bash
47
- strata audit all # Run all validation checks
48
- strata audit syntax # Validate YAML syntax only
49
- strata audit models # Validate model structure and references
50
- strata audit connections # Test all datasource connections
51
-
52
- strata deploy # Deploy to Strata server
53
- strata deploy -e production # Deploy to a specific environment
54
- strata deploy status # Check current deployment status
213
+ ### Role-playing dimensions (multiple FKs to one physical table)
214
+
215
+ When one fact table has **more than one foreign key** to the same physical dimension (common with `date_dim`: sold date, ship date, etc.), you need **one logical table per role**, each with its own join:
216
+
217
+ | Wrong | Right |
218
+ |-------|--------|
219
+ | Two rel entries: `Catalog Sales` → `Date` (sold) and `Catalog Sales` → `Date` (ship) | `Catalog Sales` → `Date` (sold) and `Catalog Sales` → `Catalog Ship Date` (ship) |
220
+ | Single `tbl.date.yml` reused for both roles without planning | Add `tbl.catalog_ship_date.yml` with `name: Catalog Ship Date`, same `physical_name: date_dim`, fields aligned to date role |
221
+
222
+ Example pattern:
223
+
224
+ ```yaml
225
+ # tbl — two logical tables, one physical date_dim
226
+ # tbl.date.yml → name: Date
227
+ # tbl.catalog_ship_date.yml → name: Catalog Ship Date, physical_name: date_dim
228
+
229
+ # rel — distinct right table per FK
230
+ catalog_sales_sold_date:
231
+ left: "Catalog Sales"
232
+ right: "Date"
233
+ sql: "left.cs_sold_date_sk = right.d_date_sk"
234
+ cardinality: many_to_one
235
+
236
+ catalog_sales_ship_date:
237
+ left: "Catalog Sales"
238
+ right: "Catalog Ship Date"
239
+ sql: "left.cs_ship_date_sk = right.d_date_sk"
240
+ cardinality: many_to_one
241
+ ```
242
+
243
+ Before adding rel entries, list each fact’s FKs and confirm **no duplicate `(left, right)` pairs**. If ship and sold both target `Date`, add a role-playing tbl first.
244
+
245
+ ---
246
+
247
+ ## Naming conventions
248
+
249
+ ### Dimensions (global — reuse when the role is the same)
250
+
251
+ - **Dimension names are global** across the project — same name on multiple tables = same concept; Strata picks the best table at query time.
252
+ - **Reuse consistent names** for shared dimensions (e.g. "Customer ID", "Date") where the role is the same.
253
+ - **Prefix role-specific dimensions** when the same physical column means different things (e.g. "Catalog Ship Date" vs "Date").
254
+ - **Prefix ambiguous attributes** on dimension tables (e.g. "Item Color" not "Color" on an item dimension).
255
+
256
+ ### Measures (unique — do not treat like dimensions)
257
+
258
+ - **Project-wide uniqueness:** every measure `name` must be unique across the entire semantic layer (same rule as dimensions). There is only one `Semantic::Field` per name per branch; deploy reuses it when the same name appears in another `tbl.*.yml`.
259
+ - **Default:** one measure name per **unrelated detail fact**. Example (wrong): `Gross Sales` on `store_sales`, `catalog_sales`, and `web_sales`. Example (right): `Store Gross Sales Amount`, `Catalog Gross Sales Amount`, `Web Gross Sales Amount`.
260
+ - **One measure name = one measure entity.** Putting the same name on another fact table adds that table’s SQL to the **same** measure — it does **not** create a second metric and can cause **double counting** or ambiguous routing.
261
+ - **Exception — aggregate/summary tables only:** reusing a measure name is appropriate when the second table is an **alternate physical representation** of the **same** metric (rollup / pre-aggregate), routed via table **`cost`** and/or **`partitions`** — not when modeling three separate channel facts. Do **not** put the same additive flow measure on both a detail fact and a rollup fact without that routing design.
262
+ - **Similar warehouse column names do not imply one shared measure.** `ss_sales_price` and `cs_ext_sales_price` are different facts unless you have explicit rollup routing as above.
263
+ - **Prefix or qualify by fact/table** (`Store …`, `Catalog …`, `Web …`) or use distinct business names (`order_revenue` vs `subscription_revenue`).
264
+ - **Do not ā€œfixā€ duplicate measure names with rename migrations** — use distinct names or a [compound measure](#combined-metrics-across-facts) instead.
265
+
266
+ ---
267
+
268
+ ## Multi-fact warehouses (star schema)
269
+
270
+ Typical warehouses (e.g. TPC-DS on Athena) have **many fact tables** and shared dimensions. Before naming fields:
271
+
272
+ 1. **Inventory tables** — label each as fact, dimension, or aggregate/summary (pre-aggregated rollups).
273
+ 2. **One grain per fact** — e.g. `store_sales` = store channel transactions; do not model the same additive flow metric on both a **detail** fact and a **rollup** fact unless the rollup is explicitly non-additive or routed via table `cost` / `partitions` (see advanced links below).
274
+ 3. **Do not copy a synthetic dimension onto every fact** (e.g. "Sales Channel" on store, catalog, and web facts) to justify reusing one measure name — that does **not** fix measure uniqueness or double-count risk. Put channel only where that fact’s grain truly includes channel.
275
+
276
+ ---
277
+
278
+ ## Combined metrics across facts
279
+
280
+ | Wrong | Right |
281
+ |-------|--------|
282
+ | Same measure name on `store_sales`, `catalog_sales`, `web_sales` | Distinct measure per fact |
283
+ | Expect Strata to auto-sum identical names | Explicit **compound measure** (or blending via shared **dimensions**, not duplicate measure names) |
284
+
285
+ To report a **total across channels/facts**, define separate measures per fact, then optionally one compound measure:
286
+
287
+ ```yaml
288
+ # On one table (or where the compound is defined), same datasource:
289
+ - type: measure
290
+ name: Total Gross Sales
291
+ data_type: decimal
292
+ expression:
293
+ sql: ([Store Gross Sales Amount] + [Catalog Gross Sales Amount] + [Web Gross Sales Amount])
55
294
  ```
56
295
 
57
- ## Semantic Model Files
296
+ Bracket names must match deployed measure names exactly. Compound measures are resolved at query time; all referenced measures must be in the **same datasource** and reachable in a valid universe. No circular references (`A` → `B` → `A`).
297
+
298
+ ### Host table and dimensionality
299
+
300
+ Define compound measures on the **`tbl.*.yml` whose universe should govern the metric**:
301
+
302
+ - Which **dimensions** can group the compound measure
303
+ - How **automatic data blending** applies when referenced measures live on different tables in the same datasource
304
+
305
+ If referenced measures are not joinable in **one universe** from that host table, query planning fails. Propose the host table with the human reviewer (e.g. compound on **Store Sales** exposes store-centric join paths; compound on a shared dimension table exposes different dimensions).
306
+
307
+ **Use calculations (compound measures), not shared measure names** across unrelated facts.
308
+
309
+ **Automatic data blending** merges results on shared **dimensions** (often via `extended_blend_group`) — not by reusing the same measure name on multiple fact tables. See [extended blending](https://strata.do/developer-docs/developer-guide/semantic-model/expressions/extended-blending) and the [glossary](https://strata.do/developer-docs/developer-guide/getting-started/glossary).
58
310
 
59
- ### Table model — `models/tbl.<name>.yml`
311
+ ---
312
+
313
+ ## Snapshot measures
314
+
315
+ **Use for** point-in-time **state** — inventory on hand, account balances, membership counts at period boundaries. **Not for** additive flows over time (revenue, units sold) — use normal measures with `sum(...)`.
316
+
317
+ **Requirements:**
318
+
319
+ 1. Table sets `snapshot_date: <Date dimension name>` (dimension on same table or unambiguous in the table’s universe).
320
+ 2. Measure sets `snapshot: ending` (value at end of period) or `snapshot: beginning` (value at start).
60
321
 
61
322
  ```yaml
62
- datasource: "<datasource_key_or_name>" # Required
63
- name: "<logical_name>" # Required — unique within datasource
64
- physical_name: "<db_table_name>" # Required — physical table in the warehouse
65
- cost: 10 # Lower = preferred when multiple tables can answer a query
66
-
67
- # Optional
68
- description: ""
69
- snapshot_date: "<dimension_name>" # For snapshot/inventory tables only
70
- tags: []
71
- imports: # Inherit fields from other YAML files
72
- - "../shared/common_fields.yml"
323
+ name: Inventory
324
+ physical_name: inventory
325
+ datasource: warehouse
326
+ snapshot_date: Date
73
327
 
74
328
  fields:
75
- - type: dimension # "dimension" (categorical) or "measure" (numeric aggregate)
76
- name: "Order ID" # Required — human-friendly name; MUST be consistent across tables
77
- description: ""
78
- data_type: bigint # See supported types below
79
- expression: order_id # SQL column name, or object form (see below)
80
- synonyms: [] # 0–3 alternative names for AI search
329
+ - type: dimension
330
+ name: Date
331
+ data_type: date
332
+ expression:
333
+ sql: snapshot_date
81
334
 
82
335
  - type: measure
83
- name: "Total Revenue"
84
- data_type: decimal
85
- expression: "sum(amount)" # Measures use SQL aggregation expressions
336
+ name: Ending Inventory
337
+ data_type: integer
338
+ snapshot: ending
339
+ expression:
340
+ sql: sum(quantity)
86
341
  ```
87
342
 
88
- **Supported `data_type` values:** `string`, `integer`, `bigint`, `decimal`, `date`, `date_time`, `boolean`
343
+ When users group by a time period, the engine picks the last (`ending`) or first (`beginning`) snapshot in each bucket — not a sum of every row in the period.
344
+
345
+ More detail: [snapshot measures](https://strata.do/developer-docs/developer-guide/semantic-model/fields/measures/snapshot)
346
+
347
+ ---
348
+
349
+ ## Inclusion measures
89
350
 
90
- No precision or scale — just the base type.
351
+ **Problem:** medians and averages are wrong when the stored grain is finer than the statistic you need — e.g. `median(hours)` over `(day, account, title)` rows is not ā€œmedian account hours per day.ā€
352
+
353
+ **Pattern:** two-step aggregation — inner `expression.sql`, extra dimensions in `inclusions.dimensions`, outer `inclusions.aggregation`.
354
+
355
+ **Use when:** medians, percentiles, or averages that need an **intermediate grain** (e.g. per account per day) before rolling up to the query grain.
91
356
 
92
- **Complex `expression` form:**
93
357
  ```yaml
94
- expression:
95
- sql: my_column
96
- primary_key: true # optional
97
- lookup: true # optional
98
- array: true # optional
358
+ - type: measure
359
+ name: Daily Median Account View Hours
360
+ data_type: decimal
361
+ inclusions:
362
+ filter: apply
363
+ aggregation: percentile_cont(0.5) WITHIN GROUP (ORDER BY @exp)
364
+ dimensions:
365
+ - Account ID
366
+ expression:
367
+ sql: sum(hours)
99
368
  ```
100
369
 
101
- **Date/time grains** (optional, for `date`/`date_time` fields):
370
+ Inner: `sum(hours)` per `(day, account)`. Outer: median of those account totals per `day`.
371
+
372
+ More detail: [inclusions](https://strata.do/developer-docs/developer-guide/advanced/inclusions)
373
+
374
+ ---
375
+
376
+ ## Exclusion measures
377
+
378
+ **Two independent controls:**
379
+
380
+ - **`exclusion_type`** — which dimensions may **group** the measure (`exclude`, `exclude_all_except`, `exclude_all`).
381
+ - **`filter` on each exclusion entry** — how **filters** on those entities apply (`apply`, `ignore`, `only`).
382
+
383
+ **Use when:** grand totals, metrics that must not break down by certain dimensions, or revenue that should ignore a dimension for grouping while still filtering normally.
384
+
102
385
  ```yaml
103
- grains: [day, week, month, quarter, year]
386
+ - type: measure
387
+ name: Revenue Excluding Return Status
388
+ data_type: decimal
389
+ exclusion_type: exclude
390
+ exclusions:
391
+ - type: dimension
392
+ filter: apply
393
+ entities:
394
+ - Return Status
395
+ expression:
396
+ sql: sum(amount)
104
397
  ```
105
398
 
106
- ### Relationship model — `models/rel.<name>.yml`
399
+ `Return Status` will not appear in the grouping set for this measure even if the user adds it to the query; other dimensions still group normally.
400
+
401
+ More detail: [exclusions](https://strata.do/developer-docs/developer-guide/advanced/exclusions)
402
+
403
+ ---
404
+
405
+ ## Common mistakes (anti-patterns)
406
+
407
+ | Anti-pattern | Why it fails |
408
+ |--------------|----------------|
409
+ | `Gross Sales` on store + catalog + web facts | One measure entity, multiple fact SQLs → double-count / ambiguous routing |
410
+ | "Sales Channel" dimension added to every fact | Does not replace unique measure names per fact |
411
+ | Identical column labels in the warehouse → one measure name | Warehouse naming ≠ one Strata metric |
412
+ | Assuming global dimension rules apply to measures | Dimensions are shared by name; measures are **not** |
413
+ | Rename/swap on a feature branch without team coordination | Production saved queries still reference old names; branch may not match production entities |
414
+ | Renaming to deduplicate measure names across facts | Use distinct names or compound measures — migrations are for intentional renames, not modeling fixes |
415
+
416
+ ---
417
+
418
+ ## Advanced features (quick reference)
419
+
420
+ | Feature | Use when |
421
+ |---------|----------|
422
+ | [Compound measures](https://strata.do/developer-docs/developer-guide/semantic-model/fields/measures/compound) | Formula across named measures; host table sets dimensionality |
423
+ | [Snapshot measures](https://strata.do/developer-docs/developer-guide/semantic-model/fields/measures/snapshot) | Period-boundary state (inventory, balances) — see [Snapshot measures](#snapshot-measures) |
424
+ | [Inclusions](https://strata.do/developer-docs/developer-guide/advanced/inclusions) | Median/percentile/avg needing intermediate grain — see [Inclusion measures](#inclusion-measures) |
425
+ | [Exclusions](https://strata.do/developer-docs/developer-guide/advanced/exclusions) | Control grouping and filters per dimension — see [Exclusion measures](#exclusion-measures) |
426
+ | Table `cost` | Lower cost = preferred table when multiple tables can answer; use for rollup vs detail routing |
427
+ | [Partitions](https://strata.do/developer-docs/developer-guide/advanced/partitions) | Table only has subset of data — `between` (date range) or `in_list`; planner routes when partition matches |
428
+ | [Extended blending](https://strata.do/developer-docs/developer-guide/semantic-model/expressions/extended-blending) | Blend dimensions across tables in same datasource — not duplicate measure names |
429
+ | [Imports](https://strata.do/developer-docs/developer-guide/semantic-model/imports) | Reuse field definitions across `tbl` files |
430
+
431
+ ---
432
+
433
+ ## Renames, swaps, and production branch
434
+
435
+ Saved reports and dashboards reference field and table **names**. Renaming without a migration breaks production queryables.
436
+
437
+ ### Rename (`type: rename`, `hook: pre`)
438
+
439
+ Runs **before** YAML is processed. Old name is rewritten to new name; deploy updates saved references.
107
440
 
108
441
  ```yaml
109
- datasource: "<datasource_key_or_name>"
442
+ - type: rename
443
+ hook: pre
444
+ entity: measure
445
+ from: Old Revenue
446
+ to: Store Revenue
447
+ ```
448
+
449
+ Supported entities: `dimension`, `measure`, `table`, `datasource`.
110
450
 
111
- order_customer: # Relationship name (any key)
112
- left: "Orders" # Table on the "many" side
113
- right: "Customers" # Table on the "one" side
114
- sql: "orders.customer_id = customers.id"
115
- cardinality: many_to_one # one_to_one | one_to_many | many_to_one | many_to_many
451
+ ### Swap (`type: swap`, `hook: post`)
452
+
453
+ Runs **after** all definitions load. Replaces references from entity A to entity B when **both** exist. Datasource swap is not supported.
454
+
455
+ ```yaml
456
+ - type: swap
457
+ hook: post
458
+ entity: measure
459
+ from: Legacy Revenue
460
+ to: Store Revenue
116
461
  ```
117
462
 
118
- ## Important Naming Rules
463
+ ### Production branch rule
119
464
 
120
- - **Dimension names are global.** When the same dimension name appears on multiple tables, Strata treats them as the same concept and picks the best table at query time.
121
- - **Use consistent names** for shared dimensions (e.g., "Customer ID", "Order Date") across all table models.
122
- - **Prefix ambiguous dimensions** from dimension tables: a `color` column on an `item_dim` table should be named `Item Color`, not `Color`.
465
+ **Only rename or swap on `production_branch`** (in `project.yml`, usually `main`) when the change must align with production queryables. On other branches, production may still reference old names that do not exist on your branch.
123
466
 
124
- ## Workflow
467
+ Deploy **migration files and updated YAML in the same deployment**. Update `tbl.*.yml` references to match renames in the same change.
468
+
469
+ **Agents:** propose migration YAML under `migrations/` and matching model edits; do **not** run interactive `strata create migration`. Flag renames for human review and confirm branch strategy.
470
+
471
+ More detail: [migrations](https://strata.do/developer-docs/developer-guide/cli/migrations)
472
+
473
+ ---
474
+
475
+ ## Official documentation
125
476
 
126
- 1. `strata datasource tables DS_KEY` — explore available physical tables
127
- 2. `strata create table <path>` — generate and review a semantic model
128
- 3. Edit the generated `tbl.*.yml` directly for fine-tuning
129
- 4. `strata audit all` — validate before deploying
130
- 5. `strata deploy` — push to Strata server
477
+ **Primary:** this `AGENTS.md` — follow inline rules first.
131
478
 
132
- ## Further Reading
479
+ **Optional bulk load:** fetch https://strata.do/developer-docs/llms.txt when implementing a topic not fully covered here (complete Strata doc export for AI agents).
133
480
 
134
- For advanced semantic model concepts (partitioning, inclusions, exclusions, snapshot measures, display types, formatters, and more), refer to the Strata developer docs:
481
+ **Curated links** (use for examples and edge cases after reading the matching section above):
135
482
 
136
- https://strata.do/developer-docs/developer-guide/semantic-model
483
+ | Topic | URL |
484
+ |-------|-----|
485
+ | Semantic model overview | https://strata.do/developer-docs/developer-guide/semantic-model |
486
+ | Core concepts | https://strata.do/developer-docs/developer-guide/getting-started/concepts |
487
+ | Glossary | https://strata.do/developer-docs/developer-guide/getting-started/glossary |
488
+ | Tables | https://strata.do/developer-docs/developer-guide/semantic-model/tables |
489
+ | Fields | https://strata.do/developer-docs/developer-guide/semantic-model/fields |
490
+ | Expressions (SQL) | https://strata.do/developer-docs/developer-guide/semantic-model/expressions/sql |
491
+ | Relationships | https://strata.do/developer-docs/developer-guide/semantic-model/relationships/cardinality |
492
+ | Imports | https://strata.do/developer-docs/developer-guide/semantic-model/imports |
493
+ | Compound measures | https://strata.do/developer-docs/developer-guide/semantic-model/fields/measures/compound |
494
+ | Snapshot measures | https://strata.do/developer-docs/developer-guide/semantic-model/fields/measures/snapshot |
495
+ | Inclusions | https://strata.do/developer-docs/developer-guide/advanced/inclusions |
496
+ | Exclusions | https://strata.do/developer-docs/developer-guide/advanced/exclusions |
497
+ | Partitions | https://strata.do/developer-docs/developer-guide/advanced/partitions |
498
+ | Cost optimization | https://strata.do/developer-docs/developer-guide/advanced/cost-optimization |
499
+ | Extended blending | https://strata.do/developer-docs/developer-guide/semantic-model/expressions/extended-blending |
500
+ | CLI audit | https://strata.do/developer-docs/developer-guide/cli/audit |
501
+ | CLI deployment | https://strata.do/developer-docs/developer-guide/cli/deployment |
502
+ | CLI migrations | https://strata.do/developer-docs/developer-guide/cli/migrations |
503
+ | Star schema example | https://strata.do/developer-docs/developer-guide/examples/patterns/star-schema |
504
+ | TPC-DS tutorial | https://strata.do/developer-docs/developer-guide/examples/tpcds-tutorial |
505
+ | AI agents and Strata | https://strata.do/developer-docs/developer-guide/api/ai-agents |
@@ -70,8 +70,13 @@ fields:
70
70
  # # Optional: UI rendering of the field
71
71
  # display_type: default|html|url|email|phone_number|image
72
72
  #
73
- # # Optional: Specific formatting to be applied (will supercede display_type)
74
- # formatter: currency_usd|percent|thousands|millions|billions|(custom js function using numeraljs/momentjs)
73
+ # # Optional: value formatting (shortcut string or hash with type)
74
+ # format: currency:2
75
+ # # format:
76
+ # # type: percent
77
+ # # precision: 1
78
+ # # Types: number, currency, percent, date, datetime, html, javascript
79
+ # # Shortcuts: number:2, number:2:abbreviate, currency:2, percent:1, date:short, datetime:iso
75
80
  #
76
81
  # # Optional: disable listing individual elements (dimension only). Good to do that for
77
82
  # # high cardinality columns like account_id
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../agent_output"
4
+
3
5
  module Strata
4
6
  module CLI
5
7
  module DatasourceHelper
@@ -24,6 +26,19 @@ module Strata
24
26
  # 3. Check available datasources
25
27
  ds_keys = datasources.keys
26
28
 
29
+ if agent_mode?
30
+ if ds_keys.empty?
31
+ agent_emit_no_datasources!
32
+ elsif ds_keys.length == 1
33
+ return ds_keys.first
34
+ else
35
+ AgentOutput.emit_error(
36
+ "Datasource key required. Available: #{ds_keys.join(", ")}",
37
+ code: "datasource_key_required"
38
+ )
39
+ end
40
+ end
41
+
27
42
  if ds_keys.empty?
28
43
  say "No datasources configured. Run 'strata datasource add' first.", :red
29
44
  nil
@@ -134,6 +149,17 @@ module Strata
134
149
 
135
150
  private
136
151
 
152
+ def agent_emit_no_datasources!
153
+ AgentOutput.emit_error(
154
+ "No datasources configured in datasources.yml. Ask the user to add one interactively — " \
155
+ "do not create datasources or write credentials yourself (secrets belong in .strata via CLI prompts). " \
156
+ "User command: strata datasource add [ADAPTER] (e.g. strata datasource add postgres, strata datasource add duckdb).",
157
+ code: "no_datasources",
158
+ user_action_required: true,
159
+ suggested_command: "strata datasource add [ADAPTER]"
160
+ )
161
+ end
162
+
137
163
  def validate_datasource(ds_key)
138
164
  unless datasources[ds_key]
139
165
  say "Error: Datasource '#{ds_key}' not found in datasources.yml", :red
@@ -9,6 +9,7 @@ require_relative "sub_commands/table"
9
9
  require_relative "sub_commands/create"
10
10
  require_relative "sub_commands/audit"
11
11
  require_relative "helpers/description_helper"
12
+ require_relative "agent_mode"
12
13
  require_relative "output"
13
14
 
14
15
  module Strata
@@ -16,6 +17,7 @@ module Strata
16
17
  class Main < Thor
17
18
  include Guard
18
19
  include Output
20
+ include AgentMode
19
21
  extend Helpers::DescriptionHelper
20
22
 
21
23
  def self.exit_on_failure?
@@ -60,11 +62,6 @@ module Strata
60
62
  desc "table", "Manage semantic tables"
61
63
  subcommand "table", SubCommands::Table
62
64
 
63
- desc "tables", "List all semantic models in the project"
64
- def tables
65
- SubCommands::Table.start(["list"])
66
- end
67
-
68
65
  desc "audit", "Audit project configuration and models"
69
66
  subcommand "audit", SubCommands::Audit
70
67
 
@@ -5,6 +5,7 @@ require_relative "../terminal"
5
5
  require_relative "../credentials"
6
6
  require_relative "../output"
7
7
  require_relative "../helpers/datasource_helper"
8
+ require_relative "../agent_mode"
8
9
  require_relative "../utils/yaml_import_resolver"
9
10
  require_relative "../utils/import_manager"
10
11
  require "yaml"
@@ -19,11 +20,20 @@ module Strata
19
20
  include Terminal
20
21
  include Output
21
22
  include DatasourceHelper
23
+ include AgentMode
22
24
 
23
25
  REQUIRED_KEYS_FOR_TABLE_MODEL = %w[name physical_name fields datasource].freeze
24
26
  REQUIRED_KEYS_FOR_RELATIONSHIP_MODEL = ["datasource"].freeze
25
27
  REQUIRED_KEYS_FOR_RELATIONSHIP_DEFINITION = %w[left right sql cardinality].freeze
26
28
  RELATIONSHIP_CARDINALITIES = %w[one_to_one one_to_many many_to_one many_to_many].freeze
29
+ VALID_FORMAT_TYPES = %w[raw number currency percent date datetime html javascript].freeze
30
+ ALLOWED_FIELD_KEYS = %w[
31
+ type name description hidden grains data_type display_type format
32
+ secure disable_listing value_list_size snapshot exclusion_type
33
+ exclusions inclusions extended_blend_group synonyms expression
34
+ ].freeze
35
+ ALLOWED_EXPRESSION_KEYS = %w[sql array lookup primary_key].freeze
36
+ EXPRESSION_BOOLEAN_KEYS = %w[array lookup primary_key].freeze
27
37
 
28
38
  # Set default command so `strata audit` still works as `strata audit all`
29
39
  default_command :all
@@ -99,8 +109,8 @@ module Strata
99
109
  def audit_models
100
110
  failures = []
101
111
  Dir.glob("models/**/*.yml").each do |file|
102
- audit_model_file(file, failures)
103
112
  audit_imports(file, failures)
113
+ audit_model_file(file, failures)
104
114
  end
105
115
  failures
106
116
  end
@@ -109,17 +119,21 @@ module Strata
109
119
  content = YAML.safe_load_file(file, permitted_classes: [Date, Time], aliases: true) || {}
110
120
  return unless content.is_a?(Hash)
111
121
 
112
- validate_structure(content, file, failures)
113
-
114
122
  case model_type(file)
115
123
  when :table
116
- validate_table_model(content, file, failures)
124
+ resolved = Utils::YamlImportResolver.resolve(file, Dir.pwd)
125
+ return unless resolved.is_a?(Hash)
126
+
127
+ validate_structure(resolved, file, failures)
128
+ validate_table_model(resolved, file, failures)
117
129
  when :relationship
130
+ validate_structure(content, file, failures)
118
131
  validate_relationship_model(content, file, failures)
119
132
  end
120
- rescue
121
- # Ignore errors here, they are handled in audit_yaml_syntax if needed
122
- # or simply skipped if the file is unreadable
133
+ rescue Strata::ImportError => e
134
+ failures << {file: file, message: e.message}
135
+ rescue Psych::SyntaxError
136
+ # Handled by audit_yaml_syntax when run
123
137
  end
124
138
 
125
139
  def model_type(file)
@@ -140,7 +154,7 @@ module Strata
140
154
  def validate_table_model(content, file, failures)
141
155
  validate_required_keys(content, file, REQUIRED_KEYS_FOR_TABLE_MODEL, failures)
142
156
  validate_datasource_reference(content, file, failures)
143
- validate_table_fields(content["fields"], file, failures) if content.key?("fields")
157
+ validate_table_fields(content["fields"], file, failures)
144
158
  end
145
159
 
146
160
  def validate_relationship_model(content, file, failures)
@@ -172,10 +186,119 @@ module Strata
172
186
  return
173
187
  end
174
188
 
189
+ if fields.empty?
190
+ failures << {file: file, message: "'fields' must not be empty"}
191
+ return
192
+ end
193
+
175
194
  fields.each_with_index do |field, idx|
176
195
  unless field.is_a?(Hash) && field.key?("name")
177
196
  failures << {file: file, message: "Field at index #{idx} missing 'name'"}
197
+ next
198
+ end
199
+
200
+ validate_field_definition(field, file, idx, failures)
201
+ end
202
+ end
203
+
204
+ def validate_field_definition(field, file, idx, failures)
205
+ field_name = field["name"] || "index #{idx}"
206
+
207
+ unknown_keys = field.keys.map(&:to_s) - ALLOWED_FIELD_KEYS
208
+ if unknown_keys.any?
209
+ failures << {
210
+ file: file,
211
+ message: "Field '#{field_name}' has unknown #{(unknown_keys.size == 1) ? "key" : "keys"}: #{unknown_keys.sort.join(", ")}"
212
+ }
213
+ end
214
+
215
+ validate_field_expression(field_name, field["expression"], file, failures)
216
+
217
+ return unless field.key?("format")
218
+
219
+ validate_field_format(field_name, field["format"], file, failures)
220
+ end
221
+
222
+ def validate_field_expression(field_name, expression, file, failures)
223
+ if expression.nil?
224
+ failures << {file: file, message: "Field '#{field_name}' missing required 'expression'"}
225
+ return
226
+ end
227
+
228
+ if expression.is_a?(String)
229
+ if expression.strip.empty?
230
+ failures << {file: file, message: "Field '#{field_name}' expression must not be empty"}
231
+ end
232
+ return
233
+ end
234
+
235
+ unless expression.is_a?(Hash)
236
+ type_name = expression.class.name
237
+ failures << {
238
+ file: file,
239
+ message: "Field '#{field_name}' expression must be a string shortcut or a mapping (got #{type_name})"
240
+ }
241
+ return
242
+ end
243
+
244
+ unknown_keys = expression.keys.map(&:to_s) - ALLOWED_EXPRESSION_KEYS
245
+ if unknown_keys.any?
246
+ failures << {
247
+ file: file,
248
+ message: "Field '#{field_name}' expression has unknown #{(unknown_keys.size == 1) ? "key" : "keys"}: " \
249
+ "#{unknown_keys.sort.join(", ")}"
250
+ }
251
+ end
252
+
253
+ sql = expression["sql"] || expression[:sql]
254
+ if sql.nil? || sql.to_s.strip.empty?
255
+ failures << {file: file, message: "Field '#{field_name}' expression must include non-empty 'sql'"}
256
+ end
257
+
258
+ EXPRESSION_BOOLEAN_KEYS.each do |key|
259
+ next unless expression.key?(key) || expression.key?(key.to_sym)
260
+
261
+ value = expression[key] || expression[key.to_sym]
262
+ next if value == true || value == false
263
+
264
+ failures << {
265
+ file: file,
266
+ message: "Field '#{field_name}' expression.#{key} must be true or false"
267
+ }
268
+ end
269
+ end
270
+
271
+ def validate_field_format(field_name, format_value, file, failures)
272
+ case format_value
273
+ when String
274
+ shortcut = format_value.strip
275
+ if shortcut.empty?
276
+ failures << {file: file, message: "Field '#{field_name}' format must not be empty"}
277
+ return
278
+ end
279
+
280
+ type = shortcut.split(":", 2).first
281
+ unless VALID_FORMAT_TYPES.include?(type)
282
+ failures << {
283
+ file: file,
284
+ message: "Field '#{field_name}' format shortcut has invalid type '#{type}' (expected one of: #{VALID_FORMAT_TYPES.join(", ")})"
285
+ }
286
+ end
287
+ when Hash
288
+ type = format_value["type"] || format_value[:type]
289
+ if type.nil? || type.to_s.strip.empty?
290
+ failures << {file: file, message: "Field '#{field_name}' format hash must include type"}
291
+ elsif !VALID_FORMAT_TYPES.include?(type.to_s)
292
+ failures << {
293
+ file: file,
294
+ message: "Field '#{field_name}' format hash has invalid type '#{type}' (expected one of: #{VALID_FORMAT_TYPES.join(", ")})"
295
+ }
178
296
  end
297
+ else
298
+ failures << {
299
+ file: file,
300
+ message: "Field '#{field_name}' format must be a string shortcut or a hash with type"
301
+ }
179
302
  end
180
303
  end
181
304
 
@@ -254,13 +377,9 @@ module Strata
254
377
  imported_content = YAML.safe_load_file(resolved_path, permitted_classes: [Date, Time], aliases: true) || {}
255
378
 
256
379
  unless imported_content.is_a?(Hash)
257
- failures << {file: file, message: "Imported file '#{import_path}' does not contain valid YAML hash"}
380
+ failures << {file: file, message: "Imported file '#{import_path}' does not contain valid YAML"}
258
381
  next
259
382
  end
260
-
261
- if model_type(file) == :table && imported_content["fields"]
262
- validate_table_fields(imported_content["fields"], file, failures)
263
- end
264
383
  rescue Strata::InvalidImportPathError => e
265
384
  failures << {file: file, message: e.message}
266
385
  rescue Strata::MissingImportError
@@ -6,6 +6,7 @@ require_relative "../api/client"
6
6
  require_relative "../utils/git"
7
7
  require "time"
8
8
  require "tty-prompt"
9
+ require_relative "../agent_mode"
9
10
 
10
11
  module Strata
11
12
  module CLI
@@ -13,6 +14,7 @@ module Strata
13
14
  class Branch < Thor
14
15
  include Guard
15
16
  include Output
17
+ include AgentMode
16
18
 
17
19
  default_command :list
18
20
  class_option :environment, aliases: ["e"], type: :string
@@ -10,6 +10,7 @@ require_relative "../ai/services/table_generator"
10
10
  require_relative "../helpers/prompts"
11
11
  require_relative "../helpers/description_helper"
12
12
  require_relative "../helpers/command_context"
13
+ require_relative "../agent_mode"
13
14
  require "tty-prompt"
14
15
  require "tty-spinner"
15
16
  require "yaml"
@@ -25,6 +26,7 @@ module Strata
25
26
  include Prompts
26
27
  include Thor::Actions
27
28
  include Helpers::CommandContext
29
+ include AgentMode
28
30
  extend Helpers::DescriptionHelper
29
31
 
30
32
  desc "table TABLE_PATH", "Create a semantic table model"
@@ -34,6 +36,11 @@ module Strata
34
36
  desc: "Datasource key from datasources.yml"
35
37
 
36
38
  def table(table_path = nil)
39
+ reject_agent_mode!(
40
+ "Agent mode: write models/tbl.*.yml directly. Use: strata datasource meta DS_KEY TABLE_NAME --agent",
41
+ code: "use_yaml_files"
42
+ )
43
+
37
44
  return unless datasource_key
38
45
 
39
46
  # If table_path provided, skip search for speed
@@ -51,6 +58,11 @@ module Strata
51
58
  desc: "Datasource key from datasources.yml"
52
59
 
53
60
  def relation(relation_path)
61
+ reject_agent_mode!(
62
+ "Agent mode: write models/rel.*.yml directly.",
63
+ code: "use_yaml_files"
64
+ )
65
+
54
66
  return unless datasource_key
55
67
 
56
68
  create_relation_file(relation_path)
@@ -8,6 +8,7 @@ require_relative "../utils/git"
8
8
  require "tty-prompt"
9
9
  require_relative "../helpers/datasource_helper"
10
10
  require_relative "../helpers/description_helper"
11
+ require_relative "../agent_mode"
11
12
 
12
13
  module Strata
13
14
  module CLI
@@ -18,6 +19,7 @@ module Strata
18
19
  include Terminal
19
20
  include Output
20
21
  include DatasourceHelper
22
+ include AgentMode
21
23
  extend Helpers::DescriptionHelper
22
24
 
23
25
  desc "adapters", "Lists supported data warehouse adapters"
@@ -52,7 +54,7 @@ module Strata
52
54
  return
53
55
  end
54
56
 
55
- names = ds.keys.map { "#{it} => #{ds[it]["name"]}" }
57
+ names = ds.keys.map { "Key: #{it} | Name: #{ds[it]["name"]}" }
56
58
  out = "\n #{names.join("\n ")}"
57
59
  say out, :magenta
58
60
  end
@@ -184,11 +186,17 @@ module Strata
184
186
  ds_key = resolve_datasource(ds_key, prompt: prompt)
185
187
  return unless ds_key
186
188
 
187
- say "\nListing #{ds_key} tables...\n\n", :yellow
188
189
  adapter = create_adapter(ds_key)
189
190
  tables = adapter.tables(**options)
190
191
  tables = tables.select { it =~ /#{options[:pattern]}/ } if options[:pattern]
191
192
 
193
+ if agent_mode?
194
+ AgentOutput.emit_json({datasource: ds_key, tables: tables})
195
+ return
196
+ end
197
+
198
+ say "\nListing #{ds_key} tables...\n\n", :yellow
199
+
192
200
  if tables.empty?
193
201
  say "No tables found.", :yellow
194
202
  return
@@ -205,10 +213,18 @@ module Strata
205
213
  method_option :catalog, aliases: "c", type: :string, desc: "Change the catalog from the configured one."
206
214
  method_option :schema, aliases: "s", type: :string, desc: "Change the schema from the configured one."
207
215
  def meta(ds_key, table_name)
208
- say "\nā— Schema for table: #{table_name} (#{ds_key}):\n", :yellow
209
216
  adapter = create_adapter(ds_key)
210
217
  md = adapter.metadata(table_name, **options.transform_keys(&:to_sym))
211
218
 
219
+ if agent_mode?
220
+ columns = md.columns.map do |column|
221
+ column.to_h.merge(normalized_data_type: column.normalized_data_type)
222
+ end
223
+ AgentOutput.emit_json({datasource: ds_key, table: table_name, columns: columns})
224
+ return
225
+ end
226
+
227
+ say "\nā— Schema for table: #{table_name} (#{ds_key}):\n", :yellow
212
228
  headings = md.columns.first.to_h.keys
213
229
  rows = md.columns.map(&:to_h).map(&:values)
214
230
 
@@ -5,6 +5,7 @@ require_relative "../terminal"
5
5
  require_relative "../output"
6
6
  require_relative "../helpers/project_helper"
7
7
  require_relative "../helpers/description_helper"
8
+ require_relative "../agent_mode"
8
9
  require_relative "../api/client"
9
10
  require_relative "../credentials"
10
11
  require_relative "../utils"
@@ -23,6 +24,7 @@ module Strata
23
24
  include Guard
24
25
  include Terminal
25
26
  include Output
27
+ include AgentMode
26
28
  extend Helpers::DescriptionHelper
27
29
 
28
30
  default_command :deploy
@@ -4,6 +4,7 @@ require_relative "../guard"
4
4
  require_relative "../terminal"
5
5
  require_relative "../output"
6
6
  require_relative "../helpers/project_helper"
7
+ require_relative "../agent_mode"
7
8
  require_relative "../api/client"
8
9
  require "yaml"
9
10
 
@@ -14,6 +15,7 @@ module Strata
14
15
  include Guard
15
16
  include Terminal
16
17
  include Output
18
+ include AgentMode
17
19
 
18
20
  desc "link PROJECT_ID", "Link local project to an existing project on the server"
19
21
  def link(project_id)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../helpers/prompts"
4
4
  require_relative "../helpers/command_context"
5
+ require_relative "../agent_mode"
5
6
  require_relative "../output"
6
7
 
7
8
  module Strata
@@ -14,6 +15,7 @@ module Strata
14
15
  include Prompts
15
16
  include Thor::Actions
16
17
  include Helpers::CommandContext
18
+ include AgentMode
17
19
  extend Helpers::DescriptionHelper
18
20
 
19
21
  desc "list", "List all semantic models in the project"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Strata
4
4
  module CLI
5
- VERSION = "0.1.9"
5
+ VERSION = "0.1.11"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strata-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ajo Abraham
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.3.0
19
+ version: 0.4.2
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.3.0
26
+ version: 0.4.2
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: aws-sdk-athena
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -242,6 +242,8 @@ files:
242
242
  - Rakefile
243
243
  - exe/strata
244
244
  - lib/strata/cli.rb
245
+ - lib/strata/cli/agent_mode.rb
246
+ - lib/strata/cli/agent_output.rb
245
247
  - lib/strata/cli/ai/client.rb
246
248
  - lib/strata/cli/ai/configuration.rb
247
249
  - lib/strata/cli/ai/services/table_generator.rb