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 knob — syntax_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.

Metadata

Release files for coop-dax-review 0.22.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for coop-dax-review 0.22.0
File Size Uploaded Provenance
coop_dax_review-0.22.0.tar.gz 101.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for coop-dax-review 0.22.0
File Interpreter ABI Platform
coop_dax_review-0.22.0-py3-none-any.whl Python 3 none any Details

Total release size: 238.9 kB

Release files / coop_dax_review-0.22.0.tar.gz

Download URL coop_dax_review-0.22.0.tar.gz
Size 101.7 kB
Tags Source
SHA-256 checksum
How to use checksums
6468a7ae5d7d16878d02087df9d4a66e15f4166134764dca0d1a42d667328ea0
BLAKE2b-256 checksum
How to use checksums
999f0b561fd39a52ad51737f732bf44e30d6ff3db63e07b481d89c1e2a8f65d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.

Transparency log

Release files / coop_dax_review-0.22.0-py3-none-any.whl

Download URL coop_dax_review-0.22.0-py3-none-any.whl
Size 137.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f3e980b7b821dcc2430c7c4f7314fd4d39f63c1cb7d96d6e178873a1bb3dcac9
BLAKE2b-256 checksum
How to use checksums
846dad3811a81d2af5f97239e7e9cb8713b661b6bfcb498ee8ed2fd5f81ed19a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.

Transparency log

Security for coop-dax-review 0.22.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for coop_dax_review-0.22.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.22.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page