PBIP Build Verification
When to Use
- A
.pbipreport was built or modified from an approved_brief/report-spec.mdand needs checking before Desktop reload or handoff. - A build renders correctly on screen but you want to confirm it matches the spec, not just that it looks plausible.
- Remediating a prior build against a punch list, not generating one from scratch.
- No
_brief/report-spec.mdexists yet — usepowerbi-report-planning/powerbi-report-designfirst; this skill has nothing to check a build against without it.
Workflow
- Locate the approved
_brief/report-spec.md— specifically its embeddedDesign Brief:YAML (color_map,pages[].layout_contract.placements[]withfield_bindings/sort_policy/color_strategy) and its## Model requirementssection — and the target.pbipproject's*.SemanticModel/definition/and*.Report/definition/pages/**/visuals/*.json. - Check the semantic model — 5 items.
report-spec.md's## Model requirementssection only lists new/changed measures and columns in prose; it is not a governance contract. For structural governance, check againstsemantic-model-authoring's ownmodeling-guidelines.mddefaults instead — these are that skill's stated rules, not an invented standard, so a failure here means the build didn't follow guidance already available to it:- Date table governance: a named calendar table exists; auto date/time (
LocalDateTable_*,DateTableTemplate_*) is removed, not just unused. - Hidden columns: every raw column an explicit measure aggregates is
isHidden; no aggregatable raw column sits visible next to its measure. - Naming: table/column/measure names are business-friendly, not source-system names — cross-check any explicit new-measure/new-column names the report spec's
## Model requirementsnames. - Format strings: every measure has one, matching the spec's stated precision/sign convention where the spec is explicit.
- Descriptions: every visible object has a
///description that states something non-obvious, not a restatement of the name.
- Date table governance: a named calendar table exists; auto date/time (
- Check each visual against the brief's
placements[]entry for it — 4 items:- Field bindings: the
queryRef(s) in each role projection match the placement'sfield_bindings, not a lookalike or a raw column standing in for a measure. - Sort policy: matches
placements[].sort_policy(value_desc/value_asc/category_asc/natural_order/none). Known schema gap: this enum has no magnitude/absolute-value option. If the page'spurposecalls for ranking by contribution or magnitude (e.g. a YoY driver chart) and the placement only has a signedsort_policy, that is a brief gap, not a build error — the spec cannot currently express what the page needs. Report it as a brief gap for whoever ownsreport-spec.md, not as a build finding. - Color strategy / conditional formatting: the visual's conditional-formatting objects match
placements[].color_strategy(measure_match/gradient/unique/semantic/none).color_strategyonly records the design decision to use conditional formatting —powerbi-report-design's own reference material stops at "use conditional formatting for traffic-light status, heatmaps, or threshold-based alerts" and never specifies the PBIR mechanics. Check those mechanics against Known-Unsafe Build Patterns below regardless of whatcolor_strategysays. - Color literals vs.
color_map: every hex value invisual.jsonresolves to acolor_mapentry for the measure it's coloring —measure_matchmeans exact base-color reuse across every card/line/bar/map/table for that measure;gradientmeans tint-to-base for that same measure. This ispowerbi-report-design's own "Color-map contract" gotcha — audit for mismatches, including near-misses a screenshot won't catch (e.g.#2F6BFFvs. acolor_mapentry of#1F5EFF).color_mapis measure-scoped only — a placement can declarecolor_strategy: measure_matchwhile showing a color with no measure behind it (a card's own value text, a plain textbox). For those, there is nothing incolor_mapto check against; check the hex instead against the spec's "Design system summary" prose (structural text/surface roles), and flag it as a brief gap if the placement'scolor_strategyimplies acolor_mapentry that doesn't exist.
- Field bindings: the
- For anything the build attempts that has no documented-safe PBIR encoding, check it against Known-Unsafe Build Patterns first, then report it as an explicit build gap with the reason — do not guess an undocumented fix and do not silently drop it.
- Report only. Do not edit model, report, or spec files during verification, and do not fix a brief gap by inventing schema the spec doesn't have — findings go back to whoever owns the build (build gaps) or whoever owns
report-spec.md(brief gaps), the same separationgovernance-bundle-qa-agentuses against its own bundle.
Known-Unsafe Build Patterns
Carry these forward as standing checks, not historical notes — each one passed powerbi-report-author validate clean and only broke at Desktop render or reload, and none of them appear in powerbi-report-design's own gotchas/anti-patterns lists (those cover visual/design quality; these are DAX-query-engine and TMDL-parser structural failures):
- TopN filter ordered by a DAX measure.
OrderByinside a TopN filter's subquery must be a raw-columnAggregation, not aMeasureexpression — documented as a Desktop crash (SemanticQueryRewriter.rewriteOrderBy), not a silent failure. If the ranking value is a measure, TopN-by-N has no documented-safe encoding; report it as a build gap. - Conditional
dataPoint.fillwith no selector. Renders successfully but paints every data point the same color regardless of the rule. Requires"selector": {"data": [{"dataViewWildcard": {"matchingOption": 0}}]}to evaluate per point. isAvailableInMdx: falseon asortByColumntarget. Breaks the column it sorts withdoes not have an attribute hierarchy enabled. Check every hidden column against what references it (sort-by, hierarchy, variation) before disabling its MDX availability.- TMDL
///description trailing an object's properties instead of leading them. Parses for some placements, throwsUnexpected line type: Empty!for others depending on what follows. Always place the description directly above thetable/column/measureline, never after. - Comparing a
date-typed source column to a#datetime(...)M literal. ThrowsWe cannot apply operator < to types DateTime and Dateat refresh, not at validation. Match the literal constructor to the source column's actual type. - A measure and a hidden column sharing the same name in one table. Passes
powerbi-report-author validateclean, then fails Desktop load itself (not render, not reload) withThe '<name>' measure cannot be created because a column with the same name already exists. Check every measure name against every column name in its own table, including hidden ones — a friendly-named measure paired with an identically-friendly-named raw column is the trap (e.g. measureUnits Inover hidden columnUnits In); keep the raw column's name distinct (its source-cased name, e.g.UnitsIn, is usually enough). - Visual-level
filterConfignested insidevisualinstead of as its sibling.powerbi-report-author validatecatches this cleanly (/visual must NOT have additional properties (property: "filterConfig")) — but if a build skipped that validation step, the schema error is the tell.
Output Template
# Build Verification Report
## Scope
- Report spec: [path to _brief/report-spec.md]
- Build: [.pbip path]
- Checked: [date]
## Semantic model
| Check | Status | Finding |
|---|---|---|
| Date table governance | Pass/Gap | |
| Hidden columns | Pass/Gap | |
| Naming | Pass/Gap | |
| Format strings | Pass/Gap | |
| Descriptions | Pass/Gap | |
## Visuals
| Visual | Field bindings | Sort policy | Color strategy / conditional formatting | Color literals vs. color_map |
|---|---|---|---|---|
## Known-unsafe patterns encountered
- ...
## Build gaps (spec asks for something the build didn't safely express)
- ...
## Brief gaps (the page needs something report-spec.md's schema can't express — e.g. magnitude sort)
- ...
Quality Checklist
- Every finding cites a file and, where possible, a property path — not a general impression.
- Every "Pass" was checked against
report-spec.md's actual stated value, not assumed from a render that looked right. - Build gaps and brief gaps are never conflated — they have different owners.
- Every dropped requirement is logged as a gap with a reason, never silently omitted.
- The report contains no fixes — only findings, routed back to whoever owns the build or the spec.
- Known-Unsafe Build Patterns is checked explicitly, not relied on to be remembered.
Relationship to Other Skills
- Consumes
_brief/report-spec.mdas produced bypowerbi-report-planning(the locked spec) andpowerbi-report-design(the embeddedDesign Brief:YAML, per its ownreferences/design-brief.mdcontract) — both are Claude Code plugin skills (microsoft/skills-for-fabric,powerbi-authoringbundle), a different host than this Cursor skill library. This skill checks their output; it does not replace or re-implement them. - Checks model structure against
semantic-model-authoring'smodeling-guidelines.md, and PBIR mechanics againstpowerbi-report-authoring's own validation loop having already run (powerbi-report-author validateclean is a prerequisite, not a substitute — see Known-Unsafe Build Patterns for what it doesn't catch). - Checks metric definitions authored by
dashboard-metric-semanticsfor internal consistency, but does not author them. - PBIP/TMDL-specific, unlike
dashboard-frontend-implementation's generic web-component scope — the two don't overlap.