Skip to main content

coop-dax-review

Offline, advisory DAX/model standards linter for our Power BI semantic models. It parses TMDL (and legacy .bim) models, builds a model catalog, checks measures and model structure against docs/standards.md (our DAX standards + Microsoft/Tabular best practices), and surfaces anything that doesn't match. It never edits or blocks — it only reports. Human reports (a sectioned, colorized terminal report, Markdown, or a self-contained branded HTML file) and machine JSON for the company analytics agent.

Sibling tool to coop-sql-review — same architecture and contracts.

Part of the Cooptimize coop suite — if your team uses coop-agent, coop install installs this plus the sibling tools (coop-sql-review, coop-data-doc); coop update keeps them current.

Install

pipx install coop-dax-review        # from PyPI

Use pipx, not system pip, so the tool stays isolated from other CLIs (ms-fabric-cli, azure-cli) it might otherwise fight over shared pins. For local development (Python 3.10–3.13; avoid 3.14 — its venvs mis-handle editable installs):

python3 -m venv .venv && .venv/bin/python -m pip install -e ".[dev]"

Usage

coop-dax-review check [MODEL_PATHS...] [--format text|json|markdown|html|sarif] [-o FILE]
                      [--html FILE] [--md FILE] [--sarif FILE] [--open/--no-open]
                      [--color/--no-color] [--log-file FILE] [--baseline FILE]
                      [--write-baseline FILE] [--save-ignores]
                      [--min-severity error|warning|info] [--strict]
coop-dax-review rules                 # list every rule (id, severity, tier, agent?)
coop-dax-review explain <RULE-ID>     # a rule's rationale + standards excerpt (--format json)
coop-dax-review upgrade [--check]     # show the command to update (never self-applies; alias: update)
coop-dax-review --version
  • MODEL_PATHS point at a PBIP/TMDL model folder (*.SemanticModel/definition/...), any folder of .tmdl files, or a legacy .bim file. Directories are searched recursively; defaults to ..
  • Run it with no paths in a terminal and it offers a checkbox picker of the subfolders to check (all selected by default — press ENTER to scan everything).
  • Advisory: exit code is always 0. --strict is the opt-in CI gate — exit 2 when any finding remains at/above --min-severity, or when no models were found/checked at all (so a typo'd path can't pass silently). A zero-model run still renders the full report (with models_checked: 0 and a scan_empty diagnostic per searched path).
  • --standards <path> overrides the bundled standards (e.g. point it at a canonical company standards file). Its sha256 travels in the JSON so the agent knows which standards a report used.
  • A config file can disable rules, override severities, and tune thresholds — all with no rebuild. It is found, first hit wins, via: --config; the COOP_DAX_REVIEW_CONFIG environment variable (point a whole CI pipeline at one config); a coop-dax-review.yml (preferred) or rules.yml (the deprecated shared name — every coop-*-review tool reads it, so two tools in one monorepo would fight over it) in the current directory or any parent up to the repo root; else beside the standards file. A broken config (bad YAML, wrong shape, a non-UTF-8 save) is a friendly one-line error naming the file, and a --config or env-var path that doesn't exist is an error too (a typo can't silently drop your overrides). For example, raise what counts as a "non-trivial" measure:
    rules:
      DAX-VAR-RETURN:
        params: { min_functions: 5 }   # also: DAX-COMPLEX-NO-HEADER.min_vars,
                                        # DAX-DISPLAY-FOLDERS.min_measures, DAX-SIMPLE-FUNCTIONS.min_calculates
    
coop-dax-review check ./MyModel.SemanticModel
coop-dax-review check . --format json --strict --min-severity warning
coop-dax-review check . --format html              # writes a report file and opens it in your browser
coop-dax-review check . --format markdown -o report.md

The default --format text is a sectioned terminal report: a banner, one section per model with ERROR/WARN/INFO severity badges, and a SUMMARY panel — including a Findings by rule count table (count desc) so you can see at a glance which rules dominate and tune or disable them in rules.yml; the Markdown and HTML reports carry the same table. It's colorized automatically when you're at a terminal and falls back to plain text when piped or redirected (override with --color/--no-color; NO_COLOR is respected). On a large legacy estate (50+ findings, no baseline in play) a one-line stderr hint points at --write-baseline ratcheting.

