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]
[--open/--no-open] [--color/--no-color] [--log-file FILE]
[--baseline FILE] [--write-baseline FILE]
[--min-severity error|warning|info] [--strict]
coop-dax-review rules # list every rule (id, severity, tier, agent?)
coop-dax-review upgrade # show the command to update (never self-applies; alias: update)
coop-dax-review --version
MODEL_PATHSpoint at a PBIP/TMDL model folder (*.SemanticModel/definition/...), any folder of.tmdlfiles, or a legacy.bimfile. 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.--strictis the opt-in CI gate — exit2when any finding remains at/above--min-severity. --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.ymlbeside the standards file (or--config) can disable rules, override severities, and tune thresholds — all with no rebuild. 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.
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 inside CALCULATE |
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 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 |
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)
Two deterministic, never-blocking ways to silence findings you've already triaged:
- Inline — drop a comment on a finding's line (or the line directly above it):
List several rule ids (// coop-dax-review:ignore DAX-VAR-RETURN reason: legacy measure, rewrite scheduledignore DAX-A, DAX-B), or use a bareignore/*to silence every rule on that line. Thereason:text is for humans; it's ignored by the parser. - 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, line-independentfingerprint(in the JSON), 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-baselineto prune).
Agent JSON contract
{
"tool": "coop-dax-review", "schema_version": 1, "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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file coop_dax_review-0.6.4.tar.gz.
File metadata
- Download URL: coop_dax_review-0.6.4.tar.gz
- Upload date:
- Size: 108.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c7517a7a5237fa1eb6138b740510cc35dbcb70d7c1e64678e3ecd4820627fb55
|
|
| MD5 |
6928d07a35f189d5c22212a652520ee2
|
|
| BLAKE2b-256 |
b9f9d282709bb5187f6372e05ec2a0aa580bfa5afd0b4e2028d825fa326f9ca9
|
Provenance
The following attestation bundles were made for coop_dax_review-0.6.4.tar.gz:
Publisher:
publish.yml on kabukisensei/coop-dax-review
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
coop_dax_review-0.6.4.tar.gz -
Subject digest:
c7517a7a5237fa1eb6138b740510cc35dbcb70d7c1e64678e3ecd4820627fb55 - Sigstore transparency entry: 2014673795
- Sigstore integration time:
-
Permalink:
kabukisensei/coop-dax-review@e5b20488e49c311ae7354c7efd1d93a4e7dc9869 -
Branch / Tag:
refs/tags/v0.6.4 - Owner: https://github.com/kabukisensei
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e5b20488e49c311ae7354c7efd1d93a4e7dc9869 -
Trigger Event:
push
-
Statement type:
File details
Details for the file coop_dax_review-0.6.4-py3-none-any.whl.
File metadata
- Download URL: coop_dax_review-0.6.4-py3-none-any.whl
- Upload date:
- Size: 129.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fbe02a7e81711642882114480e48b04e44b69b50bd860a078ec53e0fce42f50
|
|
| MD5 |
3fc38414ce989d0161c8ce840355a3f4
|
|
| BLAKE2b-256 |
909627e81320339bdd4bc45a6833743bee9a8c1588c4054b5304fc2b743d82ea
|
Provenance
The following attestation bundles were made for coop_dax_review-0.6.4-py3-none-any.whl:
Publisher:
publish.yml on kabukisensei/coop-dax-review
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
coop_dax_review-0.6.4-py3-none-any.whl -
Subject digest:
6fbe02a7e81711642882114480e48b04e44b69b50bd860a078ec53e0fce42f50 - Sigstore transparency entry: 2014673992
- Sigstore integration time:
-
Permalink:
kabukisensei/coop-dax-review@e5b20488e49c311ae7354c7efd1d93a4e7dc9869 -
Branch / Tag:
refs/tags/v0.6.4 - Owner: https://github.com/kabukisensei
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e5b20488e49c311ae7354c7efd1d93a4e7dc9869 -
Trigger Event:
push
-
Statement type: