@luquimbo/bi-superpowers 4.1.2 → 4.1.3

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 (35) 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/CHANGELOG.md +24 -0
  6. package/README.md +631 -96
  7. package/bin/cli.js +24 -45
  8. package/bin/commands/build-desktop.js +60 -6
  9. package/bin/commands/diff.js +86 -1
  10. package/bin/commands/watch.js +50 -5
  11. package/bin/postinstall.js +1 -1
  12. package/commands/report-design.md +1 -1
  13. package/desktop-extension/server.js +43 -10
  14. package/package.json +3 -4
  15. package/skills/bi-start/SKILL.md +1 -1
  16. package/skills/bi-start/scripts/update-check.js +1 -1
  17. package/skills/pbi-connect/SKILL.md +1 -1
  18. package/skills/pbi-connect/scripts/update-check.js +1 -1
  19. package/skills/project-kickoff/SKILL.md +1 -1
  20. package/skills/project-kickoff/scripts/update-check.js +1 -1
  21. package/skills/report-design/SKILL.md +2 -2
  22. package/skills/report-design/references/layouts/finance.md +2 -2
  23. package/skills/report-design/references/native-visuals.md +2 -2
  24. package/skills/report-design/references/slicer.md +1 -1
  25. package/skills/report-design/references/textbox.md +1 -1
  26. package/skills/report-design/scripts/create-visual.js +65 -1
  27. package/skills/report-design/scripts/update-check.js +1 -1
  28. package/skills/report-design/scripts/validate-pbir.js +29 -0
  29. package/src/content/skills/report-design/SKILL.md +1 -1
  30. package/src/content/skills/report-design/references/layouts/finance.md +2 -2
  31. package/src/content/skills/report-design/references/native-visuals.md +2 -2
  32. package/src/content/skills/report-design/references/slicer.md +1 -1
  33. package/src/content/skills/report-design/references/textbox.md +1 -1
  34. package/src/content/skills/report-design/scripts/create-visual.js +65 -1
  35. package/src/content/skills/report-design/scripts/validate-pbir.js +29 -0
@@ -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(
@@ -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.3";
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' });
@@ -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' });