--format html produces a self-contained, branded HTML report (inline CSS + embedded logo, no network). It is always written to a file — coop-dax-review-report.html by default, or wherever -o points — and the path is printed and opened in your browser (pass --no-open to skip the open, e.g. in CI). upgrade/update print the exact command to run yourself (pipx upgrade coop-dax-review, etc.) rather than self-applying, since a package manager can't replace the tool while it is running; upgrade --check reports whether an update is available and stops there.

Want a report file and the usual console/JSON output in one run? --html FILE, --md FILE, and --sarif FILE are extra sinks: they write a self-contained HTML, Markdown, and/or SARIF report to the paths you name in addition to whatever --format prints, and never open a browser. Handy for CI — e.g. keep the JSON contract on stdout while dropping a human-readable HTML artifact alongside it:

coop-dax-review check . --format json --html report.html --md report.md

Annotate a pull request in CI (GitHub / Azure DevOps). --format sarif emits a standard SARIF 2.1.0 report that GitHub code scanning (and Azure DevOps) turn into inline PR annotations on the exact TMDL/.bim lines. Findings map to their rule/severity, agent-review items surface as non-blocking notes, and genuinely malformed DAX (a syntax_error diagnostic) annotates as an error. A ready-to-paste GitHub Actions step:

    - name: DAX standards review
      run: coop-dax-review check models/ --format sarif -o coop-dax-review.sarif
    - name: Upload SARIF
      uses: github/codeql-action/upload-sarif@v3
      with:
        sarif_file: coop-dax-review.sarif

The tool stays advisory (exit 0) unless you add --strict, so the SARIF annotations appear without failing the build — add --strict if you want the build to go red on remaining findings.

What it checks

Run coop-dax-review rules for the live list. Deterministic rules (reported as findings):

Rule § Sev Flags
DAX-MEASURE-CATEGORY 1 warning measure not named [Category: Name]
DAX-MEASURE-NOT-PREFIXED 1 warning Table[X] where X is a measure (measures take no prefix)
DAX-COLUMN-PREFIXED 1 warning bare [X] where X is a column (columns need Table[X])
DAX-VAR-RETURN 2 info non-trivial measure with no VAR/RETURN structure
DAX-NO-NESTED-CALCULATE 3 warning CALCULATE nested directly inside CALCULATE (iterator-mediated per-row nesting is fine)
DAX-FILTER-TABLE-IN-CALCULATE 4 warning FILTER(<table>, <col> = ...) as a CALCULATE filter argument where a plain column filter suffices
DAX-SNOWFLAKE 6 info a table with relationships chained through it (snowflake link)
DAX-BIDI-RELATIONSHIP 7 warning a bidirectional cross-filter relationship
DAX-MARKED-DATE-TABLE 8 warning time-intelligence used (in a measure or calculated column) but no marked Date table
DAX-MEASURE-IN-ITERATOR 9 info a measure referenced inside a row iterator (hidden context transition)
DAX-COMPLEX-NO-HEADER 12 info a complex measure (≥3 VARs) without a /* ... */ header
DAX-DIRECTLAKE-NO-CALC-COL 13 warning a calculated column in a Direct Lake model
DAX-USE-DIVIDE 14 warning the / operator where DIVIDE() should be used (nonzero-literal divisors like / 1000 are safe and skipped)
DAX-FORMAT-STRING 15 warning a measure with no explicit formatString
DAX-NO-FLOAT-KEYS 16 info a relationship key column typed double
DAX-HIDE-FK-COLUMNS 17 info a visible foreign-key (relationship) column
DAX-KEY-SUMMARIZEBY-NONE 18 info a numeric key column that auto-aggregates (summarizeBy ≠ none)
DAX-DISPLAY-FOLDERS 19 info a measure-heavy table with no display folders
DAX-AUTO-DATETIME 21 warning auto date/time artifacts (LocalDateTable_* / DateTableTemplate_* tables)
DAX-EARLIER-TO-VAR 22 warning EARLIER/EARLIEST — the legacy pre-VAR idiom; capture the outer row value in a VAR
DAX-DEAD-INACTIVE-RELATIONSHIP 23 warning an inactive relationship never activated by USERELATIONSHIP()
DAX-IFERROR-WRAPPING 24 warning IFERROR wrapped around arithmetic (hides real errors; slower than DIVIDE())
DAX-MEASURE-DESCRIPTION 25 info a visible measure with no description

