dataspring-cli 0.4.2__py3-none-any.whl → 0.4.3__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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dataspring-author
3
- description: Conventions for writing dbt YAML descriptions and the org's business_context doc. Use when editing dbt/models/**.yml, dbt/context/business_context.md, or any file that feeds DataSpring's semantic layer or org context.
3
+ description: Building and documenting an org's data in DataSpring - the datacore loop (pipelines, connections, dbt models, metrics; pull, push, check, deploy, build, run, backfills), dbt YAML descriptions and the business_context doc. Use when editing a datacore workspace or dbt/models/**.yml, writing metrics, loading or backfilling a pipeline, or any file that feeds DataSpring's semantic layer or org context.
4
4
  ---
5
5
 
6
6
  # Authoring DataSpring's data-layer context
@@ -9,6 +9,28 @@ You are editing files that an analytical agent will read at query time.
9
9
  Lazy descriptions silently degrade every query that touches the
10
10
  affected entity. This skill exists so the conventions stay tight.
11
11
 
12
+ ## Before you start: sign in and name the org
13
+
14
+ Every command and connection acts in ONE org, named each time; nothing
15
+ switches it for later. CLI: `dataspring auth login` prints a URL and a code
16
+ at once (run it in the background and hand both to the person), then
17
+ `--org <org>`, `DATASPRING_ORG`, or `{"org": "<org>"}` in a
18
+ `.forspring.json` at the workspace root, which is the natural place:
19
+
20
+ ```bash
21
+ echo '{"org": "acme"}' > .forspring.json && dataspring whoami --json
22
+ ```
23
+
24
+ MCP: one connection per org, `https://dataspring.app/api/mcp/?org=<org>`, or
25
+ `Authorization: Bearer ds_...` for a headless agent (the key's own org).
26
+
27
+ A refusal leads with a reason and ends with its fix; match the reason. The
28
+ ones you meet while building: `role_too_low` (connections, pipelines,
29
+ deploy, build, run, cancel and reset need admin; a member can edit models
30
+ and check), `org_required` (ask which org, never guess), `not_signed_in`
31
+ (log in again). The full table, with the CLI exit codes, is in
32
+ `dataspring-consume`.
33
+
12
34
  ## 1. The contract
13
35
 
14
36
  The descriptions on metrics, dimensions, and semantic models are
@@ -206,7 +228,44 @@ version produced each issue. Edit the board and the next issue follows.
206
228
  and is what stops later history columns moving. Rendering and checking
207
229
  prose is the consumer's job and is covered in `dataspring-consume`.
208
230
 
209
- ## 8.2 Iterating on a dbt model (managed tenants)
231
+ ## 8.2 The datacore loop (managed tenants)
232
+
233
+ A managed org's pipelines, connections and dbt project are one workspace of
234
+ files. Edit, `check`, `deploy`; nothing an edit or a check does reaches
235
+ production.
236
+
237
+ ```bash
238
+ dataspring datacore pull ./ws # deployed files, each exactly as pushed
239
+ dataspring datacore push ./ws # changed files only, into a new draft
240
+ dataspring datacore check # waits; exits 1 unless it passes
241
+ dataspring datacore deploy --then-build # admin; rebuilds changed models too
242
+ ```
243
+
244
+ - **A new org** pulls its starting workspace (`datacore.yaml`, empty
245
+ `pipelines/`, `connections/`, `models/`); `datacore
246
+ status` says `deployed none yet` until the first deploy.
247
+ - **A check** compiles every pipeline, builds the changed models and their
248
+ tests into `<org>_dev`, and dry-runs their SQL. It runs as an identity
249
+ that can write `<org>_dev` only: a project whose `generate_schema_name`
250
+ puts a model in another dataset is refused, naming the models. A
251
+ workspace with no `models/dbt_project.yml` yet passes with dbt `skipped`,
252
+ so land raw data first and add models after.
253
+ - **The semantic manifest is checked per item**, each finding as `FAIL metric
254
+ <name>: <message>` (`check.dbt.failures` on the run). It is also
255
+ dry-imported as a load would import it, so what passes the check imports.
256
+ A ratio metric's `numerator` and `denominator` are metrics, not measures
257
+ (MetricFlow's definition), as are a derived metric's inputs:
258
+
259
+ ```yaml
260
+ - name: gross_margin_pct
261
+ type: ratio
262
+ type_params: {numerator: gross_margin, denominator: revenue} # two metrics
263
+ ```
264
+
265
+ - **Deploy** needs a green check of that exact draft (any edit makes a new
266
+ draft) and starts a run of each windowed pipeline it changed.
267
+
268
+ ## 8.3 Iterating on a dbt model (managed tenants)
210
269
 
211
270
  A model change does not need a load. The loop is `datacore_edit` (kind
212
271
  `model`), `datacore_run` `check`, `deploy`, then `build`: a run that loads
@@ -218,6 +277,68 @@ under `models/`. Do not use `run` to see a model change: it reloads every
218
277
  resource of the pipeline first. CLI: `dataspring datacore build --select
219
278
  "marts.finance+" --wait`.
220
279
 
280
+ **Look before you write, and verify before you deploy.** `run_sql`
281
+ (`dataspring sql`) runs as the org's read identity, which reads the org's
282
+ own `<org>_raw` (what the loads wrote), `<org>_staging` (the staging views
283
+ over raw), `<org>_core`, `<org>_marts` and `<org>_dev` (what the last
284
+ `check` built), and nothing else; `<org>` is the org id with `-` as `_`.
285
+
286
+ - Before writing a staging model, list the raw columns that actually
287
+ exist: `dataspring datacore schema --table <t>` (or
288
+ `dataspring://datacore/schema`). A field that was null on every row
289
+ loaded has no column at all, so a model written against the source API's
290
+ documentation can fail with `Unrecognized name`; select a typed null for
291
+ it instead. `dataspring datacore freshness` gives row counts and the run
292
+ that wrote each table; `dataspring datacore sample <t>` some rows.
293
+ - After a `check`, the models it built (the changed ones and everything
294
+ downstream) are tables and views in `<org>_dev` for 7 days: compare their
295
+ numbers with the source before `deploy` puts them where dashboards read,
296
+ e.g. `dataspring sql "SELECT * FROM <org>_dev.fct_project LIMIT 20"` or
297
+ `dataspring datacore schema --dataset dev`.
298
+ - A script of several statements runs as one job and answers the last
299
+ one's rows; `CREATE TEMP TABLE` in it is scratch space for an analysis
300
+ that ends with the script.
301
+
302
+ ## 8.4 Workspace files keep the customer's text
303
+
304
+ The customer's comments in a pipeline, connection or dbt YAML file are
305
+ its documentation. To replace a connection or pipeline whole, send
306
+ `datacore_edit` `apply` the YAML text as `content`: it is validated like a
307
+ `document` and stored byte for byte, so `dataspring datacore pull` returns
308
+ it as written. A `document` (an object) is stored as a fresh dump with no
309
+ comments; use it only for a file you are creating. `patch`, and a
310
+ `metric_edit` / `semantic_model_edit` update or delete in a shared dbt
311
+ file, replace only the lines of the keys or list entries that changed;
312
+ the rest of the file, mixed indentation included, stays byte for byte.
313
+ When that cannot be done (a flow-style document, say) the answer carries
314
+ `reformatted`: the whole file was rewritten in one indentation style.
315
+ Tell the user the diff is large for that reason. `dataspring datacore
316
+ push` sends each changed file as its text the same way, and skips a file
317
+ whose bytes equal the stored one's.
318
+
319
+ ## 8.5 Loads, windows and backfills
320
+
321
+ - **Windows are whole inclusive days**, the run's and every chunk's. When an
322
+ API reads its end date as exclusive (`to=2025-03-31` misses 31 March), the
323
+ resource says so and is sent the day after each window's last day:
324
+
325
+ ```yaml
326
+ window: { lookback: 7d, param_start: createdFrom, param_end: createdTo, end: exclusive }
327
+ ```
328
+
329
+ Test an API's end bound with a one-day window before a backfill: a wrong
330
+ guess silently drops the last day of every chunk.
331
+ - **A backfill** is `dataspring datacore run <pipeline> --start ... --end ...
332
+ --chunk 1M` (or `3M`, `7d`, `2w`): one child run per chunk, each started as
333
+ the previous one ends. A chunk before the last loads only the windowed
334
+ resources (and those they read rows from) and skips dbt; the last chunk
335
+ loads everything and builds the models once. A failed child fails the
336
+ backfill. One backfill per pipeline at a time.
337
+ - **Stop** a queued or running run, or a whole backfill, with `dataspring
338
+ datacore cancel <run_id>` (admin); the schedule fires again at its next slot.
339
+ - **`reset`** drops a pipeline's raw tables and load state. It is
340
+ irreversible: ask first.
341
+
221
342
  ## 9. Parameter reference
222
343
 
223
344
  <!-- dispatch-params: semantic_model_edit metric_edit board_edit briefing_edit import_manifest business_context_edit warehouse_edit datacore_edit datacore_run -->
@@ -227,14 +348,14 @@ _Generated from the dispatch registry (`GET /api/dispatch`); do not edit by hand
227
348
 
228
349
  MCP tool `semantic_model_edit`; `POST /api/dispatch/semantic_model_edit`. Edit semantic models in the org manifest. Admin/owner only for writes. The body is `{"action": {"action": "<verb>", ...}}`, one verb per table below.
229
350
 
230
- **`create`** —
351
+ **`create`** — Create a semantic model (admin/owner).
231
352
 
232
353
  | Parameter | Type | Required | Description |
233
354
  |---|---|---|---|
234
355
  | `model_data` | object | yes | Full semantic-model definition (name, measures, dimensions, entities) |
235
356
  | `expect_version` | string | no | Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale |
236
357
 
237
- **`update`** —
358
+ **`update`** — Change a semantic model (admin/owner). On an imported one a change to how it computes answers a dbt YAML proposal and writes nothing; descriptions write at once.
238
359
 
239
360
  | Parameter | Type | Required | Description |
240
361
  |---|---|---|---|
@@ -243,7 +364,7 @@ MCP tool `semantic_model_edit`; `POST /api/dispatch/semantic_model_edit`. Edit s
243
364
  | `confirmed` | boolean | no, default `False` | set true only after the user confirmed a redefinition of a NATIVE entity whose impact you showed them; has no effect on imported entities |
244
365
  | `expect_version` | string | no | Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale |
245
366
 
246
- **`delete`** —
367
+ **`delete`** — Delete a semantic model (admin/owner).
247
368
 
248
369
  | Parameter | Type | Required | Description |
249
370
  |---|---|---|---|
@@ -260,14 +381,14 @@ MCP tool `semantic_model_edit`; `POST /api/dispatch/semantic_model_edit`. Edit s
260
381
 
261
382
  MCP tool `metric_edit`; `POST /api/dispatch/metric_edit`. Edit metrics in the org manifest. Admin/owner only for writes. The body is `{"action": {"action": "<verb>", ...}}`, one verb per table below.
262
383
 
263
- **`create`** —
384
+ **`create`** — Create a metric, native to DataSpring (admin/owner). A ratio's numerator and denominator, a derived metric's inputs and a cumulative or conversion metric's base are metrics, not measures.
264
385
 
265
386
  | Parameter | Type | Required | Description |
266
387
  |---|---|---|---|
267
388
  | `metric_data` | object | yes | Metric definition (name, type, type_params) |
268
389
  | `expect_version` | string | no | Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale |
269
390
 
270
- **`update`** —
391
+ **`update`** — Change a metric (admin/owner). On an imported one a change to how it computes answers a dbt YAML proposal and writes nothing; descriptions write at once.
271
392
 
272
393
  | Parameter | Type | Required | Description |
273
394
  |---|---|---|---|
@@ -276,14 +397,14 @@ MCP tool `metric_edit`; `POST /api/dispatch/metric_edit`. Edit metrics in the or
276
397
  | `confirmed` | boolean | no, default `False` | set true only after the user confirmed a redefinition of a NATIVE entity whose impact you showed them; has no effect on imported entities |
277
398
  | `expect_version` | string | no | Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale |
278
399
 
279
- **`delete`** —
400
+ **`delete`** — Delete a metric (admin/owner; irreversible).
280
401
 
281
402
  | Parameter | Type | Required | Description |
282
403
  |---|---|---|---|
283
404
  | `name` | string | yes | |
284
405
  | `expect_version` | string | no | Managed tenants only: the datacore version (draft snapshot id) this edit was written against, from the previous edit's `version` or dataspring://datacore/files; refused with the current one when stale |
285
406
 
286
- **`preview`** —
407
+ **`preview`** — Test a metric before saving (any role; writes nothing): sample query results or validation errors, plus the `dbt_yaml` that would be committed for it.
287
408
 
288
409
  | Parameter | Type | Required | Description |
289
410
  |---|---|---|---|
@@ -430,17 +551,17 @@ MCP tool `warehouse_edit`; `POST /api/dispatch/warehouse_edit`. Manage the org's
430
551
 
431
552
  MCP tool `datacore_edit`; `POST /api/dispatch/datacore_edit`. Edit the datacore workspace: connections, pipelines and the dbt project of a managed tenant, as files. ``kind`` picks the family: The body is `{"action": {"action": "<verb>", ...}}`, one verb per table below.
432
553
 
433
- **`apply`** — Write one workspace file whole (create or replace): a connection or pipeline as a ``document`` (validated against its schema and the catalog), a dbt project file as ``content``.
554
+ **`apply`** — Write one workspace file whole (create or replace): a dbt project file as ``content``; a connection or pipeline as ``content`` (its YAML text, stored exactly as sent, comments and layout kept) or as a ``document`` (an object, stored as a fresh dump), validated against its schema and the catalog either way.
434
555
 
435
556
  | Parameter | Type | Required | Description |
436
557
  |---|---|---|---|
437
558
  | `kind` | one of `connection`, `pipeline`, `model` | yes | connection (admin), pipeline (admin) or model (member) |
438
559
  | `name` | string | yes | For kind connection or pipeline: the name, i.e. the file name without .yaml (connections/<name>.yaml, pipelines/<name>.yaml). For kind model: the path under models/, the dbt project (models/models/marts/orders.sql, models/models/staging/schema.yml, models/seeds/x.csv, models/dbt_project.yml); no traversal, .sql .yml .yaml .csv .md .txt .json, never target/ or dbt_packages/ |
439
560
  | `document` | object | no | kind connection: the whole document (connection.schema.json: kind, base_url, auth with secret NAMES). kind pipeline: the whole document (pipeline.schema.json: a catalog `connector` with config, secrets, overrides; or a `connection` reference, an inline `source` or named `sources`, with `resources`; schedule, checks, then). Secrets are named, never valued; every name must exist |
440
- | `content` | string | no | kind model: the whole file content |
561
+ | `content` | string | no | The whole file as text. kind model: required. kind connection or pipeline: the YAML text, validated like a document and stored byte for byte (comments and layout kept); give content or document, not both |
441
562
  | `expect_version` | string | no | The datacore version this edit was written against: the `version` the previous edit returned, or dataspring://datacore/files. An edit against a stale version is refused with the current one. Omit to skip the check. |
442
563
 
443
- **`patch`** — Apply a JSON merge patch (RFC 7386: a key set to null is removed) to an existing YAML file: a connection, a pipeline (a schedule, a window, a check), or a YAML file under models/ (not SQL).
564
+ **`patch`** — Apply a JSON merge patch (RFC 7386: a key set to null is removed) to an existing YAML file: a connection, a pipeline (a schedule, a window, a check), or a YAML file under models/ (not SQL). Only the lines of the keys the patch changes are rewritten; every other line, comments and indentation included, stays as written (an answer with ``reformatted`` says the file had to be rewritten whole).
444
565
 
445
566
  | Parameter | Type | Required | Description |
446
567
  |---|---|---|---|
@@ -461,13 +582,13 @@ MCP tool `datacore_edit`; `POST /api/dispatch/datacore_edit`. Edit the datacore
461
582
 
462
583
  MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check-then-deploy loop and its operations. The body is `{"action": {"action": "<verb>", ...}}`, one verb per table below.
463
584
 
464
- **`check`** — Check the current draft; no arguments beyond an optional note.
585
+ **`check`** — Check the current draft (member): compile every pipeline, build the changed dbt models into <org>_dev with their tests, and validate the semantic manifest per metric. Nothing reaches production.
465
586
 
466
587
  | Parameter | Type | Required | Description |
467
588
  |---|---|---|---|
468
589
  | `note` | string | no | Optional note, recorded on the check run |
469
590
 
470
- **`deploy`** —
591
+ **`deploy`** — Promote the checked draft to deployed (admin), or roll back to an earlier deployed snapshot; then_build also rebuilds changed models.
471
592
 
472
593
  | Parameter | Type | Required | Description |
473
594
  |---|---|---|---|
@@ -476,14 +597,14 @@ MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check
476
597
  | `then_build` | boolean | no, default `False` | When the deploy changed any file under models/, also queue a models-only build (the `build` action) of the newly deployed snapshot on the raw tables already loaded; build_select narrows it. Without it, a deploy that changed models answers with the command that does it |
477
598
  | `build_select` | string | no | With then_build: the dbt selector to rebuild (default: every model, `fqn:*`) |
478
599
 
479
- **`run`** —
600
+ **`run`** — Load a pipeline now (admin): its own window, a given window, or a chunked backfill whose chunks run back to back.
480
601
 
481
602
  | Parameter | Type | Required | Description |
482
603
  |---|---|---|---|
483
604
  | `pipeline` | string | yes | Pipeline name (a deployed pipelines/<name>.yaml) |
484
605
  | `window` | RunWindowParams | no | A date window to load instead of the pipeline's own; wider than backfill_chunk becomes a chunked backfill. Both ends are inclusive days, and so is every chunk; a resource whose API reads its end bound as exclusive declares `window.end: exclusive` in the pipeline and is sent the day after each chunk's last day |
485
606
  | `cursor` | RunCursorParams | no | {from: <value>}: a cursor to re-pull from |
486
- | `backfill_chunk` | string | no | Chunk size for a windowed run: 7d, 2w or 1M (calendar months, the default) |
607
+ | `backfill_chunk` | string | no | Chunk size for a windowed run: 7d, 2w or 1M (calendar months, the default). Every chunk before the last loads only the windowed resources (and the ones they read rows from) and skips its dbt step, each skip recorded with its reason on the child run; the last chunk loads every resource and builds the models once |
487
608
 
488
609
  **`build`** — Rebuild the deployed dbt models on the raw tables already loaded: no resource is loaded.
489
610
 
@@ -492,14 +613,14 @@ MCP tool `datacore_run`; `POST /api/dispatch/datacore_run`. The datacore's check
492
613
  | `pipeline` | string | no | A deployed pipeline whose `then` block names what to build (its dbt_select, dbt_exclude and tests). Omit to build every model of the deployed project |
493
614
  | `select` | string | no | A dbt selector (`dbt build --select`), e.g. `marts.finance+` or `+fct_postings`; replaces the pipeline's dbt_select and dbt_exclude. Default: the pipeline's then.dbt_select, or every model (`fqn:*`) when no pipeline is named |
494
615
 
495
- **`reset`** —
616
+ **`reset`** — Delete a pipeline's dlt state and drop its raw tables (admin; irreversible).
496
617
 
497
618
  | Parameter | Type | Required | Description |
498
619
  |---|---|---|---|
499
620
  | `pipeline` | string | yes | The pipeline whose dlt state and raw tables to remove |
500
621
  | `approve` | boolean | no, default `False` | Must be true: the reset is irreversible. Ask the user first |
501
622
 
502
- **`cancel`** —
623
+ **`cancel`** — Stop a queued or running run, or a whole backfill (admin); the schedule fires again at its next slot.
503
624
 
504
625
  | Parameter | Type | Required | Description |
505
626
  |---|---|---|---|
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: dataspring-consume
3
- description: How to query DataSpring correctly to answer analytical questions. Use when the user is asking analytical/business questions, when an agent is connected to a DataSpring MCP server, when running `dataspring query ...` from the terminal, or when scripting against DataSpring's REST API. Covers the three surfaces (MCP, CLI, REST) — same principles, different invocations.
3
+ description: How to query DataSpring correctly to answer analytical questions. Use when the user is asking analytical/business questions, when an agent is connected to a DataSpring MCP server, when running `dataspring query ...` from the terminal, or when scripting against DataSpring's REST API. Covers the three surfaces (MCP, CLI, REST) — same principles, different invocations — and signing in, naming the org, and what to do with a refusal.
4
4
  ---
5
5
 
6
6
  # Querying DataSpring to answer analytical questions
@@ -24,10 +24,10 @@ traces — departing from them produces misleading numbers.
24
24
  | List dimensions | `list_dimensions()` | `dataspring dimensions list` | `GET /api/semantic-layer/dimensions` |
25
25
  | Explain one metric | `explain_metric(metric_name)` | `dataspring metrics show <name>` | `GET /api/semantic-layer/metrics/<name>` |
26
26
  | Query | `query_metrics(metrics, dimensions?, grain?, start_date?, end_date?, where?, ...)` | `dataspring query -m <name> [-d ...] [-g month] ...` | `POST /api/dispatch/query_metrics` with the same fields |
27
- | Explain what was computed | `explain_query(metrics, ...)` (same fields as `query_metrics`) | (no CLI command yet) | `POST /api/dispatch/explain_query` |
27
+ | Explain what was computed | `explain_query(metrics, ...)` (same fields as `query_metrics`) | `dataspring explain_query --metrics <name> ...` | `POST /api/dispatch/explain_query` |
28
28
  | What DataSpring learned / undo | `learned_edit list` / `undo` | `dataspring learned list` | `GET /api/learned` |
29
- | Verified queries | `verified_query_edit list` / `record` | (no CLI command yet) | (no dedicated route) |
30
- | Read business_context | `dataspring://context` resource | `dataspring business-context get` | `GET /api/context` |
29
+ | Verified queries | `verified_query_edit list` / `record` | `dataspring verified_query_edit list` | `POST /api/dispatch/verified_query_edit` |
30
+ | Read business_context | `business_context_edit get` (also in the connection's instructions) | `dataspring business-context get` | `GET /api/context` |
31
31
  | Check business_context size | `business_context_edit show_size` | `dataspring business-context size` | (no dedicated route — use `GET /api/context` and measure) |
32
32
  | Org's published skill | (this skill, served via SKILL.md) | `dataspring skill instructions consume` | `GET /api/v1/skill/consume/instructions` |
33
33
  | Be woken when a number moves | `watch_edit apply` | `dataspring watch_edit apply --document-json @watch.yaml` | `POST /api/dispatch/watch_edit` |
@@ -51,15 +51,44 @@ reads is the one this deployment was built with:
51
51
 
52
52
  ```bash
53
53
  curl -fsSL https://dataspring.app/install.sh | sh # Windows: irm https://dataspring.app/install.ps1 | iex
54
- dataspring login
54
+ dataspring auth login
55
55
  dataspring skills install --all # into ~/.claude/skills/<name>/, stamped
56
56
  ```
57
57
 
58
58
  Run `dataspring skills install` again after `dataspring upgrade` (the CLI
59
- upgrades itself in place; `dataspring --version` says when the server has
60
- operations this CLI does not know). `dataspring skills diff` shows what
61
- drifted between the installed copy and the served one; `--offline` on any
62
- `skills` command uses the copy bundled in the CLI instead of the server.
59
+ upgrades itself in place through uv tool, pipx or pip, and prints the
60
+ commands to run when it cannot tell which installed it; `dataspring
61
+ --version` says when the server has operations this CLI does not know).
62
+ `dataspring skills diff` shows what drifted between the installed copy and
63
+ the served one; `--offline` on any `skills` command uses the copy bundled in
64
+ the CLI instead of the server.
65
+
66
+ ## Signing in and naming the org
67
+
68
+ People sign in with their Forspring account; who is in which org, with
69
+ which role, is the account's answer on every request. DataSpring has no
70
+ "current org": every command and every connection names one.
71
+
72
+ - **CLI.** `dataspring auth login` prints a URL and a code at once and waits
73
+ until a person approves it in a browser (the machine running the CLI needs
74
+ none). Run it in the background, hand the person the URL and the code, and
75
+ wait for it to exit. It stores a `ds_` key for the org they approve
76
+ (`--all-orgs`: one per org). With one key stored, commands act in that org.
77
+ With several, name the org on every command, in this order of precedence:
78
+ `--org <org>`, `DATASPRING_ORG` (then `FORSPRING_ORG`), or a
79
+ `.forspring.json` holding `{"org": "<org>"}` in the working directory or a
80
+ parent. `DATASPRING_API_KEY=ds_...` (CI) acts in the key's own org.
81
+ `dataspring whoami --json` says which org a command here acts in and why
82
+ (`org_from`). There is no command that switches org for later commands.
83
+ - **MCP.** One connection per org: `https://dataspring.app/api/mcp/?org=<org>`
84
+ (the slash before `?` is required), signed in with the Forspring account.
85
+ To work in two orgs, add two connections; no tool moves a connection. A
86
+ headless agent connects to the same URL with `Authorization: Bearer ds_...`;
87
+ the key acts in its own org, and `?org=` may only repeat it.
88
+ - **REST.** `Authorization: Bearer ds_...`; the key is the org.
89
+
90
+ Never pick an org for the person. When the org is ambiguous you are refused
91
+ with `org_required` and the list of their orgs (§11): ask which one.
63
92
 
64
93
  ## 1. Knowledge layers — consult before querying
65
94
 
@@ -271,6 +300,31 @@ two queries (before/after) and calling out the methodology change rather
271
300
  than silently quoting one misleading trend. Same rule regardless of
272
301
  surface — same warehouse, same boundaries.
273
302
 
303
+ ## 6.1 Say when a period is partial
304
+
305
+ A grouped query is clamped to the window you asked for, so its first and
306
+ last buckets can hold only part of their period. The answer then carries
307
+ `effective_window`: `covered` (the days the numbers cover, always the window
308
+ asked for), `partial_periods` (each cut-short bucket with the days it holds)
309
+ and a `note`. It is absent when every bucket is whole. Quarter to date at
310
+ quarter grain, or January to September at year grain, comes back as ONE
311
+ row labelled with the whole period:
312
+
313
+ ```json
314
+ "effective_window": {"covered": {"start": "2025-01-01", "end": "2025-09-30"},
315
+ "partial_periods": [{"bucket": "2025-01-01", "start": "2025-01-01", "end": "2025-09-30"}]}
316
+ ```
317
+
318
+ Label every partial bucket with the days it holds, beside its number, and
319
+ never compare it with a whole period as if it were one:
320
+
321
+ > 2025 to date (1 January to 30 September): 4.2M DKK, against 3.9M DKK for
322
+ > the same nine months of 2024.
323
+
324
+ For a like-for-like comparison send `compare_to: same_period_last_year` (or
325
+ `previous_period`), which compares the same days. `explain_query` reports the
326
+ same `effective_window` before you run anything.
327
+
274
328
  ## 7. Insights, not just data
275
329
 
276
330
  Don't just return raw query results. Interpret what the number
@@ -488,8 +542,8 @@ Examples of honest refusals:
488
542
 
489
543
  ## 11. Tool / response error recovery
490
544
 
491
- Errors come back with a `code` and a `suggestion` (canonical name; older
492
- paths still expose `hint` as an alias). Follow the suggestion.
545
+ A failed query comes back with an `error`, a `code` and a `suggestion`
546
+ (`hint` is an alias older clients read). Follow the suggestion.
493
547
 
494
548
  Codes you'll see:
495
549
 
@@ -523,10 +577,34 @@ replaced the sentence the reader actually needed. If you could not
523
577
  recover, say what you could not find out and what you would need — that
524
578
  is about THEM. If you did recover, just answer.
525
579
 
526
- Surface shape: **MCP** returns `{error, code, suggestion}` in the tool
527
- response (`hint` mirrors `suggestion` for back-compat); **CLI** prints
528
- "✗ Error: …" with a hint line, exit code 1; **REST** is HTTP 4xx/5xx with
529
- `{"detail": {"message", "code", "suggestion", ...}}`.
580
+ Surface shape: **MCP** returns `{error, code, suggestion}` as the tool
581
+ result; **REST** (`POST /api/dispatch/<tool>`) answers the same body with
582
+ HTTP 200, and a body the tool's schema refuses (or an operation that fails
583
+ outright) with 400 `{"detail": ...}`; **CLI** prints `✗ Error: …` with a `Hint:` line and exits 1.
584
+
585
+ ### Refusals: who you are, which org, what your role allows
586
+
587
+ A refusal is not a query error. It has a stable `reason`, a `note` saying
588
+ what happened and a `fix` saying what fixes it. **REST** answers it with the
589
+ reason's HTTP status and `{"ok": false, "reason", "note", "fix"}` (plus
590
+ `org`, and `contacts` and `fix_url` when the account named who can fix it);
591
+ **MCP** raises a tool error whose text is `<reason>: <note> Fix: <fix>`;
592
+ the **CLI** prints the same line and exits with the reason's code. Match on
593
+ the reason, never on the prose, and pass the fix on verbatim.
594
+
595
+ | Reason | CLI exit | What you do |
596
+ |---|---|---|
597
+ | `not_signed_in` | 4 | CLI: run `dataspring auth login` and hand the person its URL and code. MCP: reconnect and sign in. Never ask for a password or a key in chat. |
598
+ | `org_required` | 2 | Several orgs, none named. Ask the person which one (the refusal lists them); then `--org`, `DATASPRING_ORG` or `.forspring.json`. Never guess. |
599
+ | `not_a_member` | 3 | Check the org id's spelling against the list. Otherwise only an owner or admin of that org can add the person, at the Forspring account; say so and stop. |
600
+ | `product_off` | 4 | DataSpring is not switched on for this person in that org. Tell them who can switch it on (`contacts`, `fix_url`); do not retry. |
601
+ | `org_suspended` | 4 | Billing stopped DataSpring for the whole org. Tell them an owner can restart it on the billing page; do not retry. |
602
+ | `role_too_low` | 4 | Their role cannot do this. Offer what it can do (a personal preference or quick metric instead of an org-wide change), or name the role needed. |
603
+ | `org_mismatch` | 4 | The key or connection is for another org than the one named. Use that org's key or URL, or drop the conflicting `--org` / `?org=`. |
604
+ | `account_unavailable` | 1 | Forspring's account did not answer, so access could not be checked. Retry once after a few seconds; nothing changed. |
605
+
606
+ Other CLI exits: 0 success, 1 any other failure, 2 a usage error, 64 a
607
+ removed command (it prints what replaced it).
530
608
 
