@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.claude-plugin/skill-manifest.json +1 -1
- package/.plugin/plugin.json +1 -1
- package/CHANGELOG.md +24 -0
- package/README.md +631 -96
- package/bin/cli.js +24 -45
- package/bin/commands/build-desktop.js +60 -6
- package/bin/commands/diff.js +86 -1
- package/bin/commands/watch.js +50 -5
- package/bin/postinstall.js +1 -1
- package/commands/report-design.md +1 -1
- package/desktop-extension/server.js +43 -10
- package/package.json +3 -4
- package/skills/bi-start/SKILL.md +1 -1
- package/skills/bi-start/scripts/update-check.js +1 -1
- package/skills/pbi-connect/SKILL.md +1 -1
- package/skills/pbi-connect/scripts/update-check.js +1 -1
- package/skills/project-kickoff/SKILL.md +1 -1
- package/skills/project-kickoff/scripts/update-check.js +1 -1
- package/skills/report-design/SKILL.md +2 -2
- package/skills/report-design/references/layouts/finance.md +2 -2
- package/skills/report-design/references/native-visuals.md +2 -2
- package/skills/report-design/references/slicer.md +1 -1
- package/skills/report-design/references/textbox.md +1 -1
- package/skills/report-design/scripts/create-visual.js +65 -1
- package/skills/report-design/scripts/update-check.js +1 -1
- package/skills/report-design/scripts/validate-pbir.js +29 -0
- package/src/content/skills/report-design/SKILL.md +1 -1
- package/src/content/skills/report-design/references/layouts/finance.md +2 -2
- package/src/content/skills/report-design/references/native-visuals.md +2 -2
- package/src/content/skills/report-design/references/slicer.md +1 -1
- package/src/content/skills/report-design/references/textbox.md +1 -1
- package/src/content/skills/report-design/scripts/create-visual.js +65 -1
- 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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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' });
|