@luquimbo/bi-superpowers 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +46 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +4 -0
- package/AGENTS.md +34 -7
- package/README.md +139 -118
- package/bin/cli.js +45 -31
- package/bin/commands/install.js +321 -0
- package/bin/lib/microsoft-mcp.js +8 -0
- package/bin/lib/microsoft-mcp.test.js +5 -0
- package/package.json +1 -1
- package/skills/contributions/SKILL.md +1 -1
- package/skills/data-model-design/SKILL.md +1 -1
- package/skills/data-modeling/SKILL.md +82 -56
- package/skills/data-quality/SKILL.md +1 -1
- package/skills/dax/SKILL.md +74 -36
- package/skills/dax-doctor/SKILL.md +1 -1
- package/skills/dax-udf/SKILL.md +1 -1
- package/skills/deployment/SKILL.md +1 -1
- package/skills/excel-formulas/SKILL.md +1 -1
- package/skills/fabric-scripts/SKILL.md +1 -1
- package/skills/fast-standard/SKILL.md +1 -1
- package/skills/governance/SKILL.md +103 -50
- package/skills/migration-assistant/SKILL.md +1 -1
- package/skills/model-documenter/SKILL.md +1 -1
- package/skills/pbi-connect/SKILL.md +1 -1
- package/skills/power-query/SKILL.md +1 -1
- package/skills/project-kickoff/SKILL.md +1 -1
- package/skills/query-performance/SKILL.md +1 -1
- package/skills/report-design/SKILL.md +1 -1
- package/skills/report-layout/SKILL.md +1 -1
- package/skills/rls-design/SKILL.md +1 -1
- package/skills/semantic-model/SKILL.md +1 -1
- package/skills/testing-validation/SKILL.md +1 -1
- package/skills/theme-tweaker/SKILL.md +1 -1
- package/src/content/mcp-requirements.json +13 -0
- package/src/content/skills/data-modeling.md +81 -55
- package/src/content/skills/dax.md +73 -35
- package/src/content/skills/governance.md +102 -49
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install Command - Multi-agent skill installer
|
|
3
|
+
* ===============================================
|
|
4
|
+
*
|
|
5
|
+
* Installs BI Agent Superpowers skills into the correct directories
|
|
6
|
+
* for each AI coding agent. Inspired by the `npx skills` CLI from Vercel Labs.
|
|
7
|
+
*
|
|
8
|
+
* Skills are always installed at the user level (~/) to protect licensed content.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx @luquimbo/bi-superpowers install
|
|
12
|
+
* super install
|
|
13
|
+
* super install --agent claude-code --agent codex
|
|
14
|
+
* super install --all --yes
|
|
15
|
+
*
|
|
16
|
+
* @module commands/install
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const readline = require('readline');
|
|
23
|
+
|
|
24
|
+
// Agent registry: each agent's skill directory path (relative to project or home)
|
|
25
|
+
// Based on the Agent Skills specification supported by 45+ agents
|
|
26
|
+
const AGENTS = {
|
|
27
|
+
'claude-code': { name: 'Claude Code', dir: '.claude/skills' },
|
|
28
|
+
'github-copilot': { name: 'GitHub Copilot', dir: '.github/skills' },
|
|
29
|
+
codex: { name: 'Codex (OpenAI)', dir: '.agents/skills' },
|
|
30
|
+
cursor: { name: 'Cursor', dir: '.cursor/skills' },
|
|
31
|
+
windsurf: { name: 'Windsurf', dir: '.windsurf/skills' },
|
|
32
|
+
cline: { name: 'Cline', dir: '.cline/skills' },
|
|
33
|
+
continue: { name: 'Continue', dir: '.continue/skills' },
|
|
34
|
+
'roo-code': { name: 'Roo Code', dir: '.roo/skills' },
|
|
35
|
+
augment: { name: 'Augment', dir: '.augment/skills' },
|
|
36
|
+
amp: { name: 'Amp', dir: '.amp/skills' },
|
|
37
|
+
kilo: { name: 'Kilo Code', dir: '.kilocode/skills' },
|
|
38
|
+
opencode: { name: 'OpenCode', dir: '.agents/skills' },
|
|
39
|
+
openhands: { name: 'OpenHands', dir: '.openhands/skills' },
|
|
40
|
+
goose: { name: 'Goose', dir: '.goose/skills' },
|
|
41
|
+
'gemini-cli': { name: 'Gemini CLI', dir: '.gemini/skills' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Universal path — most agents read from .agents/skills/
|
|
45
|
+
const UNIVERSAL_DIR = '.agents/skills';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Detect which agents are available by checking for their config directories
|
|
49
|
+
* @param {string} baseDir - Directory to check (project root or home)
|
|
50
|
+
* @returns {string[]} Array of detected agent IDs
|
|
51
|
+
*/
|
|
52
|
+
function detectAgents(baseDir) {
|
|
53
|
+
const detected = [];
|
|
54
|
+
for (const [id, agent] of Object.entries(AGENTS)) {
|
|
55
|
+
const agentRoot = path.dirname(agent.dir);
|
|
56
|
+
const checkPath = path.join(baseDir, agentRoot);
|
|
57
|
+
if (fs.existsSync(checkPath)) {
|
|
58
|
+
detected.push(id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return detected;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create readline interface for interactive prompts
|
|
66
|
+
*/
|
|
67
|
+
function createReadline() {
|
|
68
|
+
return readline.createInterface({
|
|
69
|
+
input: process.stdin,
|
|
70
|
+
output: process.stdout,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Prompt user with a question
|
|
76
|
+
*/
|
|
77
|
+
function prompt(rl, question) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Display a numbered list and let user pick multiple items
|
|
85
|
+
* @param {readline.Interface} rl
|
|
86
|
+
* @param {Array<{id: string, name: string}>} items
|
|
87
|
+
* @param {string[]} preselected - IDs to preselect
|
|
88
|
+
* @returns {Promise<string[]>} Selected IDs
|
|
89
|
+
*/
|
|
90
|
+
async function selectMultiple(rl, items, preselected = []) {
|
|
91
|
+
items.forEach((item, i) => {
|
|
92
|
+
const marker = preselected.includes(item.id) ? '●' : '○';
|
|
93
|
+
console.log(` ${i + 1}) ${marker} ${item.name}`);
|
|
94
|
+
});
|
|
95
|
+
console.log();
|
|
96
|
+
console.log(' Enter numbers separated by commas (e.g. 1,2,3)');
|
|
97
|
+
console.log(' Press Enter for detected agents, or "a" for all');
|
|
98
|
+
|
|
99
|
+
const answer = await prompt(rl, '\n > ');
|
|
100
|
+
|
|
101
|
+
if (answer.toLowerCase() === 'a') {
|
|
102
|
+
return items.map((item) => item.id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (answer === '') {
|
|
106
|
+
return preselected.length > 0 ? preselected : items.map((item) => item.id);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const indices = answer
|
|
110
|
+
.split(',')
|
|
111
|
+
.map((s) => parseInt(s.trim(), 10) - 1)
|
|
112
|
+
.filter((i) => i >= 0 && i < items.length);
|
|
113
|
+
|
|
114
|
+
return indices.map((i) => items[i].id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Copy a skill directory to a target location
|
|
119
|
+
* @param {string} srcDir - Source skill directory (containing SKILL.md)
|
|
120
|
+
* @param {string} destDir - Destination directory
|
|
121
|
+
*/
|
|
122
|
+
function copySkillDir(srcDir, destDir) {
|
|
123
|
+
if (!fs.existsSync(destDir)) {
|
|
124
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
128
|
+
for (const entry of entries) {
|
|
129
|
+
const srcPath = path.join(srcDir, entry.name);
|
|
130
|
+
const destPath = path.join(destDir, entry.name);
|
|
131
|
+
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
copySkillDir(srcPath, destPath);
|
|
134
|
+
} else {
|
|
135
|
+
fs.copyFileSync(srcPath, destPath);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Main install command handler
|
|
142
|
+
* @param {string[]} args - CLI arguments
|
|
143
|
+
* @param {Object} config - Command config from CLI
|
|
144
|
+
*/
|
|
145
|
+
async function installCommand(args, config) {
|
|
146
|
+
const chalk = require('chalk');
|
|
147
|
+
const boxen = require('boxen');
|
|
148
|
+
|
|
149
|
+
const isYes = args.includes('--yes') || args.includes('-y');
|
|
150
|
+
const isAll = args.includes('--all');
|
|
151
|
+
const agentFlags = [];
|
|
152
|
+
|
|
153
|
+
// Parse --agent flags
|
|
154
|
+
for (let i = 0; i < args.length; i++) {
|
|
155
|
+
if ((args[i] === '--agent' || args[i] === '-a') && args[i + 1]) {
|
|
156
|
+
agentFlags.push(args[i + 1]);
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Always install at user level (home directory) to protect licensed content
|
|
162
|
+
const baseDir = os.homedir();
|
|
163
|
+
|
|
164
|
+
// Find skills from the package
|
|
165
|
+
const packageDir = config.packageDir || path.dirname(__dirname);
|
|
166
|
+
const skillsSourceDir = path.join(packageDir, 'skills');
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(skillsSourceDir)) {
|
|
169
|
+
console.error(chalk.red('Skills directory not found. Try reinstalling the package.'));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Read available skills
|
|
174
|
+
const skillDirs = fs
|
|
175
|
+
.readdirSync(skillsSourceDir, { withFileTypes: true })
|
|
176
|
+
.filter((d) => d.isDirectory() && fs.existsSync(path.join(skillsSourceDir, d.name, 'SKILL.md')))
|
|
177
|
+
.map((d) => d.name);
|
|
178
|
+
|
|
179
|
+
// Header
|
|
180
|
+
console.log(
|
|
181
|
+
boxen(
|
|
182
|
+
chalk.bold.cyan('BI Agent Superpowers') +
|
|
183
|
+
chalk.gray(` v${config.version}`) +
|
|
184
|
+
'\n' +
|
|
185
|
+
chalk.gray('Multi-agent skill installer'),
|
|
186
|
+
{
|
|
187
|
+
padding: 1,
|
|
188
|
+
borderStyle: 'round',
|
|
189
|
+
borderColor: 'cyan',
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
console.log(chalk.gray(` Install path: ~/${UNIVERSAL_DIR}/`));
|
|
195
|
+
console.log(chalk.gray(` Skills: ${skillDirs.length} available\n`));
|
|
196
|
+
|
|
197
|
+
// Determine which agents to install for
|
|
198
|
+
let selectedAgents;
|
|
199
|
+
|
|
200
|
+
if (isAll) {
|
|
201
|
+
selectedAgents = Object.keys(AGENTS);
|
|
202
|
+
} else if (agentFlags.length > 0) {
|
|
203
|
+
selectedAgents = agentFlags.filter((a) => AGENTS[a]);
|
|
204
|
+
const unknown = agentFlags.filter((a) => !AGENTS[a]);
|
|
205
|
+
if (unknown.length > 0) {
|
|
206
|
+
console.log(chalk.yellow(` Unknown agents: ${unknown.join(', ')}`));
|
|
207
|
+
console.log(chalk.gray(` Available: ${Object.keys(AGENTS).join(', ')}\n`));
|
|
208
|
+
}
|
|
209
|
+
} else if (isYes) {
|
|
210
|
+
selectedAgents = Object.keys(AGENTS);
|
|
211
|
+
} else {
|
|
212
|
+
// Interactive mode: detect and ask
|
|
213
|
+
const detected = detectAgents(baseDir);
|
|
214
|
+
|
|
215
|
+
console.log(chalk.cyan(' Select agents to install for:\n'));
|
|
216
|
+
|
|
217
|
+
const items = Object.entries(AGENTS).map(([id, agent]) => ({
|
|
218
|
+
id,
|
|
219
|
+
name: detected.includes(id) ? `${agent.name} ${chalk.green('(detected)')}` : agent.name,
|
|
220
|
+
}));
|
|
221
|
+
|
|
222
|
+
const rl = createReadline();
|
|
223
|
+
try {
|
|
224
|
+
selectedAgents = await selectMultiple(rl, items, detected);
|
|
225
|
+
} finally {
|
|
226
|
+
rl.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (selectedAgents.length === 0) {
|
|
231
|
+
console.log(chalk.yellow('\n No agents selected. Nothing to install.'));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
console.log(
|
|
236
|
+
chalk.cyan(`\n Installing ${skillDirs.length} skills for ${selectedAgents.length} agents...\n`)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Always install to universal path first
|
|
240
|
+
const universalTarget = path.join(baseDir, UNIVERSAL_DIR);
|
|
241
|
+
let installedCount = 0;
|
|
242
|
+
|
|
243
|
+
for (const skill of skillDirs) {
|
|
244
|
+
const src = path.join(skillsSourceDir, skill);
|
|
245
|
+
const dest = path.join(universalTarget, skill);
|
|
246
|
+
copySkillDir(src, dest);
|
|
247
|
+
installedCount++;
|
|
248
|
+
}
|
|
249
|
+
console.log(chalk.green(` ✓ ${UNIVERSAL_DIR}/ (${installedCount} skills)`));
|
|
250
|
+
|
|
251
|
+
// Symlink agent-specific directories to universal path
|
|
252
|
+
const agentResults = [];
|
|
253
|
+
for (const agentId of selectedAgents) {
|
|
254
|
+
const agent = AGENTS[agentId];
|
|
255
|
+
if (agent.dir === UNIVERSAL_DIR) continue; // Already handled
|
|
256
|
+
|
|
257
|
+
const agentTarget = path.join(baseDir, agent.dir);
|
|
258
|
+
|
|
259
|
+
// If the directory already exists and is not a symlink, copy instead
|
|
260
|
+
if (fs.existsSync(agentTarget) && !fs.lstatSync(agentTarget).isSymbolicLink()) {
|
|
261
|
+
// Copy skills into existing directory
|
|
262
|
+
for (const skill of skillDirs) {
|
|
263
|
+
copySkillDir(path.join(skillsSourceDir, skill), path.join(agentTarget, skill));
|
|
264
|
+
}
|
|
265
|
+
agentResults.push({ agent: agent.name, method: 'copied', dir: agent.dir });
|
|
266
|
+
} else {
|
|
267
|
+
// Create symlink to universal directory
|
|
268
|
+
const parentDir = path.dirname(agentTarget);
|
|
269
|
+
if (!fs.existsSync(parentDir)) {
|
|
270
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Remove existing symlink if present
|
|
274
|
+
if (fs.existsSync(agentTarget)) {
|
|
275
|
+
fs.unlinkSync(agentTarget);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
// Relative symlink from agent dir to universal dir
|
|
280
|
+
const relPath = path.relative(parentDir, universalTarget);
|
|
281
|
+
fs.symlinkSync(relPath, agentTarget);
|
|
282
|
+
agentResults.push({ agent: agent.name, method: 'symlinked', dir: agent.dir });
|
|
283
|
+
} catch (_e) {
|
|
284
|
+
// Fallback to copy if symlink fails (e.g. Windows without admin)
|
|
285
|
+
if (!fs.existsSync(agentTarget)) {
|
|
286
|
+
fs.mkdirSync(agentTarget, { recursive: true });
|
|
287
|
+
}
|
|
288
|
+
for (const skill of skillDirs) {
|
|
289
|
+
copySkillDir(path.join(skillsSourceDir, skill), path.join(agentTarget, skill));
|
|
290
|
+
}
|
|
291
|
+
agentResults.push({ agent: agent.name, method: 'copied', dir: agent.dir });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Print results
|
|
297
|
+
for (const result of agentResults) {
|
|
298
|
+
const icon = result.method === 'symlinked' ? '→' : '✓';
|
|
299
|
+
console.log(chalk.green(` ${icon} ${result.dir}/ (${result.method}) — ${result.agent}`));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Summary
|
|
303
|
+
const totalAgents = agentResults.length + 1; // +1 for universal
|
|
304
|
+
console.log(
|
|
305
|
+
boxen(
|
|
306
|
+
chalk.green.bold(`Installed ${installedCount} skills for ${totalAgents} agents`) +
|
|
307
|
+
'\n\n' +
|
|
308
|
+
chalk.gray('Skills are ready to use. Open your AI agent and start prompting.') +
|
|
309
|
+
'\n' +
|
|
310
|
+
chalk.gray('Example: "Help me write a DAX measure for YTD sales"'),
|
|
311
|
+
{
|
|
312
|
+
padding: 1,
|
|
313
|
+
margin: { top: 1 },
|
|
314
|
+
borderStyle: 'round',
|
|
315
|
+
borderColor: 'green',
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = installCommand;
|
package/bin/lib/microsoft-mcp.js
CHANGED
|
@@ -13,9 +13,11 @@ const path = require('path');
|
|
|
13
13
|
|
|
14
14
|
const REMOTE_POWERBI_URL = 'https://api.fabric.microsoft.com/v1/mcp/powerbi';
|
|
15
15
|
const FABRIC_MCP_PACKAGE = '@microsoft/fabric-mcp@latest';
|
|
16
|
+
const MICROSOFT_LEARN_URL = 'https://learn.microsoft.com/api/mcp';
|
|
16
17
|
const MODELING_SERVER_NAME = 'powerbi-modeling-mcp';
|
|
17
18
|
const REMOTE_SERVER_NAME = 'powerbi-remote';
|
|
18
19
|
const FABRIC_SERVER_NAME = 'fabric-mcp-server';
|
|
20
|
+
const LEARN_SERVER_NAME = 'microsoft-learn';
|
|
19
21
|
const ABSOLUTE_LAUNCHER_MODE = 'absolute';
|
|
20
22
|
const PLUGIN_ROOT_LAUNCHER_MODE = 'plugin-root';
|
|
21
23
|
|
|
@@ -63,6 +65,10 @@ function createPluginMcpConfig(options = {}) {
|
|
|
63
65
|
command: 'node',
|
|
64
66
|
args: [launcherPath],
|
|
65
67
|
},
|
|
68
|
+
[LEARN_SERVER_NAME]: {
|
|
69
|
+
type: 'http',
|
|
70
|
+
url: MICROSOFT_LEARN_URL,
|
|
71
|
+
},
|
|
66
72
|
};
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -166,9 +172,11 @@ module.exports = {
|
|
|
166
172
|
PLUGIN_ROOT_LAUNCHER_MODE,
|
|
167
173
|
REMOTE_POWERBI_URL,
|
|
168
174
|
FABRIC_MCP_PACKAGE,
|
|
175
|
+
MICROSOFT_LEARN_URL,
|
|
169
176
|
MODELING_SERVER_NAME,
|
|
170
177
|
REMOTE_SERVER_NAME,
|
|
171
178
|
FABRIC_SERVER_NAME,
|
|
179
|
+
LEARN_SERVER_NAME,
|
|
172
180
|
resolveModelingLauncherPath,
|
|
173
181
|
createPluginMcpConfig,
|
|
174
182
|
createMcpConfigForFormat,
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
PLUGIN_ROOT_LAUNCHER_MODE,
|
|
12
12
|
REMOTE_POWERBI_URL,
|
|
13
13
|
FABRIC_MCP_PACKAGE,
|
|
14
|
+
MICROSOFT_LEARN_URL,
|
|
14
15
|
createPluginMcpConfig,
|
|
15
16
|
createMcpConfigForFormat,
|
|
16
17
|
mergeMcpConfig,
|
|
@@ -58,6 +59,10 @@ describe('createPluginMcpConfig', () => {
|
|
|
58
59
|
assert.strictEqual(config['powerbi-modeling-mcp'].type, 'stdio');
|
|
59
60
|
assert.strictEqual(config['powerbi-modeling-mcp'].command, 'node');
|
|
60
61
|
assert.ok(config['powerbi-modeling-mcp'].args[0].endsWith('powerbi-modeling-launcher.js'));
|
|
62
|
+
assert.deepStrictEqual(config['microsoft-learn'], {
|
|
63
|
+
type: 'http',
|
|
64
|
+
url: MICROSOFT_LEARN_URL,
|
|
65
|
+
});
|
|
61
66
|
});
|
|
62
67
|
});
|
|
63
68
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "data-model-design"
|
|
3
3
|
description: "Use when the user asks about Data Model Design Skill, especially phrases like \"diseñar modelo de datos\", \"crear modelo Power BI\", \"arquitectura de datos\", \"empezar proyecto BI\", \"nuevo modelo semántico\"."
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.1.1"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/data-model-design.md instead. -->
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "data-modeling"
|
|
3
3
|
description: "Use when the user asks about Data Modeling Skill, especially phrases like \"data model\", \"star schema\", \"fact table\", \"relationship\", \"surrogate key\", \"SCD\"."
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.1.1"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/data-modeling.md instead. -->
|
|
@@ -30,56 +30,60 @@ Best practices for designing semantic models in Power BI.
|
|
|
30
30
|
|
|
31
31
|
### Fact Tables
|
|
32
32
|
- Contain measurable events/transactions
|
|
33
|
+
- **Plural names** (multiple events): `Sales`, `Orders`, `Transactions`
|
|
34
|
+
- **No `Fact_` prefix** — that's developer jargon, users see this in the model
|
|
33
35
|
- Granularity: one row per event/transaction
|
|
34
|
-
- Include foreign keys to dimension tables
|
|
35
|
-
-
|
|
36
|
+
- Include foreign keys to dimension tables (hidden from report view)
|
|
37
|
+
- Hide raw numeric columns; expose them through explicit measures
|
|
36
38
|
|
|
37
39
|
```
|
|
38
|
-
|
|
39
|
-
├──
|
|
40
|
-
├── DateKey (FK)
|
|
41
|
-
├── ProductKey (FK)
|
|
42
|
-
├── CustomerKey (FK)
|
|
43
|
-
├── StoreKey (FK)
|
|
44
|
-
├── Quantity
|
|
45
|
-
├──
|
|
46
|
-
└──
|
|
40
|
+
Sales ← plural, business name, no prefix
|
|
41
|
+
├── SalesKey (PK, hidden)
|
|
42
|
+
├── DateKey (FK, hidden)
|
|
43
|
+
├── ProductKey (FK, hidden)
|
|
44
|
+
├── CustomerKey (FK, hidden)
|
|
45
|
+
├── StoreKey (FK, hidden)
|
|
46
|
+
├── Quantity (hidden, exposed via [Quantity Sold] measure)
|
|
47
|
+
├── Unit Price (hidden, used in measures)
|
|
48
|
+
└── Sales Amount (hidden, used in [Sales] measure)
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
### Dimension Tables
|
|
50
52
|
- Contain descriptive attributes
|
|
51
|
-
-
|
|
53
|
+
- **Singular names** (describes one entity): `Customer`, `Product`, `Date`
|
|
54
|
+
- **No `Dim_` prefix** — users see this name
|
|
55
|
+
- Include surrogate key (integer PK, hidden)
|
|
52
56
|
- Denormalized for simplicity
|
|
53
57
|
- Include business key for lookups
|
|
54
58
|
|
|
55
59
|
```
|
|
56
|
-
|
|
57
|
-
├── ProductKey (PK, surrogate)
|
|
58
|
-
├──
|
|
59
|
-
├──
|
|
60
|
+
Product ← singular, business name, no prefix
|
|
61
|
+
├── ProductKey (PK, surrogate, hidden)
|
|
62
|
+
├── Product ID (business key, visible)
|
|
63
|
+
├── Product Name
|
|
60
64
|
├── Category
|
|
61
65
|
├── Subcategory
|
|
62
66
|
├── Brand
|
|
63
|
-
└──
|
|
67
|
+
└── Unit Cost
|
|
64
68
|
```
|
|
65
69
|
|
|
66
70
|
### Date Dimension
|
|
67
|
-
Essential for time intelligence. Mark as Date Table in Power BI.
|
|
71
|
+
Essential for time intelligence. Mark as Date Table in Power BI. Singular: `Date`, never `Dates` or `Dim_Date`.
|
|
68
72
|
|
|
69
73
|
```
|
|
70
|
-
|
|
71
|
-
├── DateKey (PK, YYYYMMDD integer)
|
|
74
|
+
Date ← singular, no prefix
|
|
75
|
+
├── DateKey (PK, YYYYMMDD integer, hidden)
|
|
72
76
|
├── Date (actual date)
|
|
73
77
|
├── Year
|
|
74
78
|
├── Quarter
|
|
75
79
|
├── Month
|
|
76
|
-
├──
|
|
80
|
+
├── Month Name
|
|
77
81
|
├── Week
|
|
78
|
-
├──
|
|
79
|
-
├──
|
|
80
|
-
├──
|
|
81
|
-
├──
|
|
82
|
-
└──
|
|
82
|
+
├── Day of Week
|
|
83
|
+
├── Day Name
|
|
84
|
+
├── Is Weekend
|
|
85
|
+
├── Is Holiday
|
|
86
|
+
└── Fiscal Year (if different from calendar)
|
|
83
87
|
```
|
|
84
88
|
|
|
85
89
|
## Relationship Best Practices
|
|
@@ -125,55 +129,62 @@ Option 2: Duplicate dimension tables (clearer but redundant)
|
|
|
125
129
|
|
|
126
130
|
## Naming Conventions
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
|
131
|
-
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
|
132
|
+
**Core principle:** user-visible names speak business language, technical elements stay hidden. No `dim`, `fact`, `tbl` prefixes — those leak SQL jargon into the user experience.
|
|
133
|
+
|
|
134
|
+
| Element | Convention | Example | Anti-Pattern |
|
|
135
|
+
|---------|------------|---------|--------------|
|
|
136
|
+
| Fact tables | Plural, business name | `Sales`, `Orders`, `Transactions` | `Fact_Sales`, `fct_sales` |
|
|
137
|
+
| Dimension tables | Singular, business name | `Customer`, `Product`, `Date` | `Dim_Customer`, `dim_customers` |
|
|
138
|
+
| Bridge tables | `Bridge` prefix | `Bridge Customer Region` | `BridgeCustomerRegion` |
|
|
139
|
+
| Foreign keys | `[Entity]Key`, hidden | `ProductKey`, `CustomerKey` | `prod_id`, `FK_Product` |
|
|
140
|
+
| Surrogate PK | `[Entity]Key`, hidden | `CustomerKey` | `ID`, `pk_customer` |
|
|
141
|
+
| Business key | `[Entity] ID` or `Code` | `Customer ID`, `Product Code` | `BusinessKey` |
|
|
142
|
+
| Attribute columns | Natural language | `First Name`, `Order Date` | `FrstNm`, `OrdDt` |
|
|
143
|
+
| Measures | Auto-explicative, no prefix | `Sales`, `Margin %`, `Sales YTD` | `Total Sales`, `Sum of Sales` |
|
|
144
|
+
|
|
145
|
+
See `/governance` for the complete naming guide.
|
|
135
146
|
|
|
136
147
|
## Common Patterns
|
|
137
148
|
|
|
138
149
|
### Many-to-Many with Bridge Table
|
|
139
150
|
```
|
|
140
|
-
|
|
151
|
+
Customer ──(1:*)── Bridge Customer Product ──(*:1)── Product
|
|
141
152
|
```
|
|
142
153
|
|
|
143
154
|
### Slowly Changing Dimensions (SCD)
|
|
144
155
|
```
|
|
145
156
|
Type 1: Overwrite (no history)
|
|
146
157
|
Type 2: Add new row with version tracking
|
|
147
|
-
-
|
|
158
|
+
- Start Date, End Date, Is Current flag
|
|
148
159
|
|
|
149
|
-
|
|
150
|
-
├── CustomerKey (surrogate, unique per version)
|
|
151
|
-
├──
|
|
152
|
-
├──
|
|
160
|
+
Customer (SCD2)
|
|
161
|
+
├── CustomerKey (surrogate, unique per version, hidden)
|
|
162
|
+
├── Customer ID (business key)
|
|
163
|
+
├── Customer Name
|
|
153
164
|
├── Address
|
|
154
|
-
├──
|
|
155
|
-
├──
|
|
156
|
-
└──
|
|
165
|
+
├── Start Date
|
|
166
|
+
├── End Date
|
|
167
|
+
└── Is Current
|
|
157
168
|
```
|
|
158
169
|
|
|
159
170
|
### Junk Dimension (Low-Cardinality Flags)
|
|
160
171
|
Combine multiple low-cardinality attributes:
|
|
161
172
|
```
|
|
162
|
-
|
|
163
|
-
├── OrderFlagKey
|
|
164
|
-
├──
|
|
165
|
-
├──
|
|
166
|
-
├──
|
|
167
|
-
└──
|
|
173
|
+
Order Flags
|
|
174
|
+
├── OrderFlagKey (hidden)
|
|
175
|
+
├── Is Rush
|
|
176
|
+
├── Is Gift
|
|
177
|
+
├── Is Online
|
|
178
|
+
└── Payment Type
|
|
168
179
|
```
|
|
169
180
|
|
|
170
181
|
### Degenerate Dimension
|
|
171
182
|
Attributes stored in fact table (no separate dimension):
|
|
172
183
|
```
|
|
173
|
-
|
|
184
|
+
Sales
|
|
174
185
|
├── ...
|
|
175
|
-
├──
|
|
176
|
-
├──
|
|
186
|
+
├── Invoice Number (degenerate dimension)
|
|
187
|
+
├── Order Number (degenerate dimension)
|
|
177
188
|
└── ...
|
|
178
189
|
```
|
|
179
190
|
|
|
@@ -199,9 +210,12 @@ Fact_Sales
|
|
|
199
210
|
|
|
200
211
|
## Model Validation Checklist
|
|
201
212
|
|
|
213
|
+
- [ ] No technical prefixes (`Dim_`, `Fact_`, `tbl_`)
|
|
214
|
+
- [ ] Dimensions are singular (`Customer`), facts are plural (`Sales`)
|
|
215
|
+
- [ ] Foreign keys hidden from report view
|
|
216
|
+
- [ ] Sumable source columns hidden (only explicit measures visible)
|
|
202
217
|
- [ ] All relationships are single-direction (except where required)
|
|
203
218
|
- [ ] Date table marked as Date Table
|
|
204
|
-
- [ ] Foreign keys hidden from report view
|
|
205
219
|
- [ ] No circular dependencies
|
|
206
220
|
- [ ] Measures in dedicated folder or table
|
|
207
221
|
- [ ] Appropriate data types assigned
|
|
@@ -218,14 +232,26 @@ Fact_Sales
|
|
|
218
232
|
|
|
219
233
|
### Snowflake Complexity
|
|
220
234
|
```
|
|
221
|
-
❌
|
|
222
|
-
✓ Denormalize:
|
|
235
|
+
❌ Product → Category → Department
|
|
236
|
+
✓ Denormalize: Product (includes Category, Department columns)
|
|
223
237
|
```
|
|
224
238
|
|
|
225
239
|
### Calculated Columns for Measures
|
|
226
240
|
```
|
|
227
241
|
❌ Calculated column: [Profit] = [Revenue] - [Cost]
|
|
228
|
-
✓ Measure: Profit = SUM([Revenue]) - SUM([Cost])
|
|
242
|
+
✓ Measure: Profit = SUM(Sales[Revenue]) - SUM(Sales[Cost])
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Technical Prefixes on User-Visible Tables
|
|
246
|
+
```
|
|
247
|
+
❌ Fact_Sales, Dim_Customer (developer jargon leaks to users)
|
|
248
|
+
✓ Sales, Customer (business names)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Visible Sumable Columns
|
|
252
|
+
```
|
|
253
|
+
❌ Quantity column visible — users drag it into visuals as Sum/Average
|
|
254
|
+
✓ Hide Quantity, expose explicit measure: Quantity Sold = SUM(Sales[Quantity])
|
|
229
255
|
```
|
|
230
256
|
|
|
231
257
|
### Missing Date Dimension
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: "data-quality"
|
|
3
3
|
description: "Use when the user asks about Data Quality Skill, especially phrases like \"data quality\", \"check for errors\", \"data profiling\", \"clean data\", \"calidad de datos\"."
|
|
4
|
-
version: "1.
|
|
4
|
+
version: "1.1.1"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
<!-- Generated by BI Agent Superpowers. Edit src/content/skills/data-quality.md instead. -->
|