@luquimbo/bi-superpowers 4.1.2 → 4.1.4

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 (44) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/skill-manifest.json +1 -1
  4. package/.plugin/plugin.json +1 -1
  5. package/AGENTS.md +9 -7
  6. package/CHANGELOG.md +40 -0
  7. package/README.md +637 -96
  8. package/bin/cli.js +76 -61
  9. package/bin/commands/build-desktop.js +60 -6
  10. package/bin/commands/diff.js +86 -1
  11. package/bin/commands/mcp-setup.js +26 -3
  12. package/bin/commands/watch.js +50 -5
  13. package/bin/postinstall.js +1 -1
  14. package/bin/utils/mcp-detect.js +1 -1
  15. package/commands/bi-start.md +3 -3
  16. package/commands/pbi-connect.md +60 -24
  17. package/commands/report-design.md +1 -1
  18. package/desktop-extension/server.js +43 -10
  19. package/package.json +3 -4
  20. package/skills/bi-start/SKILL.md +4 -4
  21. package/skills/bi-start/scripts/update-check.js +1 -1
  22. package/skills/pbi-connect/SKILL.md +61 -25
  23. package/skills/pbi-connect/scripts/update-check.js +1 -1
  24. package/skills/project-kickoff/SKILL.md +1 -1
  25. package/skills/project-kickoff/scripts/update-check.js +1 -1
  26. package/skills/report-design/SKILL.md +2 -2
  27. package/skills/report-design/references/layouts/finance.md +2 -2
  28. package/skills/report-design/references/native-visuals.md +2 -2
  29. package/skills/report-design/references/slicer.md +1 -1
  30. package/skills/report-design/references/textbox.md +1 -1
  31. package/skills/report-design/scripts/create-visual.js +65 -1
  32. package/skills/report-design/scripts/update-check.js +1 -1
  33. package/skills/report-design/scripts/validate-pbir.js +29 -0
  34. package/src/content/base.md +1 -1
  35. package/src/content/routing.md +1 -1
  36. package/src/content/skills/bi-start.md +3 -3
  37. package/src/content/skills/pbi-connect.md +60 -24
  38. package/src/content/skills/report-design/SKILL.md +1 -1
  39. package/src/content/skills/report-design/references/layouts/finance.md +2 -2
  40. package/src/content/skills/report-design/references/native-visuals.md +2 -2
  41. package/src/content/skills/report-design/references/slicer.md +1 -1
  42. package/src/content/skills/report-design/references/textbox.md +1 -1
  43. package/src/content/skills/report-design/scripts/create-visual.js +65 -1
  44. package/src/content/skills/report-design/scripts/validate-pbir.js +29 -0
@@ -679,6 +679,66 @@ function nextInt(existing, key) {
679
679
  return max + 1;
680
680
  }
681
681
 
682
+ function validateVisualName(name) {
683
+ if (typeof name !== 'string' || name.trim() === '') {
684
+ fail('--name must be a non-empty visual folder name');
685
+ }
686
+
687
+ if (name !== name.trim()) {
688
+ fail(`--name must not have leading or trailing whitespace (got: ${JSON.stringify(name)})`);
689
+ }
690
+
691
+ if (
692
+ name === '.' ||
693
+ name === '..' ||
694
+ /[\\/]/.test(name) ||
695
+ path.basename(name) !== name ||
696
+ path.win32.basename(name) !== name ||
697
+ path.posix.basename(name) !== name
698
+ ) {
699
+ fail(`--name must be a single visual folder name, not a path (got: ${JSON.stringify(name)})`);
700
+ }
701
+
702
+ if (/[\u0000-\u001f<>:"|?*]/.test(name)) {
703
+ fail(`--name contains characters that are not valid in a Windows folder name (got: ${JSON.stringify(name)})`);
704
+ }
705
+ }
706
+
707
+ function validateNumericOptions(args) {
708
+ const numericOptions = [
709
+ ['x', '--x'],
710
+ ['y', '--y'],
711
+ ['z', '-z'],
712
+ ['width', '--width'],
713
+ ['height', '--height'],
714
+ ['tabOrder', '--tab-order'],
715
+ ];
716
+
717
+ for (const [key, flag] of numericOptions) {
718
+ if (args[key] != null && !Number.isFinite(args[key])) {
719
+ fail(`${flag} must be a finite number`);
720
+ }
721
+ }
722
+
723
+ for (const [key, flag] of [
724
+ ['width', '--width'],
725
+ ['height', '--height'],
726
+ ]) {
727
+ if (args[key] != null && args[key] <= 0) {
728
+ fail(`${flag} must be greater than 0`);
729
+ }
730
+ }
731
+ }
732
+
733
+ function assertPathInside(childPath, parentPath, label) {
734
+ const parent = path.resolve(parentPath);
735
+ const child = path.resolve(childPath);
736
+ const relative = path.relative(parent, child);
737
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
738
+ fail(`${label} resolved outside the expected directory: ${child}`);
739
+ }
740
+ }
741
+
682
742
  // ---------------------------------------------------------------------------
