Metadata-Version: 2.4
Name: coop-dax-review
Version: 0.8.0
Summary: 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-URL: Homepage, https://github.com/kabukisensei/coop-dax-review
Project-URL: Repository, https://github.com/kabukisensei/coop-dax-review
Project-URL: Issues, https://github.com/kabukisensei/coop-dax-review/issues
Author: Aaron Jennings
License: MIT
License-File: LICENSE
Keywords: dax,fabric,linter,powerbi,semantic-model,standards,tmdl
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: click>=8.1
Requires-Dist: coop-review-core>=0.2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: questionary>=2.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# 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`](https://github.com/kabukisensei/coop-sql-review) — same
architecture and contracts.

## Install

```sh
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:

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

## Usage

```sh
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:
  ```yaml
  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
  ```

```sh
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:

```sh
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 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)

Three 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):
  ```
  // 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:
  ```yaml
  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:
  ```sh
  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-independent `fingerprint` (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-baseline` to prune).

## Agent JSON contract

```json
{
  "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.
