@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.
Files changed (38) hide show
  1. package/.claude-plugin/marketplace.json +46 -0
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +4 -0
  4. package/AGENTS.md +34 -7
  5. package/README.md +139 -118
  6. package/bin/cli.js +45 -31
  7. package/bin/commands/install.js +321 -0
  8. package/bin/lib/microsoft-mcp.js +8 -0
  9. package/bin/lib/microsoft-mcp.test.js +5 -0
  10. package/package.json +1 -1
  11. package/skills/contributions/SKILL.md +1 -1
  12. package/skills/data-model-design/SKILL.md +1 -1
  13. package/skills/data-modeling/SKILL.md +82 -56
  14. package/skills/data-quality/SKILL.md +1 -1
  15. package/skills/dax/SKILL.md +74 -36
  16. package/skills/dax-doctor/SKILL.md +1 -1
  17. package/skills/dax-udf/SKILL.md +1 -1
  18. package/skills/deployment/SKILL.md +1 -1
  19. package/skills/excel-formulas/SKILL.md +1 -1
  20. package/skills/fabric-scripts/SKILL.md +1 -1
  21. package/skills/fast-standard/SKILL.md +1 -1
  22. package/skills/governance/SKILL.md +103 -50
  23. package/skills/migration-assistant/SKILL.md +1 -1
  24. package/skills/model-documenter/SKILL.md +1 -1
  25. package/skills/pbi-connect/SKILL.md +1 -1
  26. package/skills/power-query/SKILL.md +1 -1
  27. package/skills/project-kickoff/SKILL.md +1 -1
  28. package/skills/query-performance/SKILL.md +1 -1
  29. package/skills/report-design/SKILL.md +1 -1
  30. package/skills/report-layout/SKILL.md +1 -1
  31. package/skills/rls-design/SKILL.md +1 -1
  32. package/skills/semantic-model/SKILL.md +1 -1
  33. package/skills/testing-validation/SKILL.md +1 -1
  34. package/skills/theme-tweaker/SKILL.md +1 -1
  35. package/src/content/mcp-requirements.json +13 -0
  36. package/src/content/skills/data-modeling.md +81 -55
  37. package/src/content/skills/dax.md +73 -35
  38. 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;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@luquimbo/bi-superpowers",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Plugin-first Claude Code toolkit for Power BI, Microsoft Fabric, and Excel workflows powered by official Microsoft MCP servers.",
5
5
  "main": "bin/cli.js",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: "contributions"
3
3
  description: "Contributions Validation Skill: Contribution validation."
4
- version: "1.0.0"
4
+ version: "1.1.1"
5
5
  ---
6
6
 
7
7
  <!-- Generated by BI Agent Superpowers. Edit src/content/skills/contributions.md instead. -->
@@ -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.0.0"
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.0.0"
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
- - Include numeric measures (Amount, Quantity, etc.)
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
- Fact_Sales
39
- ├── SalesID (PK)
40
- ├── DateKey (FK)
41
- ├── ProductKey (FK)
42
- ├── CustomerKey (FK)
43
- ├── StoreKey (FK)
44
- ├── Quantity
45
- ├── UnitPrice
46
- └── TotalAmount
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
- - Include surrogate key (integer PK)
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
- Dim_Product
57
- ├── ProductKey (PK, surrogate)
58
- ├── ProductID (business key)
59
- ├── ProductName
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
- └── UnitCost
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
- Dim_Date
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
- ├── MonthName
80
+ ├── Month Name
77
81
  ├── Week
78
- ├── DayOfWeek
79
- ├── DayName
80
- ├── IsWeekend
81
- ├── IsHoliday
82
- └── FiscalYear (if different from calendar)
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
- | Element | Convention | Example |
129
- |---------|------------|---------|
130
- | Fact tables | `Fact_` prefix | `Fact_Sales`, `Fact_Inventory` |
131
- | Dimension tables | `Dim_` prefix | `Dim_Product`, `Dim_Customer` |
132
- | Bridge tables | `Bridge_` prefix | `Bridge_CustomerProduct` |
133
- | Foreign keys | Match dimension key name | `ProductKey`, `CustomerKey` |
134
- | Measures | Business-friendly names | `Total Sales`, `Avg Order Value` |
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
- Dim_Customer ──(1:*)── Bridge_CustomerProduct ──(*:1)── Dim_Product
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
- - StartDate, EndDate, IsCurrent flag
158
+ - Start Date, End Date, Is Current flag
148
159
 
149
- Dim_Customer_SCD2
150
- ├── CustomerKey (surrogate, unique per version)
151
- ├── CustomerID (business key)
152
- ├── CustomerName
160
+ Customer (SCD2)
161
+ ├── CustomerKey (surrogate, unique per version, hidden)
162
+ ├── Customer ID (business key)
163
+ ├── Customer Name
153
164
  ├── Address
154
- ├── StartDate
155
- ├── EndDate
156
- └── IsCurrent
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
- Dim_OrderFlags
163
- ├── OrderFlagKey
164
- ├── IsRush
165
- ├── IsGift
166
- ├── IsOnline
167
- └── PaymentType
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
- Fact_Sales
184
+ Sales
174
185
  ├── ...
175
- ├── InvoiceNumber (degenerate dimension)
176
- ├── OrderNumber (degenerate dimension)
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
- Dim_ProductDim_CategoryDim_Department
222
- ✓ Denormalize: Dim_Product (includes Category, Department)
235
+ ProductCategoryDepartment
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.0.0"
4
+ version: "1.1.1"
5
5
  ---
6
6
 
7
7
  <!-- Generated by BI Agent Superpowers. Edit src/content/skills/data-quality.md instead. -->