Power BI Validation and Self-Testing Overview Validation skill for any TMDL, PBIR, DAX, or M artifact a developer (or Claude) generates. The goal: catch syntax, schema, and best-practice errors locally before a Fabric REST deploy fails. This skill is essential for the powerbi-expert agent's Self-Validation Protocol -- whenever the agent writes TMDL or PBIR, it should describe (or run) the matching validation step from this skill. As of 2026, Power BI validation has four distinct layers, each catching a different class of error: | Layer | TMDL Tool | PBIR Tool | What it catches | |-------|----…

| grep -E '(definition|Report)/' || true)\n\nif [ -z \"$CHANGED_PBIR\" ]; then\n exit 0\nfi\n\n# Layer 1: JSON Schema\npython ./scripts/validate_pbir.py \"MyProject.Report/definition\" || {\n echo \"PBIR schema validation FAILED. Fix and recommit.\"\n exit 1\n}\n\n# Layer 2+4: Structure + Lineage\npython ./scripts/lint_pbir_lineage.py \"MyProject.Report\" || {\n echo \"PBIR lineage check FAILED. Fix and recommit.\"\n exit 1\n}\n\n# Layer 3: PBI-InspectorV2 (errors block; warnings allowed)\n./pbi-inspector/PBIInspectorCLI \\\n -fabricitem \"./MyProject.Report\" \\\n -rules \"./pbi-inspector-rules.json\" \\\n -formats GitHub\nINSPECTOR_EXIT=$?\nif [ $INSPECTOR_EXIT -eq 2 ]; then\n echo \"PBI-InspectorV2 ERROR severity rules failed. Fix and recommit.\"\n exit 1\nfi\n\necho \"PBIR validation passed.\"\nexit 0\n```\n\n## 8. Common PBIR Validation Failures\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| `$schema not found` | File missing `$schema` URL | Add the appropriate `$schema` for the file type |\n| `additionalProperties not allowed: 'X'` | Hand-edited file uses a property that doesn't exist on the schema | Check schema URL for valid properties; remove the unknown one |\n| Bookmark targets missing page | Page deleted but bookmark not updated | Delete the bookmark or recreate the page |\n| `pageBinding name not unique` | Two drillthrough pages have the same `pageBinding.name` | Rename one (use a GUID for uniqueness) |\n| Visual won't render after PBIR conversion | Missing `query.queryState` after manual edit | Reload from a known-good version; never hand-edit `queryState` |\n| `Report has more than 1000 pages` (deploy time) | Service-enforced limit | Split into multiple reports OR archive old pages |\n| Bookmarks group references orphaned bookmark | Bookmark file deleted but `bookmarks.json` not updated | Run lineage linter to find and remove the orphan reference |\n| Theme not applied after deployment | RegisteredResource file missing or not committed to Git | Verify the file exists in `StaticResources/RegisteredResources/` |\n| `definition.pbir` rejected by Fabric Git | Schema version too low | Update to `version: \"4.0\"` |\n| `definition.pbir` byPath doesn't resolve | Relative path wrong (case sensitivity on Linux runners) | Use `./MyProject.SemanticModel` (forward slash, exact case) |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":19421,"content_sha256":"a28b56228ebf42ddeebab8348b35987d67695e0c80482ea7f1966baea4ab84f2"},{"filename":"references/tmdl-validation-recipes.md","content":"# TMDL Validation Recipes\n\nComplete cookbook for validating TMDL files at every layer: syntax parser, TOM schema, BPA, lineage, and DAX/M syntax. Recipes target the 2026 toolchain (TmdlSerializer in Microsoft.AnalysisServices.Tabular, Tabular Editor 2/3, semantic-link-labs).\n\n## 1. Layer 1+2: TmdlSerializer Round-Trip (.NET)\n\nSmallest possible validator. One C# file, one NuGet package, no project required (use `dotnet script` or a single-file compile).\n\n### 1a. Single-file C# script\n\n```csharp\n#r \"nuget: Microsoft.AnalysisServices.NetCore.retail.amd64, 19.84.1\"\n\nusing System;\nusing Microsoft.AnalysisServices.Tabular;\nusing Microsoft.AnalysisServices.Tabular.Tmdl;\n\nif (Args.Count == 0)\n{\n Console.Error.WriteLine(\"Usage: validate-tmdl \u003cfolder>\");\n return 1;\n}\n\nstring folder = Args[0];\ntry\n{\n var db = TmdlSerializer.DeserializeDatabaseFromFolder(folder);\n\n Console.WriteLine($\"OK: TMDL parsed\");\n Console.WriteLine($\" CompatibilityLevel = {db.CompatibilityLevel}\");\n Console.WriteLine($\" Tables = {db.Model.Tables.Count}\");\n Console.WriteLine($\" Measures = {db.Model.Tables.Sum(t => t.Measures.Count)}\");\n Console.WriteLine($\" Relationships = {db.Model.Relationships.Count}\");\n\n // Layer 2 deeper check: TOM Validate()\n var validation = db.Model.Validate();\n if (validation.Errors.Count > 0)\n {\n Console.Error.WriteLine($\"VALIDATE ERRORS ({validation.Errors.Count})\");\n foreach (var e in validation.Errors)\n Console.Error.WriteLine($\" - {e.Object?.GetType().Name}: {e.Message}\");\n return 3;\n }\n return 0;\n}\ncatch (TmdlFormatException fx)\n{\n Console.Error.WriteLine($\"SYNTAX ERROR {fx.Document}:{fx.Line}\");\n Console.Error.WriteLine($\" {fx.LineText}\");\n Console.Error.WriteLine($\" -> {fx.Message}\");\n return 1;\n}\ncatch (TmdlSerializationException sx)\n{\n Console.Error.WriteLine($\"METADATA ERROR {sx.Document}:{sx.Line}\");\n Console.Error.WriteLine($\" {sx.Message}\");\n return 2;\n}\n```\n\nRun with:\n\n```bash\ndotnet script validate-tmdl.csx -- \"MyProject.SemanticModel/definition\"\n```\n\n### 1b. Why both `TmdlFormatException` AND `TmdlSerializationException`?\n\n| Exception | Layer | What it catches |\n|-----------|-------|-----------------|\n| `TmdlFormatException` | Parser (text -> tokens) | Bad indentation, invalid keyword, malformed expression delimiter, missing colon |\n| `TmdlSerializationException` | Object builder (tokens -> TOM) | Property name doesn't exist on object type, value doesn't match property type, required parent missing |\n| `model.Validate()` returns `Errors` | TOM semantic | Measure references undefined column, sortByColumn invalid, calculation group precedence collision, role expression syntax |\n\nAlways catch all three and report them with distinct exit codes so CI can route different failure types to different log streams.\n\n## 2. Layer 1+2: TmdlSerializer from Python (pythonnet)\n\nWhen you need TMDL parsing inside a Python pipeline (Fabric notebook, Airflow, GitLab CI), `pythonnet` lets Python load the .NET TOM assembly directly. semantic-link-labs uses this internally.\n\n```python\n%pip install pythonnet -q\n\nimport clr\nimport os\nclr.AddReference(os.path.join(os.path.dirname(__file__), \"Microsoft.AnalysisServices.Tabular.dll\"))\n\nfrom Microsoft.AnalysisServices.Tabular import Database\nfrom Microsoft.AnalysisServices.Tabular.Tmdl import TmdlSerializer, TmdlFormatException, TmdlSerializationException\n\ndef validate_tmdl_folder(folder: str) -> dict:\n try:\n db = TmdlSerializer.DeserializeDatabaseFromFolder(folder)\n result = {\n \"ok\": True,\n \"compat_level\": db.CompatibilityLevel,\n \"tables\": db.Model.Tables.Count,\n \"measures\": sum(t.Measures.Count for t in db.Model.Tables),\n }\n validation = db.Model.Validate()\n if validation.Errors.Count > 0:\n result[\"ok\"] = False\n result[\"validation_errors\"] = [e.Message for e in validation.Errors]\n return result\n except TmdlFormatException as fx:\n return {\"ok\": False, \"type\": \"syntax\", \"document\": fx.Document, \"line\": fx.Line, \"message\": fx.Message}\n except TmdlSerializationException as sx:\n return {\"ok\": False, \"type\": \"metadata\", \"document\": sx.Document, \"line\": sx.Line, \"message\": sx.Message}\n\nprint(validate_tmdl_folder(\"./MyProject.SemanticModel/definition\"))\n```\n\nInside a Fabric notebook the assembly is already on the runtime, so the explicit `AddReference` becomes:\n\n```python\nimport sempy.fabric # implicitly loads Microsoft.AnalysisServices.Tabular\nfrom Microsoft.AnalysisServices.Tabular.Tmdl import TmdlSerializer\n```\n\n## 3. Tabular Editor 2 CLI Validation\n\nThe free Tabular Editor 2 CLI is the recommended runner for any CI pipeline that doesn't have a .NET project.\n\n### 3a. Parse-only validation (no BPA, no deploy)\n\n```bash\n# Linux / macOS via mono\nmono TabularEditor.exe \"MyProject.SemanticModel/definition\" -B \"/tmp/out.bim\"\n\n# Windows\nTabularEditor.exe \"MyProject.SemanticModel\\definition\" -B \"C:\\temp\\out.bim\"\n```\n\nThe `-B` switch (bim output) forces a TmdlSerializer round-trip plus a TOM validation. Any failure exits non-zero with a precise error message.\n\n### 3b. Parse + BPA + custom script\n\n```bash\nTabularEditor.exe \"MyProject.SemanticModel/definition\" \\\n -S \"Scripts/validate-naming.csx\" \\\n -A \"https://raw.githubusercontent.com/TabularEditor/BestPracticeRules/master/BPARules.json\" \\\n -V \\\n -G\n```\n\n| Switch | Effect |\n|--------|--------|\n| `-S \u003cfile>` | Run a C# script before BPA. Use this for custom validation that BPA can't express. |\n| `-A \u003curl\\|file>` | Run BPA with the rules at the given location. |\n| `-V` | Verbose: list every BPA violation, not just counts. |\n| `-G` | GitHub Actions / Azure DevOps log format -- groups violations by category and renders file paths as clickable links in CI logs. |\n\n### 3c. Custom validation C# script (Scripts/validate-naming.csx)\n\nTabular Editor C# scripts run with `Model` pre-bound to the loaded TOM model. Use this for validation rules that BPA cannot express because they need procedural logic.\n\n```csharp\n// Fail if any measure name uses snake_case (we want PascalCase or 'Spaced Name')\nforeach (var m in Model.AllMeasures)\n{\n if (m.Name.Contains(\"_\"))\n {\n Error($\"Measure '{m.Name}' uses snake_case. Use PascalCase or 'Spaced Name'.\");\n }\n}\n\n// Fail if any table has zero measures AND zero relationships AND is not hidden\nforeach (var t in Model.Tables.Where(t => !t.IsHidden))\n{\n var hasMeasures = t.Measures.Any();\n var inRel = Model.Relationships.Any(r => r.FromTable == t || r.ToTable == t);\n if (!hasMeasures && !inRel)\n Error($\"Table '{t.Name}' is orphaned (no measures, no relationships, not hidden).\");\n}\n\n// Fail if any calculation group has overlapping precedence\nvar precedenceMap = new Dictionary\u003cint, string>();\nforeach (var t in Model.Tables.Where(t => t.CalculationGroup != null))\n{\n var p = t.CalculationGroup.Precedence;\n if (precedenceMap.ContainsKey(p))\n Error($\"Calc groups '{t.Name}' and '{precedenceMap[p]}' both use precedence {p}.\");\n else\n precedenceMap[p] = t.Name;\n}\n```\n\nThe `Error()` function in a Tabular Editor C# script causes the CLI to exit non-zero, blocking the deploy.\n\n### 3d. Exit code reference\n\n| Code | Meaning |\n|------|---------|\n| 0 | Success: parsed, BPA passed, script passed |\n| 1 | Warnings only (BPA Warning severity) |\n| 2 | Errors (BPA Error severity, or `Error()` called in C# script) |\n| 4 | Deploy failed (only relevant when using `-D`) |\n\nPin Tabular Editor 2 to a specific release (e.g., `2.25.0`) in CI. Newer releases sometimes add stricter validation that breaks previously-green builds.\n\n## 4. semantic-link-labs Validation (Fabric Notebooks)\n\nFor pipelines that already live inside Fabric (notebooks, Spark jobs, Data Factory), `semantic-link-labs` is the path of least resistance.\n\n### 4a. Run BPA against a deployed model\n\n```python\n%pip install semantic-link-labs -q\nimport sempy_labs as labs\n\nresults = labs.run_model_bpa(\n dataset=\"SalesModel\",\n workspace=\"Sales-Dev\",\n extended=True, # Adds VertiPaq Analyzer stats so performance rules can fire\n return_dataframe=True,\n)\n\n# Filter to only failing Error-severity rules\nfailures = results[(results[\"Severity\"] >= 3)]\ndisplay(failures)\n\nif len(failures) > 0:\n raise Exception(f\"BPA failed: {len(failures)} Error-severity violations\")\n```\n\n### 4b. Run BPA against every model in a workspace, write to delta\n\n```python\nlabs.run_model_bpa_bulk(\n workspace=\"Sales-Dev\",\n extended=True,\n)\n\n# Results land in the lakehouse-attached notebook at:\n# Tables/modelbparesults\ndf = spark.table(\"modelbparesults\")\ndf.filter(\"Severity >= 3\").show()\n```\n\nThis is the pattern for **scheduled BPA reporting** -- run it daily against every workspace, write to delta, build a Power BI report on top showing trends and per-team owners.\n\n### 4c. Custom rule set\n\n```python\nimport sempy_labs as labs\n\n# Start from the built-in ~60 rules, then add or override\nmy_rules = labs.model_bpa_rules() # built-in rules as a list of dicts\n\nmy_rules.append({\n \"ID\": \"CONTOSO_NO_AUTO_DATETIME\",\n \"Name\": \"Auto date/time must be disabled\",\n \"Category\": \"Performance\",\n \"Severity\": 3,\n \"Scope\": \"Model\",\n \"Expression\": \"AutoDateTime = false\",\n \"FixExpression\": None,\n \"CompatibilityLevel\": 1200,\n})\n\nmy_rules.append({\n \"ID\": \"CONTOSO_MEASURE_FOLDER\",\n \"Name\": \"Every measure must have a display folder\",\n \"Category\": \"Maintenance\",\n \"Severity\": 2,\n \"Scope\": \"Measure\",\n \"Expression\": \"DisplayFolder \u003c> ''\",\n})\n\nresults = labs.run_model_bpa(\n dataset=\"SalesModel\",\n workspace=\"Sales-Dev\",\n rules=my_rules,\n)\n```\n\n### 4d. Offline TMDL validation from Python\n\n`semantic-link-labs` can also load a TMDL folder **without connecting** to a workspace, using the embedded TmdlSerializer wrapper. This is the way to run validation in a notebook before publishing.\n\n```python\nimport sempy_labs as labs\nfrom sempy_labs.tom import import_model_from_tmdl\n\n# Load TMDL files from a local path (or attached lakehouse) as an in-memory TOM database\ndb = import_model_from_tmdl(folder=\"./MyProject.SemanticModel/definition\")\n\n# Run TOM Validate()\nerrors = db.Model.Validate().Errors\nif errors.Count > 0:\n for e in errors:\n print(f\" - {e.Object.Name}: {e.Message}\")\n raise Exception(f\"{errors.Count} TOM validation errors\")\n\n# Run BPA against the in-memory model (no deploy required)\nresults = labs.run_model_bpa(model=db.Model, extended=False)\ndisplay(results[results[\"Severity\"] >= 3])\n```\n\nThis is the **best path** for \"validate this TMDL folder I just generated, without ever talking to Power BI\".\n\n## 5. INFO DAX Functions (Live Model Introspection)\n\nFor models already deployed, the 2026-preferred way to introspect metadata is the `INFO.*` DAX function family. These are far more agent-friendly than DMVs because they return tables you can query like any other DAX expression.\n\n```dax\n// All measures with their tables, expressions, and folders\nEVALUATE\nSELECTCOLUMNS(\n INFO.MEASURES(),\n \"Table\", LOOKUPVALUE(INFO.TABLES()[Name], INFO.TABLES()[ID], [TableID]),\n \"Measure\", [Name],\n \"Expression\", [Expression],\n \"Folder\", [DisplayFolder]\n)\n\n// Find measures referencing a missing column\nEVALUATE\nFILTER(\n INFO.MEASURES(),\n SEARCH(\"Sales[NonExistentColumn]\", [Expression], 1, 0) > 0\n)\n\n// Find columns with no measures referencing them (candidates for hiding)\nEVALUATE\nVAR Referenced =\n SUMMARIZE(\n FILTER(INFO.MEASURES(), [Expression] \u003c> \"\"),\n [Name]\n )\nRETURN\n EXCEPTALL(INFO.COLUMNS(), Referenced)\n```\n\nUse `semantic-link.evaluate_dax` from a Fabric notebook to run these against any deployed model.\n\n## 6. Combined Validation Pre-Commit Hook\n\nA combined `.git/hooks/pre-commit` script that runs all three TMDL validation layers locally before allowing a commit.\n\n```bash\n#!/usr/bin/env bash\nset -e\n\nCHANGED_TMDL=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.tmdl

Power BI Validation and Self-Testing Overview Validation skill for any TMDL, PBIR, DAX, or M artifact a developer (or Claude) generates. The goal: catch syntax, schema, and best-practice errors locally before a Fabric REST deploy fails. This skill is essential for the powerbi-expert agent's Self-Validation Protocol -- whenever the agent writes TMDL or PBIR, it should describe (or run) the matching validation step from this skill. As of 2026, Power BI validation has four distinct layers, each catching a different class of error: | Layer | TMDL Tool | PBIR Tool | What it catches | |-------|----…

|| true)\n\nif [ -z \"$CHANGED_TMDL\" ]; then\n exit 0\nfi\n\n# Layer 1+2: TmdlSerializer parse + TOM Validate via Tabular Editor\necho \"Validating TMDL syntax and TOM metadata...\"\nmono ~/te2/TabularEditor.exe \"MyProject.SemanticModel/definition\" -B /tmp/check.bim > /tmp/te2-out.txt 2>&1 || {\n cat /tmp/te2-out.txt\n echo\n echo \"Pre-commit hook FAILED: TMDL syntax or metadata error.\"\n echo \"Fix the errors above and commit again.\"\n exit 1\n}\n\n# Layer 3: BPA (errors block; warnings allowed)\necho \"Running Best Practice Analyzer...\"\nmono ~/te2/TabularEditor.exe \"MyProject.SemanticModel/definition\" \\\n -A \"https://raw.githubusercontent.com/TabularEditor/BestPracticeRules/master/BPARules.json\" \\\n -V > /tmp/bpa-out.txt 2>&1\nBPA_EXIT=$?\n\nif [ $BPA_EXIT -eq 2 ]; then\n cat /tmp/bpa-out.txt\n echo\n echo \"Pre-commit hook FAILED: BPA Error-severity violations.\"\n exit 1\nfi\n\nif [ $BPA_EXIT -eq 1 ]; then\n cat /tmp/bpa-out.txt\n echo \"BPA warnings present but allowed. Continuing.\"\nfi\n\necho \"TMDL validation passed.\"\nexit 0\n```\n\nSave as `.git/hooks/pre-commit` and `chmod +x` it. Skip with `git commit --no-verify` only when explicitly intended.\n\n## 7. Troubleshooting Validation Failures\n\n| Symptom | Likely cause | Fix |\n|---------|--------------|-----|\n| `TmdlFormatException` mentioning indentation | Mixed tabs and spaces | Convert all indentation to single tabs (TMDL spec mandates tabs) |\n| `TmdlFormatException: invalid keyword 'X'` | Object type misspelled or wrong casing on serialize | Use camelCase for object types and properties |\n| `TmdlSerializationException: property 'Y' is not valid on Z` | Property exists on a newer compatibility level | Bump `database.tmdl` `compatibilityLevel` |\n| `model.Validate()` reports orphaned column | Column has `sourceColumn` referencing a non-existent source field | Fix `sourceColumn` or remove the column |\n| `model.Validate()` reports DAX error | Measure references undefined column or syntax error | Run DaxFormatter API on the expression to localize the issue |\n| BPA `MODEL_PERFORMANCE_AVOID_AUTO_DATETIME` fires | Auto date/time enabled in PBIP project | Set `autoDateTime: false` in `model.tmdl` |\n| BPA `DAX_PERFORMANCE_AVOID_DIVISION_OPERATOR` fires | DAX uses `/` instead of DIVIDE() | Replace with `DIVIDE(numerator, denominator, 0)` |\n| Tabular Editor CLI exits 0 but Power BI Service deploy fails | Service-only validation (e.g., role member doesn't exist in tenant) | These can only be caught at deploy time |\n| `semantic-link-labs` says `connect_semantic_model: not found` | Workspace name has spaces or special chars | Use the workspace ID (GUID) instead of the name |\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":14756,"content_sha256":"afbce08889d852c0593a6c9c5c3b42f7a002ccad0befcb17ef287862bf66e516"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Power BI Validation and Self-Testing","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Overview","type":"text"}]},{"type":"paragraph","content":[{"text":"Validation skill for any TMDL, PBIR, DAX, or M artifact a developer (or Claude) generates. The goal: catch syntax, schema, and best-practice errors ","type":"text"},{"text":"locally","type":"text","marks":[{"type":"strong"}]},{"text":" before a Fabric REST deploy fails. This skill is essential for the powerbi-expert agent's ","type":"text"},{"text":"Self-Validation Protocol","type":"text","marks":[{"type":"strong"}]},{"text":" -- whenever the agent writes TMDL or PBIR, it should describe (or run) the matching validation step from this skill.","type":"text"}]},{"type":"paragraph","content":[{"text":"As of 2026, Power BI validation has four distinct layers, each catching a different class of error:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Layer","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TMDL Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PBIR Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it catches","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1. Syntax / parser","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TmdlSerializer.DeserializeDatabaseFromFolder","type":"text","marks":[{"type":"code_inline"}]},{"text":" (.NET)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JSON schema validation (","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" URLs)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Indentation errors, invalid keywords, malformed JSON","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2. Object / schema","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TmdlSerializer","type":"text","marks":[{"type":"code_inline"}]},{"text":" -> ","type":"text"},{"text":"TmdlSerializationException","type":"text","marks":[{"type":"code_inline"}]},{"text":" (valid syntax, invalid TOM metadata)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PBIR JSON schemas in ","type":"text"},{"text":"microsoft/json-schemas","type":"text","marks":[{"type":"code_inline"}]},{"text":" repo","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid property combinations, type mismatches, missing required properties","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3. Best practice (BPA)","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tabular Editor BPA rules (","type":"text"},{"text":"BPARules.json","type":"text","marks":[{"type":"code_inline"}]},{"text":") or ","type":"text"},{"text":"semantic-link-labs.run_model_bpa","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PBI-InspectorV2 rules (","type":"text"},{"text":"Base-rules.json","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Anti-patterns, missing display folders, ambiguous relationships, naming conventions","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4. Lineage / cross-reference","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DAX measure references resolve, sortByColumn exists, calculation group precedence","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bookmarks reference real pages, drillthrough targets exist, theme files present","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Dangling references, broken bookmarks, missing visuals","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"The cardinal rule:","type":"text","marks":[{"type":"strong"}]},{"text":" never deploy without passing layers 1 and 2; never merge to main without passing layer 3.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"2026 Validation Tooling Snapshot","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tool","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validates","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Runtime","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Status","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TmdlSerializer","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Microsoft.AnalysisServices.Tabular)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TMDL syntax + TOM schema","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".NET / pythonnet","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tabular Editor 2 CLI","type":"text","marks":[{"type":"code_inline"}]},{"text":" (free)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TMDL load + BPA + custom C# scripts","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".NET CLI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA, free","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tabular Editor 3 CLI","type":"text","marks":[{"type":"code_inline"}]},{"text":" (paid)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Same + advanced rules + DAX debugger","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".NET CLI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA, commercial","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"semantic-link-labs.run_model_bpa","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TMDL/TOM model BPA from Python","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fabric notebook (Python)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA, ~60 rules built in","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"semantic-link-labs.run_model_bpa_bulk","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"BPA across all models in workspace","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fabric notebook","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PBI-InspectorV2","type":"text","marks":[{"type":"code_inline"}]},{"text":" (\"Fab Inspector\")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PBIR / PBIP / Fabric item rules","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".NET CLI / Docker","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"v2.3+, GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pbi-tools","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PBIX extract/compile + basic TMDL","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".NET CLI","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Stable for TMDL, evolving for PBIR","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"fabric-cicd","type":"text","marks":[{"type":"code_inline"}]},{"text":" (built-in)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"parameter.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" + repo structure pre-deployment","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DaxFormatter","type":"text","marks":[{"type":"code_inline"}]},{"text":" API","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"DAX syntax","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Microsoft TMDL VS Code extension","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TMDL syntax in editor","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"VS Code","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Community ","type":"text"},{"text":"CPIM.TMDL-language-support","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"TMDL + DAX + M semantic highlighting","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"VS Code","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"INFO DAX functions","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Live model introspection (replaces DMVs)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"XMLA / Desktop","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GA","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Self-Validation Protocol (For Generated Artifacts)","type":"text"}]},{"type":"paragraph","content":[{"text":"When generating TMDL or PBIR artifacts inside an agent loop, follow this minimum protocol:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Before writing files","type":"text","marks":[{"type":"strong"}]},{"text":" -- mentally validate the structure: every object reference must resolve, every required property must be set.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After writing files","type":"text","marks":[{"type":"strong"}]},{"text":" -- run a syntax-level parse (TmdlSerializer for TMDL; JSON schema validation for PBIR).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Before suggesting deployment","type":"text","marks":[{"type":"strong"}]},{"text":" -- run a BPA pass (Tabular Editor CLI or semantic-link-labs).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Report results inline","type":"text","marks":[{"type":"strong"}]},{"text":" -- never silently swallow validation errors. Surface line numbers, file paths, and the specific rule that failed.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"A valid agent response that generates a 50-line TMDL measure block should always be followed by either:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"(a) A validation script the user can paste, OR","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"(b) An inline Bash/PowerShell/Python validation invocation if the environment supports it.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"TMDL Validation -- Layer 1 (Syntax Parser)","type":"text"}]},{"type":"paragraph","content":[{"text":"The fastest, lowest-dependency TMDL syntax check is ","type":"text"},{"text":"TmdlSerializer.DeserializeDatabaseFromFolder","type":"text","marks":[{"type":"code_inline"}]},{"text":". It throws:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TmdlFormatException","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- the TMDL text has invalid syntax (bad keyword, wrong indentation, malformed expression). Includes ","type":"text"},{"text":"Document","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Line","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"LineText","type":"text","marks":[{"type":"code_inline"}]},{"text":" properties pointing to the exact location.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TmdlSerializationException","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- the TMDL text parses but produces invalid TOM metadata (e.g., a ","type":"text"},{"text":"column","type":"text","marks":[{"type":"code_inline"}]},{"text":" references a ","type":"text"},{"text":"dataType","type":"text","marks":[{"type":"code_inline"}]},{"text":" that doesn't exist, or a ","type":"text"},{"text":"partition","type":"text","marks":[{"type":"code_inline"}]},{"text":" references an unknown data source).","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Minimal C# validator (.NET 8):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"csharp"},"content":[{"text":"using Microsoft.AnalysisServices.Tabular;\nusing Microsoft.AnalysisServices.Tabular.Tmdl;\n\nstring folder = args[0];\ntry\n{\n var db = TmdlSerializer.DeserializeDatabaseFromFolder(folder);\n Console.WriteLine($\"OK: TMDL parsed. CompatLevel={db.CompatibilityLevel}, Tables={db.Model.Tables.Count}\");\n return 0;\n}\ncatch (TmdlFormatException fx)\n{\n Console.Error.WriteLine($\"SYNTAX ERROR {fx.Document}:{fx.Line}\");\n Console.Error.WriteLine($\" {fx.LineText}\");\n Console.Error.WriteLine($\" -> {fx.Message}\");\n return 1;\n}\ncatch (TmdlSerializationException sx)\n{\n Console.Error.WriteLine($\"METADATA ERROR {sx.Document}:{sx.Line}\");\n Console.Error.WriteLine($\" {sx.Message}\");\n return 2;\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"One-liner via Tabular Editor 2 CLI","type":"text","marks":[{"type":"strong"}]},{"text":" (no C# project required):","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Loads TMDL folder; non-zero exit on parse failure\nTabularEditor.exe \"MyProject.SemanticModel/definition\" -B \"MyProject.bim\"","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"-B","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bim output) switch forces a deserialize + reserialize round-trip. Any parse failure exits non-zero with the error written to stderr.","type":"text"}]},{"type":"paragraph","content":[{"text":"For full scripted patterns and Python equivalents, see ","type":"text"},{"text":"references/tmdl-validation-recipes.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"TMDL Validation -- Layer 3 (Best Practice Analyzer)","type":"text"}]},{"type":"paragraph","content":[{"text":"The Best Practice Analyzer (BPA) is the canonical anti-pattern checker for tabular models. It is the same engine in Tabular Editor 2, Tabular Editor 3, semantic-link-labs, and ","type":"text"},{"text":"Fabric > Workspace settings > Best Practice Analyzer","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Tabular Editor 2 CLI (free, recommended for CI):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Run BPA against a TMDL folder using the official Microsoft rule set\nTabularEditor.exe \"MyProject.SemanticModel/definition\" \\\n -A \"https://raw.githubusercontent.com/TabularEditor/BestPracticeRules/master/BPARules.json\" \\\n -V \\\n -G\n\n# Exit codes:\n# 0 = no violations\n# 1 = warnings only\n# 2 = errors found (any rule with Severity >= 3) -- pipeline should FAIL","type":"text"}]},{"type":"paragraph","content":[{"text":"Switches that matter for CI/CD:","type":"text","marks":[{"type":"strong"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Switch","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-A \u003crules.json>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run BPA with the specified rules file (URL or local path)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-V","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Verbose output (lists each violation)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-G","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GitHub Actions / Azure Pipelines log format (group sections, file paths)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-D \u003cconn>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Deploy after passing BPA","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-S \u003cscript>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run a C# script before BPA (custom validation)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"Severity-driven failure:","type":"text","marks":[{"type":"strong"}]},{"text":" when a BPA rule is set to ","type":"text"},{"text":"Error","type":"text","marks":[{"type":"code_inline"}]},{"text":" (level 3), the CLI ","type":"text"},{"text":"immediately stops and exits non-zero","type":"text","marks":[{"type":"strong"}]},{"text":". Set BPA rules to Error severity for any anti-pattern that should block a PR; set to Warning for advisory-only rules.","type":"text"}]},{"type":"paragraph","content":[{"text":"Standard Microsoft rule set:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"TabularEditor/BestPracticeRules","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/TabularEditor/BestPracticeRules","title":null}}]},{"text":" -- ~60 rules covering performance, error prevention, DAX, maintenance, and naming. Always pin to a specific commit in CI.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a complete BPA rule reference (every Microsoft rule explained, plus how to author custom rules), see ","type":"text"},{"text":"references/bpa-rules-reference.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"TMDL Validation from Python (semantic-link-labs)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"%pip install semantic-link-labs -q\nimport sempy_labs as labs\n\n# Run the default BPA against a deployed model\nresults = labs.run_model_bpa(\n dataset=\"SalesModel\",\n workspace=\"Sales-Dev\",\n extended=True, # adds VertiPaq Analyzer stats for performance rules\n)\nresults.head(20)\n\n# Run BPA against every model in a workspace and store to delta\nlabs.run_model_bpa_bulk(\n workspace=\"Sales-Dev\",\n extended=True,\n)\n\n# Custom rule set from a JSON file in the lakehouse\nmy_rules = labs.model_bpa_rules() # built-in rule definitions\nmy_rules.append({\n \"ID\": \"AVOID_AUTO_DATE\",\n \"Name\": \"Disable auto date/time\",\n \"Category\": \"Performance\",\n \"Severity\": 3,\n \"Scope\": \"Model\",\n \"Expression\": \"DiscourageImplicitMeasures and not AutoDateTime\",\n})\nlabs.run_model_bpa(dataset=\"SalesModel\", rules=my_rules)","type":"text"}]},{"type":"paragraph","content":[{"text":"semantic-link-labs","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the ","type":"text"},{"text":"Python path","type":"text","marks":[{"type":"strong"}]},{"text":" for layer 3. Use it inside Fabric notebooks, scheduled BPA runs, or Spark pipelines. See ","type":"text"},{"text":"references/tmdl-validation-recipes.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full Python validation cookbook including offline TMDL parse from a local folder.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"PBIR Validation -- Layer 1 (JSON Schema)","type":"text"}]},{"type":"paragraph","content":[{"text":"Every PBIR file embeds a ","type":"text"},{"text":"$schema","type":"text","marks":[{"type":"code_inline"}]},{"text":" URL pointing to the official Microsoft schema in ","type":"text"},{"text":"microsoft/json-schemas","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/microsoft/json-schemas/tree/main/fabric/item/report/definition","title":null}}]},{"text":". This means ","type":"text"},{"text":"any","type":"text","marks":[{"type":"strong"}]},{"text":" JSON Schema validator can syntax-check PBIR files locally.","type":"text"}]},{"type":"paragraph","content":[{"text":"Python ","type":"text","marks":[{"type":"strong"}]},{"text":"jsonschema","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" validator:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"python"},"content":[{"text":"import json\nimport urllib.request\nfrom pathlib import Path\nfrom jsonschema import Draft202012Validator, RefResolver\n\ndef validate_pbir_file(pbir_file: Path) -> list[str]:\n doc = json.loads(pbir_file.read_text(encoding=\"utf-8\"))\n schema_url = doc.get(\"$schema\")\n if not schema_url:\n return [f\"{pbir_file}: no $schema declared\"]\n\n schema = json.loads(urllib.request.urlopen(schema_url).read())\n validator = Draft202012Validator(schema)\n errors = sorted(validator.iter_errors(doc), key=lambda e: e.path)\n return [f\"{pbir_file}#{'/'.join(map(str, e.path))}: {e.message}\" for e in errors]\n\n# Walk the entire PBIR folder\nreport_root = Path(\"MyProject.Report/definition\")\nall_errors = []\nfor f in report_root.rglob(\"*.json\"):\n all_errors.extend(validate_pbir_file(f))\n\nif all_errors:\n print(f\"FAIL: {len(all_errors)} schema violations\")\n for e in all_errors[:50]:\n print(f\" {e}\")\n raise SystemExit(1)\nprint(f\"OK: validated {sum(1 for _ in report_root.rglob('*.json'))} PBIR files\")","type":"text"}]},{"type":"paragraph","content":[{"text":"Cache the schemas locally","type":"text","marks":[{"type":"strong"}]},{"text":" for offline CI: ","type":"text"},{"text":"git clone https://github.com/microsoft/json-schemas.git","type":"text","marks":[{"type":"code_inline"}]},{"text":" once, then point ","type":"text"},{"text":"RefResolver","type":"text","marks":[{"type":"code_inline"}]},{"text":" at the local copy. Stops your CI from making 1000+ HTTP calls per build.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"PBIR Validation -- Layer 3 (PBI-InspectorV2 / Fab Inspector)","type":"text"}]},{"type":"paragraph","content":[{"text":"NatVanG/PBI-InspectorV2","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/NatVanG/PBI-InspectorV2","title":null}}]},{"text":" (also known as ","type":"text"},{"text":"Fab Inspector","type":"text","marks":[{"type":"strong"}]},{"text":") is the canonical rules-based PBIR/PBIP validator. v2.3+ supports all Fabric item types (semantic models, reports, notebooks, lakehouses) via the ","type":"text"},{"text":"-fabricitem","type":"text","marks":[{"type":"code_inline"}]},{"text":" switch and the new PBIR enhanced format (the original ","type":"text"},{"text":"PBI-Inspector","type":"text","marks":[{"type":"code_inline"}]},{"text":" repo only handles PBIR-Legacy).","type":"text"}]},{"type":"paragraph","content":[{"text":"Install (cross-platform .NET tool):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Download the latest release from https://github.com/NatVanG/PBI-InspectorV2/releases\n# Or use the published Docker image\ndocker pull natvang/pbi-inspector-v2:latest","type":"text"}]},{"type":"paragraph","content":[{"text":"Run against a PBIP folder:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"PBIInspectorCLI \\\n -fabricitem \"./MyProject.Report\" \\\n -rules \"./pbi-inspector-rules.json\" \\\n -formats \"JSON,HTML,GitHub\" \\\n -output \"./inspector-results\"\n\n# Exit codes:\n# 0 = all rules passed\n# 1 = warnings only\n# 2 = at least one Error-severity rule failed","type":"text"}]},{"type":"paragraph","content":[{"text":"Rules format","type":"text","marks":[{"type":"strong"}]},{"text":" -- start from ","type":"text"},{"text":"Base-rules.json","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/NatVanG/PBI-InspectorV2/blob/main/Rules/Base-rules.json","title":null}}]},{"text":" and customize. Each rule has:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Name","type":"text","marks":[{"type":"code_inline"}]},{"text":" (display)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Description","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"LogType","type":"text","marks":[{"type":"code_inline"}]},{"text":" (Error / Warning / Info)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Disabled","type":"text","marks":[{"type":"code_inline"}]},{"text":" (skip without deleting)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Path","type":"text","marks":[{"type":"code_inline"}]},{"text":" (JSONPath into PBIR file)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Test","type":"text","marks":[{"type":"code_inline"}]},{"text":" (one of ","type":"text"},{"text":"isEqualTo","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"isGreaterThan","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"isLessThan","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"mustExist","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"mustNotExist","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"regex","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc.)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Common rules to enforce on every PBIR PR:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"[\n {\n \"Name\": \"All visuals have a title\",\n \"LogType\": \"Error\",\n \"Path\": \"$.visual.objects.title[0].properties.show.expr.Literal.Value\",\n \"Test\": \"isEqualTo\",\n \"Expected\": \"true\"\n },\n {\n \"Name\": \"Page count under limit\",\n \"LogType\": \"Error\",\n \"Path\": \"$.pages\",\n \"Test\": \"arrayLengthLessThan\",\n \"Expected\": 1000\n },\n {\n \"Name\": \"Bookmarks reference real pages\",\n \"LogType\": \"Error\",\n \"Path\": \"$.children[?(@.targetSection)].targetSection\",\n \"Test\": \"mustResolveToPage\"\n }\n]","type":"text"}]},{"type":"paragraph","content":[{"text":"Full rule examples and CI gating patterns in ","type":"text"},{"text":"references/pbir-validation-recipes.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Fabric CI/CD, DAX, Lineage, CI Gates & Error Catalog","type":"text"}]},{"type":"paragraph","content":[{"text":"Focused recipes for fabric-cicd pre-deployment validation, DAX syntax validation without a server, and lineage / cross-reference validation live in ","type":"text"},{"text":"references/fabric-dax-lineage-validation.md","type":"text","marks":[{"type":"code_inline"}]},{"text":". GitHub Actions CI gate patterns, common error mappings, and static-validation limits live in ","type":"text"},{"text":"references/ci-gates-and-error-catalog.md","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Additional Resources","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Reference Files","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/tmdl-validation-recipes.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- Full TMDL validation cookbook: TmdlSerializer C# patterns, Python pythonnet wrapper, Tabular Editor C# scripts, INFO DAX introspection, offline parsing","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/pbir-validation-recipes.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- PBIR JSON schema validation, PBI-InspectorV2 rule examples, lineage cross-reference linter, GitHub Actions integration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/bpa-rules-reference.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- The standard Microsoft BPA ruleset summary, rule authoring guide, severity strategy, and pinning recipes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/fabric-dax-lineage-validation.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- fabric-cicd, DAX syntax, and lineage validation recipes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"references/ci-gates-and-error-catalog.md","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- CI gate patterns, common validation errors, and static-validation limits","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Related Skills","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"powerbi-master:tmdl-mastery","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- TMDL syntax reference (use this when generating TMDL; come back here to validate it)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"powerbi-master:programmatic-development","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- PBIR generation (use this when generating PBIR; come back here to validate it)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"powerbi-master:performance-optimization","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" -- For run-time validation via DAX Studio, VertiPaq Analyzer, Performance Analyzer","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Official 2026 References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TmdlSerializer Class (Microsoft Learn)","type":"text","marks":[{"type":"link","attrs":{"href":"https://learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.tabular.tmdlserializer","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TmdlFormatException Class (Microsoft Learn)","type":"text","marks":[{"type":"link","attrs":{"href":"https://learn.microsoft.com/en-us/dotnet/api/microsoft.analysisservices.tabular.tmdl.tmdlformatexception","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Tabular Editor BPA documentation","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.tabulareditor.com/common/using-bpa.html","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"TabularEditor/BestPracticeRules (official Microsoft rule set)","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/TabularEditor/BestPracticeRules","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NatVanG/PBI-InspectorV2 (Fab Inspector)","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/NatVanG/PBI-InspectorV2","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"semantic-link-labs Best Practice Analyzer","type":"text","marks":[{"type":"link","attrs":{"href":"https://semantic-link-labs.readthedocs.io/en/stable/sempy_labs.html","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"microsoft/json-schemas (PBIR official JSON schemas)","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/microsoft/json-schemas/tree/main/fabric/item/report/definition","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"fabric-cicd parameterization","type":"text","marks":[{"type":"link","attrs":{"href":"https://microsoft.github.io/fabric-cicd/latest/how_to/parameterization/","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"validation-testing","author":"@skillopedia","source":{"stars":39,"repo_name":"claude-plugin-marketplace","origin_url":"https://github.com/josiahsiegel/claude-plugin-marketplace/blob/HEAD/plugins/powerbi-master/skills/validation-testing/SKILL.md","repo_owner":"josiahsiegel","body_sha256":"a9c05d063284d74f84553eac707f7eb6bfc4e5c30d7a80108bd8cfb32230abb9","cluster_key":"ac551d0e93ed6804f3fa8841a88bdd3159c23446deb0db06f4c9be24cd93d7b5","clean_bundle":{"format":"clean-skill-bundle-v1","source":"josiahsiegel/claude-plugin-marketplace/plugins/powerbi-master/skills/validation-testing/SKILL.md","attachments":[{"id":"2306d44a-ea92-532e-9675-7476e9284844","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/2306d44a-ea92-532e-9675-7476e9284844/attachment.md","path":"references/bpa-rules-reference.md","size":14637,"sha256":"0890abdcb0977d93625112437fda4a9d324afc2cf88630eb08f0e058439168de","contentType":"text/markdown; charset=utf-8"},{"id":"017654ca-720d-5ae0-a6d7-d9f1125d9196","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/017654ca-720d-5ae0-a6d7-d9f1125d9196/attachment.md","path":"references/ci-gates-and-error-catalog.md","size":4634,"sha256":"6e5a36e6343c90ba362510a893efe8ecc2a913889c598519e156361490c7cd94","contentType":"text/markdown; charset=utf-8"},{"id":"ce4c6dee-45b8-5224-ac18-f03b90c6de56","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/ce4c6dee-45b8-5224-ac18-f03b90c6de56/attachment.md","path":"references/fabric-dax-lineage-validation.md","size":2733,"sha256":"bc26c21313261c2f1cfca4298614badbe078425bb467bb9b91ca64025046f6ba","contentType":"text/markdown; charset=utf-8"},{"id":"cf368fee-fb51-547b-807e-178ccfd244c8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/cf368fee-fb51-547b-807e-178ccfd244c8/attachment.md","path":"references/pbir-validation-recipes.md","size":19421,"sha256":"a28b56228ebf42ddeebab8348b35987d67695e0c80482ea7f1966baea4ab84f2","contentType":"text/markdown; charset=utf-8"},{"id":"336cefb4-90b9-538d-a2b7-97f35c9af75e","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/336cefb4-90b9-538d-a2b7-97f35c9af75e/attachment.md","path":"references/tmdl-validation-recipes.md","size":14756,"sha256":"afbce08889d852c0593a6c9c5c3b42f7a002ccad0befcb17ef287862bf66e516","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"ac999d85bb2957b9c9bc847428e5e494797b185a5957d38dea3484cbf9136d64","attachment_count":5,"text_attachments":5,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"plugins/powerbi-master/skills/validation-testing/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"clean-skills-v1","description":"TMDL and PBIR validation, linting, and pre-deployment testing.\nPROACTIVELY activate for: (1) validating TMDL syntax before deploy, (2) validating PBIR schema, (3) catching TmdlFormatException / TmdlSerializationException early, (4) Best Practice Analyzer (BPA) rules and BPA CLI, (5) Tabular Editor BPA scripting, (6) PBI-Inspector / PBI-InspectorV2 / Fab Inspector, (7) PBIR JSON schema validation, (8) pre-deployment validation in CI, (9) fabric-cicd parameter.yml validation, (10) catching breaking changes between TMDL versions.\nProvides: BPA rule library, validation CLI commands, CI integration for validation, error catalog (TmdlFormatException, etc.), and a pre-deploy validation playbook.\n"}},"renderedAt":1782979331724}

Power BI Validation and Self-Testing Overview Validation skill for any TMDL, PBIR, DAX, or M artifact a developer (or Claude) generates. The goal: catch syntax, schema, and best-practice errors locally before a Fabric REST deploy fails. This skill is essential for the powerbi-expert agent's Self-Validation Protocol -- whenever the agent writes TMDL or PBIR, it should describe (or run) the matching validation step from this skill. As of 2026, Power BI validation has four distinct layers, each catching a different class of error: | Layer | TMDL Tool | PBIR Tool | What it catches | |-------|----…