683
743
  // main()
684
744
  // ---------------------------------------------------------------------------
@@ -698,6 +758,7 @@ function main() {
698
758
  if (!args.reportPath) fail('--report-path is required');
699
759
  if (!args.page) fail('--page is required');
700
760
  if (!args.type) fail('--type is required (use --list-types to see options)');
761
+ validateNumericOptions(args);
701
762
 
702
763
  // ---- type validation with friendly errors for known non-native aliases
703
764
  if (KNOWN_NON_NATIVE_TYPES[args.type]) {
@@ -727,8 +788,11 @@ function main() {
727
788
  } while (existing.some((v) => v.name === candidate));
728
789
  name = candidate;
729
790
  }
791
+ validateVisualName(name);
730
792
 
731
- const visualDir = path.join(pageDir, 'visuals', name);
793
+ const visualsDir = path.join(pageDir, 'visuals');
794
+ const visualDir = path.join(visualsDir, name);
795
+ assertPathInside(visualDir, visualsDir, '--name');
732
796
  const visualJsonPath = path.join(visualDir, 'visual.json');
733
797
  if (fs.existsSync(visualJsonPath)) {
734
798
  fail(
@@ -47,7 +47,7 @@ const HTTPS_TIMEOUT_MS = 5000;
47
47
  // Rewritten at generation time when this helper is copied into
48
48
  // `skills/<name>/scripts/update-check.js`. In the canonical source under
49
49
  // `bin/commands/`, it stays null and we fall back to package.json.
50
- const BUNDLED_INSTALLED_VERSION = "4.1.2";
50
+ const BUNDLED_INSTALLED_VERSION = "4.1.4";
51
51
 
52
52
  // ---------------------------------------------------------------------------
53
53
  // Argument parsing
@@ -130,6 +130,33 @@ function boundRoles(visualData) {
130
130
  return bound;
131
131
  }
132
132
 
133
+ function validatePosition(position) {
134
+ const errors = [];
135
+ const requiredFields = ['x', 'y', 'z', 'height', 'width', 'tabOrder'];
136
+
137
+ if (!position || typeof position !== 'object') {
138
+ return requiredFields.map((field) => ({
139
+ severity: 'error',
140
+ rule: 'position-number',
141
+ field,
142
+ message: `position.${field} must be a finite number`,
143
+ }));
144
+ }
145
+
146
+ for (const field of requiredFields) {
147
+ if (!Number.isFinite(position[field])) {
148
+ errors.push({
149
+ severity: 'error',
150
+ rule: 'position-number',
151
+ field,
152
+ message: `position.${field} must be a finite number`,
153
+ });
154
+ }
155
+ }
156
+
157
+ return errors;
158
+ }
159
+
133
160
  function validateVisual(visual) {
134
161
  const errors = [];
135
162
  const { data, path: filePath } = visual;
@@ -148,6 +175,8 @@ function validateVisual(visual) {
148
175
  errors.push({ severity: 'error', rule: 'name', message: 'missing "name"' });
149
176
  }
150
177
 
178
+ errors.push(...validatePosition(data.position));
179
+
151
180
  const vt = data.visual && data.visual.visualType;
152
181
  if (!vt) {
153
182
  errors.push({ severity: 'error', rule: 'visual-type', message: 'missing visualType' });
@@ -142,7 +142,7 @@ AI Assistant → microsoft-learn HTTP MCP → learn.microsoft.com docs
142
142
 
143
143
  ### Practical guidance
144
144
 
145
- - Prefer `.mcp.json` in the plugin root over tool-specific MCP config files.
145
+ - Prefer user-level agent MCP config files written by `super install`. Use project-root `.mcp.json` only when the user explicitly uses an optional local Claude Code plugin.
146
146
  - Never invent or hardcode local ports for the official Modeling MCP flow.
147
147
  - On macOS/Linux, explain that the local Modeling MCP is unavailable and fall back to `microsoft-learn` for docs. Live editing of a local model requires Windows.
148
148
 
@@ -52,7 +52,7 @@ Scan current directory for relevant files:
52
52
  |----------------|-----------------|
53
53
  | `.pbix` / `.pbip` / `.tmdl` files | `/project-kickoff` (if not analyzed) |
54
54
  | `.xlsx` / `.xlsm` files | `/project-kickoff` |
55
- | No plugin `.mcp.json` found when user asks about Power BI | `/pbi-connect` |
55
+ | Agent MCP config missing when user asks about Power BI | `/pbi-connect` |
56
56
 
57
57
  ### 4. CHECK KEYWORDS
58
58
 
@@ -49,7 +49,7 @@ Interpret the single-line output:
49
49
  ```bash
50
50
  super upgrade
51
51
  ```
52
- After it finishes, remind: _"Si instalaste skills en el perfil del agente, corré `super install --yes`. Si además usás un plugin local generado con `super kickoff`, corré `super recharge` dentro de ese repo."_
52
+ After it finishes, remind: _"Corré `super install --all --yes` para refrescar la instalación user-level de todos los agentes. Solo si además mantenés un plugin local de Claude Code generado con `super kickoff`, corré `super recharge` dentro de ese repo."_
53
53
 
54
54
  On `no` — respect it, continue to PHASE 1 silently. The update-state.json already tracks the user's snooze per `update-check.js` semantics.
55
55
 
@@ -80,7 +80,7 @@ Do these detections in order:
80
80
  ```
81
81
  (or equivalent). `$pbiDesktopRunning = true` if present.
82
82
 
83
- 4. **MCP configured**: look for `.mcp.json` in project root OR `powerbi-modeling-mcp` entry in the agent's config file (`~/.claude.json`, `~/.codex/config.toml`, etc). Keep the check shallow — no need to deep-diff.
83
+ 4. **MCP configured**: look first for `powerbi-modeling-mcp` in the agent's user-level config file (`~/.claude.json`, `~/.codex/config.toml`, etc). Only check project-root `.mcp.json` when the user explicitly says they use a local Claude Code plugin. Keep the check shallow — no need to deep-diff.
84
84
 
85
85
  ### Emit the context in 3-4 lines max
86
86
 
@@ -174,7 +174,7 @@ Stop. Don't hover. The user will tell you what they want next.
174
174
  - **Project analysis or setup**: that's `/project-kickoff`. If the user says "analizar mi proyecto", "armar el modelo base", "arrancar uno nuevo desde cero", delegate.
175
175
  - **MCP wiring details**: that's `/pbi-connect`. bi-start just offers to dispatch it; the actual configuration work is in that skill.
176
176
  - **Report authoring**: that's `/report-design`. Same pattern.
177
- - **Running the update**: bi-start offers + dispatches `super upgrade`; the actual refresh path after eso (`/plugin update bi-superpowers`, `super install --yes`, o `super recharge`) is owned by `/bin/cli.js`.
177
+ - **Running the update**: bi-start offers + dispatches `super upgrade`; the actual refresh path after eso (`/plugin update bi-superpowers`, `super install --all --yes`, o `super recharge` only for local Claude Code plugins) is owned by `/bin/cli.js`.
178
178
 
179
179
  ## Related Skills
180
180
 
@@ -9,10 +9,10 @@ Activate this skill when user mentions:
9
9
  - "can't connect to Power BI", "connection error", "MCP not working"
10
10
 
11
11
  ## Identity
12
- You are a **Power BI MCP Connection Specialist**. Your job is to help the user connect their AI agent to Power BI Desktop using the official Microsoft MCP servers shipped with bi-superpowers, with a plugin-first workflow.
12
+ You are a **Power BI MCP Connection Specialist**. Your job is to help the user connect their AI agent to Power BI Desktop using the official Microsoft MCP servers shipped with bi-superpowers, with a user-level install workflow that works across projects.
13
13
 
14
14
  ## MANDATORY RULES
15
- 1. **PLUGIN-FIRST.** Prefer `.mcp.json` in the Claude Code plugin root.
15
+ 1. **USER-LEVEL FIRST.** Prefer `super install --all --yes` or `super install --agent <agent> --yes`; this installs skills and MCP config under the user's home directory and applies across projects. `.mcp.json` is only for an optional repo-local Claude Code plugin.
16
16
  2. **OFFICIAL SERVERS ONLY.** Use `powerbi-modeling-mcp` (local) and `microsoft-learn` (HTTP). Do not invent or recommend unofficial MCPs.
17
17
  3. **WINDOWS LIMITATION.** Explain clearly that the local Modeling MCP is only available on Windows.
18
18
  4. **NO PORT INVENTION.** Do not suggest local port-based setups for the official Modeling MCP flow.
@@ -33,7 +33,8 @@ I'll help you connect your AI agent using the official Microsoft MCP servers.
33
33
  What do you need?
34
34
 
35
35
  1. Connect to Power BI Desktop on this machine (Windows)
36
- 2. Verify that my plugin `.mcp.json` is configured correctly
36
+ 2. Verify that my agent MCP config is installed correctly
37
+ 3. Verify an optional local Claude Code plugin `.mcp.json`
37
38
  ```
38
39
 
39
40
  ---
@@ -56,11 +57,21 @@ Before we continue:
56
57
 
57
58
  ### If the user is on Windows and installed the extension
58
59
 
59
- Guide them to:
60
+ Guide them to the user-level install:
60
61
 
61
- 1. Run `bi-superpowers mcp-setup`
62
- 2. Confirm `.mcp.json` contains `powerbi-modeling-mcp`
63
- 3. Restart or refresh Claude Code
62
+ 1. Run `super install --all --yes`, or for one agent run `super install --agent codex --yes` / `super install --agent claude-code --yes` / etc.
63
+ 2. Confirm the agent config contains `powerbi-modeling-mcp` and `microsoft-learn`.
64
+ 3. Restart or refresh the AI agent so it reloads skills and MCP servers.
65
+
66
+ Use these config locations:
67
+
68
+ | Agent | Skill path | MCP config |
69
+ | --- | --- | --- |
70
+ | Claude Code | `~/.claude/skills` or `~/.agents/skills` | `~/.claude.json` |
71
+ | GitHub Copilot | `~/.copilot/skills` | `~/.copilot/mcp-config.json` |
72
+ | Codex | `~/.agents/skills` | `~/.codex/config.toml` |
73
+ | Gemini CLI | `~/.gemini/skills` | `~/.gemini/settings.json` |
74
+ | Kilo Code | `~/.kilo/skills` | `~/.kilo/mcp_settings.json` |
64
75
 
65
76
  Use this explanation:
66
77
 
@@ -72,18 +83,13 @@ and starts it with `--start`.
72
83
 
73
84
  If the user wants a config example, show:
74
85
 
75
- ```json
76
- {
77
- "powerbi-modeling-mcp": {
78
- "type": "stdio",
79
- "command": "node",
80
- "args": ["${CLAUDE_PLUGIN_ROOT}/bin/mcp/powerbi-modeling-launcher.js"]
81
- },
82
- "microsoft-learn": {
83
- "type": "http",
84
- "url": "https://learn.microsoft.com/api/mcp"
85
- }
86
- }
86
+ ```toml
87
+ [mcp_servers.powerbi-modeling-mcp]
88
+ command = "node"
89
+ args = ["<package-dir>/bin/mcp/powerbi-modeling-launcher.js"]
90
+
91
+ [mcp_servers.microsoft-learn]
92
+ url = "https://learn.microsoft.com/api/mcp"
87
93
  ```
88
94
 
89
95
  ### If the user installed the executable manually
@@ -97,9 +103,11 @@ BI_SUPERPOWERS_POWERBI_MODELING_MCP_PATH
97
103
  Then re-run:
98
104
 
99
105
  ```bash
100
- super mcp-setup
106
+ super install --all --yes
101
107
  ```
102
108
 
109
+ If they are intentionally maintaining a repo-local Claude Code plugin, they can run `super mcp-setup` inside that plugin project instead.
110
+
103
111
  ### If the user is on macOS or Linux
104
112
 
105
113
  Say:
@@ -114,15 +122,41 @@ For live editing of a local semantic model, you need a Windows environment.
114
122
 
115
123
  ---
116
124
 
117
- ## PHASE 2: Verify Plugin Config
125
+ ## PHASE 2: Verify Agent MCP Config
118
126
 
119
127
  If the user chooses option 2:
120
128
 
129
+ Check the config for the agent they use:
130
+
131
+ - Claude Code: `~/.claude.json`
132
+ - GitHub Copilot: `~/.copilot/mcp-config.json`
133
+ - Codex: `~/.codex/config.toml`
134
+ - Gemini CLI: `~/.gemini/settings.json`
135
+ - Kilo Code: `~/.kilo/mcp_settings.json`
136
+
137
+ Confirm:
138
+
139
+ - skills are installed under the agent's user-level skill directory
140
+ - config includes `powerbi-modeling-mcp`
141
+ - config includes `microsoft-learn`
142
+
143
+ If anything is missing, recommend:
144
+
145
+ ```bash
146
+ super install --agent <agent-id> --yes
147
+ ```
148
+
149
+ Then restart or refresh the agent.
150
+
151
+ ## PHASE 3: Verify Optional Local Claude Code Plugin Config
152
+
153
+ If the user chooses option 3, or explicitly says they use `super kickoff` / `claude --plugin-dir`:
154
+
121
155
  Check these files in order:
122
156
 
123
157
  1. `.claude-plugin/plugin.json`
124
158
  2. `.mcp.json`
125
- 3. `.bi-superpowers.json` if present
159
+ 3. `.bi-superpowers.json`
126
160
 
127
161
  Confirm:
128
162
 
@@ -151,7 +185,8 @@ claude --plugin-dir .
151
185
  | --- | --- |
152
186
  | Modeling MCP missing on Windows | Install the Microsoft extension in VS Code or Cursor |
153
187
  | Modeling MCP installed manually | Set `BI_SUPERPOWERS_POWERBI_MODELING_MCP_PATH` |
154
- | Plugin not loading MCPs | Re-run `bi-superpowers mcp-setup` and restart Claude Code |
188
+ | Agent not loading MCPs | Re-run `super install --agent <agent-id> --yes` and restart the agent |
189
+ | Local Claude Code plugin not loading MCPs | Re-run `super mcp-setup` inside the plugin project and restart Claude Code |
155
190
  | macOS/Linux local modeling request | Use `microsoft-learn` for docs; live editing requires Windows |
156
191
  | User asks about Excel MCP | Explain Excel remains supported through skills and library content, not a default MCP |
157
192
 
@@ -163,7 +198,8 @@ claude --plugin-dir .
163
198
  | --- | --- | --- |
164
199
  | Recommend `uvx` for Modeling MCP | Not the official Microsoft installation path | Use the official executable via the local launcher |
165
200
  | Ask the user to find a localhost port | Not required in the new flow | Use the official Modeling MCP launcher |
166
- | Put plugin MCP config in `.claude/settings.json` first | Plugin-first flow uses `.mcp.json` | Prefer `.mcp.json` at the plugin root |
201
+ | Run `super kickoff` for Codex/GitHub Copilot/Gemini/Kilo setup | `kickoff` creates repo-local Claude Code plugin files | Use `super install --agent <agent-id> --yes` |
202
+ | Treat `.mcp.json` as the default install target | It is only for optional local Claude Code plugins | Use the agent's user-level MCP config |
167
203
  | Invent unofficial MCPs (remote, fabric, etc.) | This plugin only ships 2 official MCPs | Only use the 2 official MCPs we ship (`powerbi-modeling-mcp` and `microsoft-learn`) |
168
204
 
169
205
  ---
@@ -266,7 +266,7 @@ Do NOT hand-author `report.json` theme metadata. The canonical PBIR shape (for r
266
266
  - `resourcePackages` includes a `{name: "RegisteredResources", type: "RegisteredResources"}` package whose `items[]` has `{name: "<file>.json", path: "<file>.json", type: "CustomTheme"}`
267
267
  - the theme file itself lives at `<reportPath>/StaticResources/RegisteredResources/<file>.json`
268
268
 
269
- **Conditional formatting (variance KPI green/red, diverging matrix gradients, signed-color bars) is DEFERRED to v4.1.** The layouts mention conditional color in design notes; in v4.0.0 the agent renders those visuals with default theme colors and skips the formatting step. Do not call `pbi format` from this skill in v4.0.0 — the `pbi format` syntax is not yet documented or tested for our use cases. Better a plain report that renders than a fancy one that breaks.
269
+ **Conditional formatting note.** Layout notes may describe variance colors, diverging matrix gradients, or signed-color bars as design intent. The current `report-design` flow does not author those formatting rules. Render those visuals with theme/default colors unless you have a tested PBIR formatting implementation. Do not invent `pbi format` calls from this skill.
270
270
 
271
271
  ### 4.4 Validate (two layers)
272
272
 
@@ -28,7 +28,7 @@ Roles used below (to be resolved via the MCP):
28
28
  | Scenario slicer | slicer (manual JSON — see `references/slicer.md`) | 848, 312, 336, 80 | `Scenario[Scenario]` |
29
29
 
30
30
  **Design notes:**
31
- - Variance KPI: conditional formatting (green positive, red negative) is deferred to v4.1 render as plain card for v4.0.0.
31
+ - Variance KPI: green/red conditional coloring is design intent only. Render as a plain card with theme/default colors unless a tested PBIR formatting implementation is available.
32
32
  - Year slicer default: current year (single-select).
33
33
 
34
34
  ---
@@ -61,5 +61,5 @@ Roles used below (to be resolved via the MCP):
61
61
  | Account × Month matrix | matrix | 16, 512, 1168, 144 | rows: `account-dim-column`, cols: `'Date'[Month]`, values: `variance-measure` with diverging gradient |
62
62
 
63
63
  **Design notes:**
64
- - Variance-sign coloring (diverging palette) is deferred to v4.1 render bars/matrix with default theme colors for v4.0.0.
64
+ - Variance-sign coloring (diverging palette) is design intent only. Render bars/matrix with theme/default colors unless a tested PBIR formatting implementation is available.
65
65
  - The bottom matrix is short (144px) so it acts as a "heatmap strip" rather than a scrollable table.
@@ -311,7 +311,7 @@ Si PBI Desktop introduce un visualType nuevo (o encontrás uno nativo que falta
311
311
 
312
312
  ## Features de `visual.json` que `create-visual.js` NO soporta todavía
313
313
 
314
- Descubiertos al regenerar la smoke-test "Galería de Visuales" end-to-end con el script (backlog item 2, cycle v4.1). No bloquean el authoring — la shape que emite el script pasa `pbi report validate` y `validate-pbir.js` — pero sí dejan regresiones visuales si el `visual.json` hand-written aprovechaba alguno de estos features:
314
+ Descubiertos al regenerar la smoke-test "Galería de Visuales" end-to-end con el script. No bloquean el authoring — la shape que emite el script pasa `pbi report validate` y `validate-pbir.js` — pero sí dejan regresiones visuales si el `visual.json` hand-written aprovechaba alguno de estos features:
315
315
 
316
316
  1. **`query.sortDefinition`** — sort explícito por medida/columna con dirección. `create-visual.js` no lo emite, así que un visual "ranking" tipo barChart ordenado descendente por una medida pierde el orden en la regeneración. **Workaround**: ordenar en Desktop manualmente después del `.pbip` abrir, o hand-extender el `visual.json` con `sortDefinition` post-creación. **Fix futuro**: `--sort-by "Role:Field" --sort-dir descending` en `create-visual.js`.
317
317
 
@@ -319,7 +319,7 @@ Descubiertos al regenerar la smoke-test "Galería de Visuales" end-to-end con el
319
319
 
320
320
  3. **`queryState.<Role>.projections[].active`** — las columnas siempre salen con `active: true`, las medidas nunca. Si un visual hand-written tenía `active: true` en una medida (para indicar que la medida es la "default Y") o `active: false` en una columna (hidden projection), el script normaliza. Rara vez significativo para el render pero cambia el shape JSON.
321
321
 
322
- 4. **Formato condicional**: colores signados (positivo verde / negativo rojo), gradientes en matriz, data bars en tabla. Ninguno de estos expresa shape via `create-visual.js`. Quedó deferido a v4.1+ en `SKILL.md` PHASE 4.3.
322
+ 4. **Formato condicional**: colores signados (positivo verde / negativo rojo), gradientes en matriz, data bars en tabla. Ninguno de estos expresa shape via `create-visual.js`; tratarlos como diseño previsto hasta que exista una implementación PBIR testeada.
323
323
 
324
324
  Cualquier cambio en el script que cubra estos gaps debe: (a) agregar el flag a `parseArgs`, (b) cubrirlo con tests en `create-visual.test.js`, (c) documentarlo en este archivo, (d) regenerar la Galería para que ejercite el feature.
325
325
 
@@ -86,4 +86,4 @@ cat > "<reportPath>/definition/pages/<pageName>/visuals/<visualName>/visual.json
86
86
  EOF
87
87
  ```
88
88
 
89
- A reusable helper script (`scripts/write-slicer.sh`) is a v4.1 candidate; v4.0.0 ships with the heredoc pattern.
89
+ Prefer `scripts/create-visual.js --type slicer` for new slicers. Keep the heredoc pattern above only as a fallback when the helper script is unavailable.
@@ -98,4 +98,4 @@ cat > "<reportPath>/definition/pages/<pageName>/visuals/<visualName>/visual.json
98
98
  EOF
99
99
  ```
100
100
 
101
- A reusable helper script (`scripts/write-textbox.sh`) is a v4.1 candidate; v4.0.0 ships with the heredoc pattern.
101
+ Prefer `scripts/create-visual.js --type textbox` for new textboxes. Keep the heredoc pattern above only as a fallback when the helper script is unavailable.
@@ -679,6 +679,66 @@ function nextInt(existing, key) {
679
679
  return max + 1;
680
680
  }
681
681
 
682
+ function validateVisualName(name) {
683
+ if (typeof name !== 'string' || name.trim() === '') {
684
+ fail('--name must be a non-empty visual folder name');
685
+ }
686
+
687
+ if (name !== name.trim()) {
688
+ fail(`--name must not have leading or trailing whitespace (got: ${JSON.stringify(name)})`);
689
+ }
690
+
691
+ if (
692
+ name === '.' ||
693
+ name === '..' ||
694
+ /[\\/]/.test(name) ||
695
+ path.basename(name) !== name ||
696
+ path.win32.basename(name) !== name ||
697
+ path.posix.basename(name) !== name
698
+ ) {
699
+ fail(`--name must be a single visual folder name, not a path (got: ${JSON.stringify(name)})`);
700
+ }
701
+
702
+ if (/[\u0000-\u001f<>:"|?*]/.test(name)) {
703
+ fail(`--name contains characters that are not valid in a Windows folder name (got: ${JSON.stringify(name)})`);
704
+ }
705
+ }
706
+
707
+ function validateNumericOptions(args) {
708
+ const numericOptions = [
709
+ ['x', '--x'],
710
+ ['y', '--y'],
711
+ ['z', '-z'],
712
+ ['width', '--width'],
713
+ ['height', '--height'],
714
+ ['tabOrder', '--tab-order'],
715
+ ];
716
+
717
+ for (const [key, flag] of numericOptions) {
718
+ if (args[key] != null && !Number.isFinite(args[key])) {
719
+ fail(`${flag} must be a finite number`);
720
+ }
721
+ }
722
+
723
+ for (const [key, flag] of [
724
+ ['width', '--width'],
725
+ ['height', '--height'],
726
+ ]) {
727
+ if (args[key] != null && args[key] <= 0) {
728
+ fail(`${flag} must be greater than 0`);
729
+ }
730
+ }
731
+ }
732
+
733
+ function assertPathInside(childPath, parentPath, label) {
734
+ const parent = path.resolve(parentPath);
735
+ const child = path.resolve(childPath);
736
+ const relative = path.relative(parent, child);
737
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
738
+ fail(`${label} resolved outside the expected directory: ${child}`);
739
+ }
740
+ }
741
+
682
742
  // ---------------------------------------------------------------------------
683
743
  // main()
684
744
  // ---------------------------------------------------------------------------
@@ -698,6 +758,7 @@ function main() {
698
758
  if (!args.reportPath) fail('--report-path is required');
699
759
  if (!args.page) fail('--page is required');
700
760
  if (!args.type) fail('--type is required (use --list-types to see options)');
761
+ validateNumericOptions(args);
701
762
 
702
763
  // ---- type validation with friendly errors for known non-native aliases
703
764
  if (KNOWN_NON_NATIVE_TYPES[args.type]) {
@@ -727,8 +788,11 @@ function main() {
727
788
  } while (existing.some((v) => v.name === candidate));
728
789
  name = candidate;
729
790
  }
791
+ validateVisualName(name);
730
792
 
731
- const visualDir = path.join(pageDir, 'visuals', name);
793
+ const visualsDir = path.join(pageDir, 'visuals');
794
+ const visualDir = path.join(visualsDir, name);
795
+ assertPathInside(visualDir, visualsDir, '--name');
732
796
  const visualJsonPath = path.join(visualDir, 'visual.json');
733
797
  if (fs.existsSync(visualJsonPath)) {
734
798
  fail(
@@ -130,6 +130,33 @@ function boundRoles(visualData) {
130
130
  return bound;
131
131
  }
132
132
 
133
+ function validatePosition(position) {
134
+ const errors = [];
135
+ const requiredFields = ['x', 'y', 'z', 'height', 'width', 'tabOrder'];
136
+
137
+ if (!position || typeof position !== 'object') {
138
+ return requiredFields.map((field) => ({
139
+ severity: 'error',
140
+ rule: 'position-number',
141
+ field,
142
+ message: `position.${field} must be a finite number`,
143
+ }));
144
+ }
145
+
146
+ for (const field of requiredFields) {
147
+ if (!Number.isFinite(position[field])) {
148
+ errors.push({
149
+ severity: 'error',
150
+ rule: 'position-number',
151
+ field,
152
+ message: `position.${field} must be a finite number`,
153
+ });
154
+ }
155
+ }
156
+
157
+ return errors;
158
+ }
159
+
133
160
  function validateVisual(visual) {
134
161
  const errors = [];
135
162
  const { data, path: filePath } = visual;
@@ -148,6 +175,8 @@ function validateVisual(visual) {
148
175
  errors.push({ severity: 'error', rule: 'name', message: 'missing "name"' });
149
176
  }
150
177
 
178
+ errors.push(...validatePosition(data.position));
179
+
151
180
  const vt = data.visual && data.visual.visualType;
152
181
  if (!vt) {
153
182
  errors.push({ severity: 'error', rule: 'visual-type', message: 'missing visualType' });