531
609
  ## 12. Multi-turn / multi-call efficiency
532
610
 
@@ -606,9 +684,13 @@ How it reaches you:
606
684
  `dataspring business-context size` shows the byte budget.
607
685
  - **REST**: `GET /api/context` returns `{"content": "..."}`.
608
686
 
687
+ - **MCP**: the connection's instructions carry it from the first turn;
688
+ `business_context_edit` `get` reads it again. (`dataspring://context` is
689
+ the user's own query preferences, not this document.)
690
+
609
691
  Treat `business_context` as part of the same authoritative layer
610
- as metric descriptions. Same provenance (dbt repo via the
611
- manifest workflow), same trust level.
692
+ as metric descriptions: the same trust level. It is edited in DataSpring
693
+ (`business_context_edit`), and an edit is live at once.
612
694
 
613
695
  That's the working agreement. Same principles, three surfaces —
614
696
  do this and the answers will be right.
@@ -699,11 +781,11 @@ MCP tool `explain_query`; `POST /api/dispatch/explain_query`. Show what a query
699
781
 
700
782
  #### `run_sql`
701
783
 
702
- MCP tool `run_sql`; `POST /api/dispatch/run_sql`. Run ONE read-only SQL statement against the org's warehouse and get the rows back, ungoverned.
784
+ MCP tool `run_sql`; `POST /api/dispatch/run_sql`. Run read-only SQL (one statement, or a script whose last statement answers) against the org's warehouse and get the rows back, ungoverned.
703
785
 
704
786
  | Parameter | Type | Required | Description |
705
787
  |---|---|---|---|
706
- | `sql` | string | yes | One SQL statement (BigQuery Standard SQL on a managed warehouse). Name tables as <org>_marts.<table>, <org>_core.<table> or <org>_staging.<table>, <org> being the organization id with every hyphen as an underscore (acme-dk is acme_dk_marts); the identity it runs as can read those and nothing else, so a write or a reach outside them is refused by the warehouse. |
788
+ | `sql` | string | yes | One SQL statement (BigQuery Standard SQL on a managed warehouse), or a script of several separated by ';' whose last statement's rows are returned (CREATE TEMP TABLE ... ; SELECT ... works: temp tables are the scratch space, gone when the script ends). Name tables as <org>_marts.<table>, <org>_core.<table>, <org>_staging.<table>, <org>_raw.<table> (what the loads wrote) or <org>_dev.<table> (what the last datacore check built; expire after 7 days), <org> being the organization id with every hyphen as an underscore (acme-dk is acme_dk_marts); the identity it runs as can read those five datasets of its own org and nothing else, so a write to a dataset or a reach outside them is refused by the warehouse. |
707
789
  | `warehouse` | string | no | One of the org's warehouses (see dataspring://warehouses); omit for the active one. |
708
790
  | `max_rows` | integer | no, default `1000` | Rows to return at most (default 1000, cap 10000). Aggregate in SQL rather than paging. |
709
791
  | `format` | one of `json`, `csv`, `markdown` | no, default `json` | 'json' answers rows in `data`; 'csv' or 'markdown' answer one document in `content`. |
@@ -766,7 +848,7 @@ MCP tool `update_context`; `POST /api/dispatch/update_context`. Update user pref
766
848
 
767
849
  MCP tool `quick_metric_edit`; `POST /api/dispatch/quick_metric_edit`. Edit user-defined quick metrics (arithmetic expressions over existing metrics). The body is `{"action": {"action": "<verb>", ...}}`, one verb per table below.
768
850
 
769
- **`create`** —
851
+ **`create`** — Save a personal quick metric: arithmetic over existing metrics, queried as `qm:<name>`.
770
852
 
771
853
  | Parameter | Type | Required | Description |
772
854
  |---|---|---|---|
@@ -774,7 +856,7 @@ MCP tool `quick_metric_edit`; `POST /api/dispatch/quick_metric_edit`. Edit user-
774
856
  | `expression` | string | yes | Arithmetic expression over existing metrics (e.g., "total_revenue / order_count") |
775
857
  | `description` | string | no | |
776
858
 
777
- **`update`** —
859
+ **`update`** — Change one of your quick metrics.
778
860
 
779
861
  | Parameter | Type | Required | Description |
780
862
  |---|---|---|---|
@@ -783,7 +865,7 @@ MCP tool `quick_metric_edit`; `POST /api/dispatch/quick_metric_edit`. Edit user-
783
865
  | `expression` | string | no | |
784
866
  | `description` | string | no | |
785
867
 
786
- **`delete`** —
868
+ **`delete`** — Delete one of your quick metrics.
787
869
 
788
870
  | Parameter | Type | Required | Description |
789
871
  |---|---|---|---|
@@ -12,8 +12,9 @@ readers see it, and be exact about what changed.
12
12
 
13
13
  Before agreeing anything is wrong, call `explain_query`; with no
14
14
  `params` it explains this session's last query. Quote back the metric it
15
- resolved, the filters applied (standing preferences included), the effective
16
- window, and briefly the SQL. Often the number is right and the disagreement
15
+ resolved, the filters (standing preferences included), the window, and
16
+ briefly the SQL. A bucket in `effective_window.partial_periods` (a year to
17
+ date labelled as the year) is often the whole dispute: name its days. Often the number is right and the disagreement
17
18
  is about which metric was meant — ambiguity, not a correction. Never
18
19
  apologise for a number you have not re-read.
19
20
 
@@ -48,7 +49,7 @@ Read the WHAT YOU MAY CHANGE block — this user's targets, nothing else. An
48
49
  admin gets the systemic fix; a member or viewer a personal preference,
49
50
  verified query or quick metric, which fixes their experience immediately.
50
51
  Both are complete answers: never route a user elsewhere, never offer what
51
- ends in a permission error. §7 is the same split for a coverage gap.
52
+ ends in a `role_too_low` refusal. §7 is the same split for a coverage gap.
52
53
 
53
54
  ## 4. Ownership and ceremony
54
55
 
cli/bundled_manifest.py CHANGED
@@ -6,7 +6,7 @@ the server's to say whether the server has operations this CLI does not
6
6
  know. tests/test_skills_surface.py asserts it is current.
7
7
  """
8
8
 
9
- MANIFEST_HASH = "b9fdc845fd9d3a22cea19e234b330e7e41d17f5d5ba6b5e1c713123d38109a6d"
9
+ MANIFEST_HASH = "e61e4a47f3c193beb2f6d731efde82839f5e62788bfa18560277459487bb26a7"
10
10
  TOOL_KEYS = (
11
11
  "business_context_edit",
12
12
  "semantic_model_edit",