Skip to main content

Offline, advisory DAX/model standards linter for Power BI semantic models: parses TMDL/.bim, checks measures and model structure against the team's DAX standards, and reports anything that doesn't match. Human report + machine JSON for the company agent. Never edits or blocks.

Project description

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.

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 -m venv .venv && .venv/bin/pip install -e ".[dev]"

Usage

coop-dax-review check [MODEL_PATHS...] [--format text|json|markdown|html] [-o FILE]
                      [--html FILE] [--md 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 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 rules.yml (found via --config, else a rules.yml in the current directory, else beside the standards file) can disable rules, override severities, and tune thresholds — all with no rebuild. A broken rules.yml (bad YAML, wrong shape, a non-UTF-8 save) is a friendly one-line error naming the file, and a --config 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. 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).

--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 and --md FILE are extra sinks: they write a self-contained HTML and/or Markdown 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

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> = ...) 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
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

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 for a non-trivial measure
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); docs/standards-proposed-additions.md is the original candidate list.

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 (all unchecked — opt in to the ones you want gone); the picks are appended to rules.yml for you, so the next run silences them. A rules.yml in your current directory 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 2): fingerprints used to include the cwd-relative file path and changed in this release. Delete and regenerate any baseline files and rules.yml ignore: lists written by earlier versions (re-run --write-baseline / --save-ignores).

Agent JSON contract

{
  "tool": "coop-dax-review", "schema_version": 2, "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).
  • CLAUDE.md — orientation for Claude Code sessions in this repo.

Project details


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.9.0.tar.gz (117.1 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.9.0-py3-none-any.whl (139.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: coop_dax_review-0.9.0.tar.gz
  • Upload date:
  • Size: 117.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for coop_dax_review-0.9.0.tar.gz
Algorithm Hash digest
SHA256 5bbcd6ac45e5937f62349af65e9de5fd0e266e8876a817261c3ecc3a021a643e
MD5 3a36a5603b84ee2b04f54b3140cb4d41
BLAKE2b-256 dc262a9e70ab41b0119a73c8759a6c485e65b6583b1f117c645851c817c9393a

See more details on using hashes here.

Provenance

The following attestation bundles were made for coop_dax_review-0.9.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.9.0-py3-none-any.whl.

File metadata

  • Download URL: coop_dax_review-0.9.0-py3-none-any.whl
  • Upload date:
  • Size: 139.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for coop_dax_review-0.9.0-py3-none-any.whl
Algorithm Hash digest
SHA256 3e4f1b2b768b400da521685bcd24c354059c683fde084f438a7275b1070db19d
MD5 ab3755aef808502adc08b9a816d2dc9a
BLAKE2b-256 262b29ee273386908acf59bd8d0f5c20f23cea6ec5513b75defe65b80c313bf3

See more details on using hashes here.

Provenance

The following attestation bundles were made for coop_dax_review-0.9.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.

Supported by

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