Agent-judgment rules — the tool detects the construct but emits to the JSON agent_review list (never an auto-finding), because the call needs intent the linter can't infer:

Rule § Judges
DAX-KEEPFILTERS-NEEDED 5 whether a CALCULATE boolean filter needs KEEPFILTERS (checked per top-level filter argument)
DAX-STAR-SCHEMA 6 whether a snowflake chain should be flattened to a star
DAX-CONTEXT-TRANSITION 9 whether an iterator's context transition is intended/correct
DAX-SIMPLE-FUNCTIONS 10 whether a CALCULATE-heavy measure could use simpler functions
DAX-VALIDATION 11 whether the §11 validation checklist was run (one model-level item counting the non-trivial measures)
DAX-IMPLICIT-MEASURE 20 whether a visible auto-aggregating numeric column should become an explicit measure

See RULES.md for the full taxonomy. docs/standards.md §14–§20 are adopted Microsoft/Tabular best practices (DIVIDE, format strings, key column types, hidden FKs, key summarizeBy, display folders, explicit measures), and §22–§25 add EARLIER→VAR, dead inactive relationships, IFERROR-wrapping, and measure descriptions; docs/standards-proposed-additions.md is the original candidate list.

DAX syntax errors

Beyond the standards rules, check runs a cheap structural validation over every measure body and every calculated-column expression and reports genuinely malformed DAX — the kind that would import broken into Power BI — as error-severity syntax_error diagnostics. It catches: unbalanced parentheses, unbalanced brackets, an unterminated string literal ("..." with no closing quote), an unterminated block comment (/* ... with no */), and an empty measure body. Parens/brackets/quotes that live inside an identifier ([Net (USD)]), a string, or a comment are never counted, so compliant DAX is never flagged. This is drift detection, not a grammar: the standards rules still run on whatever parsed. Because a syntax_error is error-severity, it flips the JSON verdict to not-clean and fails --strict (exit 2) — so a broken measure never passes CI as clean.

Tune it two ways:

  • rules.yml knobsyntax_errors: error (default) | warning (demote but keep it visible in the JSON) | off (drop entirely):
    syntax_errors: warning
    
  • Inline — silence a single occurrence with a syntax (or bare *) ignore on the finding's line or the line above it. A rule-scoped ignore does not cover a syntax error:
    // coop-dax-review:ignore syntax
    

Suppressing findings (adopting on an existing model)

Three deterministic, never-blocking ways to silence findings you've already triaged. All three apply to regular findings and to agent_review items (the judgment-call prompts in the JSON), so a triaged item stays silenced everywhere:

  • Inline — drop a comment on a finding's line (or the line directly above it):

    // coop-dax-review:ignore DAX-VAR-RETURN reason: legacy measure, rewrite scheduled
    

    List several rule ids (ignore DAX-A, DAX-B), or use a bare ignore / * to silence every rule on that line. The reason: text is for humans; it's ignored by the parser.

  • rules.yml ignore list — a human-readable, fingerprint-matched suppression list that lives in the one config file (like a baseline, but readable and hand-editable). Add an ignore: block:

    ignore:
      - fingerprint: 4ad6aeb79867
        rule: DAX-BIDI-RELATIONSHIP        # rule / where / note are for humans; matching is by fingerprint
        where: Sales/FactSales[ProductId] -> DimCustomer[CustomerId]
        note: intentional many-to-many, reviewed 2026-07
    

    You don't have to hand-copy fingerprints: run check --save-ignores and, at an interactive terminal, you get a checkbox of this run's findings grouped by rule × model (a header per group, an "ignore all N" row for multi-finding groups, individual findings nested — all unchecked, opt in to the ones you want gone); the picks are appended to the config file this run read (so a team config beside the standards file, or one found in a parent directory, is updated in place rather than shadowed by a new ./rules.yml). A coop-dax-review.yml or rules.yml in your current directory (or any parent up to the repo root) is auto-discovered with no --config flag, so the loop is just "run, --save-ignores, re-run". An ignore entry that no longer matches any finding (you fixed it) is reported as a diagnostic so the list self-cleans.

  • Baseline (ratchet) — record today's findings and surface only new ones going forward:

    coop-dax-review check . --write-baseline dax-baseline.json   # once, to capture the status quo
    coop-dax-review check . --baseline dax-baseline.json         # thereafter: only new findings appear
    

    Each finding has a stable fingerprint (in the JSON) — independent of line numbers and of file paths, so a baseline written on one machine or from one directory still matches from another — and the baseline is a sorted list of those. A baseline entry that no longer matches any finding (you fixed it) is reported as a diagnostic so the file self-cleans (--write-baseline to prune).

    One-time migration (schema_version 3): the fingerprint identity changed in this release (a stable core for the volatile-message rules + an occurrence ordinal — coordinated with coop-sql-review's schema 4, one family rule). Delete and regenerate any baseline files and rules.yml ignore: lists written by earlier versions once: coop-dax-review check <models> --write-baseline baseline.json and re-run coop-dax-review check <models> --save-ignores. Until then, old entries are reported loudly as stale diagnostics on every run — nothing goes silently missing.

Agent JSON contract

{
  "tool": "coop-dax-review", "schema_version": 3, "version": "x.y.z",
  "standards": {"path": "...", "sha256": "..."},
  "models_checked": 2,
  "verdict": {"clean": false, "highest_severity": "warning"},
  "findings": [{"rule_id":"...","severity":"warning","model":"Sales","file":"...","line":12,
                "object":"[Sales: Revenue YTD]","message":"...","standard_ref":"§3","fingerprint":"4ad6aeb79867"}],
  "summary": {"error":0,"warning":2,"info":4},
  "agent_review": [{"rule_id":"...","model":"Sales","file":"...","line":40,"object":"[...]","note":"...","standard_ref":"§5","fingerprint":"..."}],
  "diagnostics": [{"severity":"warning","category":"parse_failed","file":"...","message":"..."}]
}

schema_version lets a consumer pin the shape; verdict/models_checked give a quick machine verdict + coverage signal; each finding's fingerprint is a stable id for tracking across runs.

Project docs

  • SPEC.md — architecture, CLI, agent contract, milestones.
  • RULES.md — every standard mapped to a concrete check (deterministic vs agent-judgment).
  • docs/standards.md — the canonical DAX standards the linter checks against (bundled as package data).
  • AGENTS.md — orientation for coding agents (and humans) working in this repo; CLAUDE.md imports it.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

coop_dax_review-0.22.0.tar.gz (101.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

coop_dax_review-0.22.0-py3-none-any.whl (137.2 kB view details)

Uploaded Python 3

File details

Details for the file coop_dax_review-0.22.0.tar.gz.

File metadata

  • Download URL: coop_dax_review-0.22.0.tar.gz
  • Upload date:
  • Size: 101.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for coop_dax_review-0.22.0.tar.gz
Algorithm Hash digest
SHA256 6468a7ae5d7d16878d02087df9d4a66e15f4166134764dca0d1a42d667328ea0
MD5 f2337dc35415a337d9c96ec3944c06f9
BLAKE2b-256 999f0b561fd39a52ad51737f732bf44e30d6ff3db63e07b481d89c1e2a8f65d0

See more details on using hashes here.

Provenance

The following attestation bundles were made for coop_dax_review-0.22.0.tar.gz:

Publisher: publish.yml on kabukisensei/coop-dax-review

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file coop_dax_review-0.22.0-py3-none-any.whl.

File metadata

File hashes

Hashes for coop_dax_review-0.22.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f3e980b7b821dcc2430c7c4f7314fd4d39f63c1cb7d96d6e178873a1bb3dcac9
MD5 86909e0d88110f6a6e3d3a4df5666d3f
BLAKE2b-256 846dad3811a81d2af5f97239e7e9cb8713b661b6bfcb498ee8ed2fd5f81ed19a

See more details on using hashes here.

Provenance

The following attestation bundles were made for coop_dax_review-0.22.0-py3-none-any.whl:

Publisher: publish.yml on kabukisensei/coop-dax-review

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.22.0 This release

2 files

0.21.2

2 files

0.21.1

2 files

0.21.0

2 files

0.20.0

2 files

0.19.0

2 files

0.18.0

2 files

0.17.0

2 files

0.15.0

2 files

0.14.0

2 files

0.13.0

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.4

2 files

0.6.3

2 files

0.6.2

2 files

0.6.1

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page