@spectratools/graphic-designer-cli 0.4.0 → 0.7.0

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/dist/qa.d.ts CHANGED
@@ -1,15 +1,24 @@
1
- import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BUTof436.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-BeFz_nk1.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';
4
4
 
5
5
  type QaSeverity = 'error' | 'warning';
6
6
  type QaIssue = {
7
- code: 'DIMENSIONS_MISMATCH' | 'ELEMENT_CLIPPED' | 'ELEMENT_OVERLAP' | 'LOW_CONTRAST' | 'FOOTER_SPACING' | 'TEXT_TRUNCATED' | 'MISSING_LAYOUT' | 'DRAW_OUT_OF_BOUNDS';
7
+ code: 'DIMENSIONS_MISMATCH' | 'ELEMENT_CLIPPED' | 'ELEMENT_OVERLAP' | 'LOW_CONTRAST' | 'FOOTER_SPACING' | 'TEXT_TRUNCATED' | 'MISSING_LAYOUT' | 'DRAW_OUT_OF_BOUNDS' | 'REFERENCE_MISMATCH';
8
8
  severity: QaSeverity;
9
9
  message: string;
10
10
  elementId?: string;
11
11
  details?: Record<string, number | string | boolean>;
12
12
  };
13
+ type QaReferenceResult = {
14
+ similarity: number;
15
+ verdict: 'match' | 'close' | 'mismatch';
16
+ regions: Array<{
17
+ label: string;
18
+ similarity: number;
19
+ description?: string;
20
+ }>;
21
+ };
13
22
  type QaReport = {
14
23
  pass: boolean;
15
24
  checkedAt: string;
@@ -27,6 +36,7 @@ type QaReport = {
27
36
  footerSpacingPx?: number;
28
37
  };
29
38
  issues: QaIssue[];
39
+ reference?: QaReferenceResult;
30
40
  };
31
41
  /**
32
42
  * Read and parse a sidecar `.meta.json` file produced by
@@ -57,6 +67,7 @@ declare function runQa(options: {
57
67
  imagePath: string;
58
68
  spec: DesignSpec;
59
69
  metadata?: RenderMetadata;
70
+ referencePath?: string;
60
71
  }): Promise<QaReport>;
61
72
 
62
- export { type QaIssue, type QaReport, type QaSeverity, readMetadata, runQa };
73
+ export { type QaIssue, type QaReferenceResult, type QaReport, type QaSeverity, readMetadata, runQa };
package/dist/qa.js CHANGED
@@ -1,7 +1,154 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/compare.ts
12
+ var compare_exports = {};
13
+ __export(compare_exports, {
14
+ compareImages: () => compareImages
15
+ });
16
+ import sharp from "sharp";
17
+ function clampUnit(value) {
18
+ if (value < 0) {
19
+ return 0;
20
+ }
21
+ if (value > 1) {
22
+ return 1;
23
+ }
24
+ return value;
25
+ }
26
+ function toRegionLabel(row, column) {
27
+ const letter = String.fromCharCode(65 + row);
28
+ return `${letter}${column + 1}`;
29
+ }
30
+ function validateGrid(grid) {
31
+ if (!Number.isInteger(grid) || grid <= 0) {
32
+ throw new Error(`Invalid grid value "${grid}". Expected a positive integer.`);
33
+ }
34
+ if (grid > 26) {
35
+ throw new Error(`Invalid grid value "${grid}". Maximum supported grid is 26.`);
36
+ }
37
+ return grid;
38
+ }
39
+ function validateThreshold(threshold) {
40
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
41
+ throw new Error(`Invalid threshold value "${threshold}". Expected a number between 0 and 1.`);
42
+ }
43
+ return threshold;
44
+ }
45
+ async function readDimensions(path) {
46
+ const metadata = await sharp(path).metadata();
47
+ if (!metadata.width || !metadata.height) {
48
+ throw new Error(`Unable to read image dimensions for "${path}".`);
49
+ }
50
+ return {
51
+ width: metadata.width,
52
+ height: metadata.height
53
+ };
54
+ }
55
+ async function normalizeToRaw(path, width, height) {
56
+ const normalized = await sharp(path).rotate().resize(width, height, {
57
+ fit: "contain",
58
+ position: "centre",
59
+ background: { r: 0, g: 0, b: 0, alpha: 0 }
60
+ }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
61
+ return {
62
+ data: normalized.data,
63
+ width: normalized.info.width,
64
+ height: normalized.info.height
65
+ };
66
+ }
67
+ function scorePixelDifference(a, b, offset) {
68
+ const redDiff = Math.abs(a.data[offset] - b.data[offset]);
69
+ const greenDiff = Math.abs(a.data[offset + 1] - b.data[offset + 1]);
70
+ const blueDiff = Math.abs(a.data[offset + 2] - b.data[offset + 2]);
71
+ const alphaDiff = Math.abs(a.data[offset + 3] - b.data[offset + 3]);
72
+ const rgbDelta = (redDiff + greenDiff + blueDiff) / (3 * 255);
73
+ const alphaDelta = alphaDiff / 255;
74
+ return rgbDelta * 0.75 + alphaDelta * 0.25;
75
+ }
76
+ async function compareImages(target, rendered, options = {}) {
77
+ const grid = validateGrid(options.grid ?? DEFAULT_GRID);
78
+ const threshold = validateThreshold(options.threshold ?? DEFAULT_THRESHOLD);
79
+ const closeThreshold = clampUnit(threshold - (options.closeMargin ?? DEFAULT_CLOSE_MARGIN));
80
+ const targetDimensions = await readDimensions(target);
81
+ const renderedDimensions = await readDimensions(rendered);
82
+ const normalizedWidth = Math.max(targetDimensions.width, renderedDimensions.width);
83
+ const normalizedHeight = Math.max(targetDimensions.height, renderedDimensions.height);
84
+ const [targetImage, renderedImage] = await Promise.all([
85
+ normalizeToRaw(target, normalizedWidth, normalizedHeight),
86
+ normalizeToRaw(rendered, normalizedWidth, normalizedHeight)
87
+ ]);
88
+ const regionDiffSums = new Array(grid * grid).fill(0);
89
+ const regionCounts = new Array(grid * grid).fill(0);
90
+ let totalDiff = 0;
91
+ for (let y = 0; y < normalizedHeight; y += 1) {
92
+ const row = Math.min(Math.floor(y * grid / normalizedHeight), grid - 1);
93
+ for (let x = 0; x < normalizedWidth; x += 1) {
94
+ const column = Math.min(Math.floor(x * grid / normalizedWidth), grid - 1);
95
+ const regionIndex = row * grid + column;
96
+ const offset = (y * normalizedWidth + x) * 4;
97
+ const diff = scorePixelDifference(targetImage, renderedImage, offset);
98
+ totalDiff += diff;
99
+ regionDiffSums[regionIndex] += diff;
100
+ regionCounts[regionIndex] += 1;
101
+ }
102
+ }
103
+ const pixelCount = normalizedWidth * normalizedHeight;
104
+ const similarity = clampUnit(1 - totalDiff / pixelCount);
105
+ const regions = [];
106
+ for (let row = 0; row < grid; row += 1) {
107
+ for (let column = 0; column < grid; column += 1) {
108
+ const regionIndex = row * grid + column;
109
+ const regionCount = regionCounts[regionIndex];
110
+ const regionSimilarity = regionCount > 0 ? clampUnit(1 - regionDiffSums[regionIndex] / regionCount) : 1;
111
+ regions.push({
112
+ label: toRegionLabel(row, column),
113
+ row,
114
+ column,
115
+ similarity: regionSimilarity
116
+ });
117
+ }
118
+ }
119
+ const verdict = similarity >= threshold ? "match" : similarity >= closeThreshold ? "close" : "mismatch";
120
+ return {
121
+ targetPath: target,
122
+ renderedPath: rendered,
123
+ targetDimensions,
124
+ renderedDimensions,
125
+ normalizedDimensions: {
126
+ width: normalizedWidth,
127
+ height: normalizedHeight
128
+ },
129
+ dimensionMismatch: targetDimensions.width !== renderedDimensions.width || targetDimensions.height !== renderedDimensions.height,
130
+ grid,
131
+ threshold,
132
+ closeThreshold,
133
+ similarity,
134
+ verdict,
135
+ regions
136
+ };
137
+ }
138
+ var DEFAULT_GRID, DEFAULT_THRESHOLD, DEFAULT_CLOSE_MARGIN;
139
+ var init_compare = __esm({
140
+ "src/compare.ts"() {
141
+ "use strict";
142
+ DEFAULT_GRID = 3;
143
+ DEFAULT_THRESHOLD = 0.8;
144
+ DEFAULT_CLOSE_MARGIN = 0.1;
145
+ }
146
+ });
147
+
1
148
  // src/qa.ts
2
149
  import { readFile } from "fs/promises";
3
150
  import { resolve } from "path";
4
- import sharp from "sharp";
151
+ import sharp2 from "sharp";
5
152
 
6
153
  // src/code-style.ts
7
154
  var CARBON_SURROUND_COLOR = "rgba(171, 184, 195, 1)";
@@ -448,6 +595,15 @@ var drawGradientRectSchema = z2.object({
448
595
  radius: z2.number().min(0).max(256).default(0),
449
596
  opacity: z2.number().min(0).max(1).default(1)
450
597
  }).strict();
598
+ var drawGridSchema = z2.object({
599
+ type: z2.literal("grid"),
600
+ spacing: z2.number().min(5).max(200).default(40),
601
+ color: colorHexSchema2.default("#1E2D4A"),
602
+ width: z2.number().min(0.1).max(4).default(0.5),
603
+ opacity: z2.number().min(0).max(1).default(0.2),
604
+ offsetX: z2.number().default(0),
605
+ offsetY: z2.number().default(0)
606
+ }).strict();
451
607
  var drawCommandSchema = z2.discriminatedUnion("type", [
452
608
  drawRectSchema,
453
609
  drawCircleSchema,
@@ -456,7 +612,8 @@ var drawCommandSchema = z2.discriminatedUnion("type", [
456
612
  drawBezierSchema,
457
613
  drawPathSchema,
458
614
  drawBadgeSchema,
459
- drawGradientRectSchema
615
+ drawGradientRectSchema,
616
+ drawGridSchema
460
617
  ]);
461
618
  var defaultCanvas = {
462
619
  width: 1200,
@@ -520,10 +677,26 @@ var cardElementSchema = z2.object({
520
677
  tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
521
678
  icon: z2.string().min(1).max(64).optional()
522
679
  }).strict();
680
+ var flowNodeShadowSchema = z2.object({
681
+ color: colorHexSchema2.optional(),
682
+ blur: z2.number().min(0).max(64).default(8),
683
+ offsetX: z2.number().min(-32).max(32).default(0),
684
+ offsetY: z2.number().min(-32).max(32).default(0),
685
+ opacity: z2.number().min(0).max(1).default(0.3)
686
+ }).strict();
523
687
  var flowNodeElementSchema = z2.object({
524
688
  type: z2.literal("flow-node"),
525
689
  id: z2.string().min(1).max(120),
526
- shape: z2.enum(["box", "rounded-box", "diamond", "circle", "pill", "cylinder", "parallelogram"]),
690
+ shape: z2.enum([
691
+ "box",
692
+ "rounded-box",
693
+ "diamond",
694
+ "circle",
695
+ "pill",
696
+ "cylinder",
697
+ "parallelogram",
698
+ "hexagon"
699
+ ]).default("rounded-box"),
527
700
  label: z2.string().min(1).max(200),
528
701
  sublabel: z2.string().min(1).max(300).optional(),
529
702
  sublabelColor: colorHexSchema2.optional(),
@@ -543,20 +716,35 @@ var flowNodeElementSchema = z2.object({
543
716
  badgeText: z2.string().min(1).max(32).optional(),
544
717
  badgeColor: colorHexSchema2.optional(),
545
718
  badgeBackground: colorHexSchema2.optional(),
546
- badgePosition: z2.enum(["top", "inside-top"]).default("inside-top")
719
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
720
+ shadow: flowNodeShadowSchema.optional()
547
721
  }).strict();
722
+ var anchorHintSchema = z2.union([
723
+ z2.enum(["top", "bottom", "left", "right", "center"]),
724
+ z2.object({
725
+ x: z2.number().min(-1).max(1),
726
+ y: z2.number().min(-1).max(1)
727
+ }).strict()
728
+ ]);
548
729
  var connectionElementSchema = z2.object({
549
730
  type: z2.literal("connection"),
550
731
  from: z2.string().min(1).max(120),
551
732
  to: z2.string().min(1).max(120),
552
733
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
734
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
553
735
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
554
736
  label: z2.string().min(1).max(200).optional(),
555
737
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
556
738
  color: colorHexSchema2.optional(),
557
- width: z2.number().min(0.5).max(8).optional(),
739
+ width: z2.number().min(0.5).max(10).optional(),
740
+ strokeWidth: z2.number().min(0.5).max(10).default(2),
558
741
  arrowSize: z2.number().min(4).max(32).optional(),
559
- opacity: z2.number().min(0).max(1).default(1)
742
+ arrowPlacement: z2.enum(["endpoint", "boundary"]).default("endpoint"),
743
+ opacity: z2.number().min(0).max(1).default(1),
744
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
745
+ tension: z2.number().min(0.1).max(0.8).default(0.35),
746
+ fromAnchor: anchorHintSchema.optional(),
747
+ toAnchor: anchorHintSchema.optional()
560
748
  }).strict();
561
749
  var codeBlockStyleSchema = z2.object({
562
750
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -625,6 +813,10 @@ var elementSchema = z2.discriminatedUnion("type", [
625
813
  shapeElementSchema,
626
814
  imageElementSchema
627
815
  ]);
816
+ var diagramCenterSchema = z2.object({
817
+ x: z2.number(),
818
+ y: z2.number()
819
+ }).strict();
628
820
  var autoLayoutConfigSchema = z2.object({
629
821
  mode: z2.literal("auto"),
630
822
  algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
@@ -640,7 +832,9 @@ var autoLayoutConfigSchema = z2.object({
640
832
  /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
641
833
  radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
642
834
  /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
643
- radialSortBy: z2.enum(["id", "connections"]).optional()
835
+ radialSortBy: z2.enum(["id", "connections"]).optional(),
836
+ /** Explicit center used by curve/arc connection routing. */
837
+ diagramCenter: diagramCenterSchema.optional()
644
838
  }).strict();
645
839
  var gridLayoutConfigSchema = z2.object({
646
840
  mode: z2.literal("grid"),
@@ -648,13 +842,17 @@ var gridLayoutConfigSchema = z2.object({
648
842
  gap: z2.number().int().min(0).max(256).default(24),
649
843
  cardMinHeight: z2.number().int().min(32).max(4096).optional(),
650
844
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
651
- equalHeight: z2.boolean().default(false)
845
+ equalHeight: z2.boolean().default(false),
846
+ /** Explicit center used by curve/arc connection routing. */
847
+ diagramCenter: diagramCenterSchema.optional()
652
848
  }).strict();
653
849
  var stackLayoutConfigSchema = z2.object({
654
850
  mode: z2.literal("stack"),
655
851
  direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
656
852
  gap: z2.number().int().min(0).max(256).default(24),
657
- alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
853
+ alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
854
+ /** Explicit center used by curve/arc connection routing. */
855
+ diagramCenter: diagramCenterSchema.optional()
658
856
  }).strict();
659
857
  var manualPositionSchema = z2.object({
660
858
  x: z2.number().int(),
@@ -664,7 +862,9 @@ var manualPositionSchema = z2.object({
664
862
  }).strict();
665
863
  var manualLayoutConfigSchema = z2.object({
666
864
  mode: z2.literal("manual"),
667
- positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
865
+ positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
866
+ /** Explicit center used by curve/arc connection routing. */
867
+ diagramCenter: diagramCenterSchema.optional()
668
868
  }).strict();
669
869
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
670
870
  autoLayoutConfigSchema,
@@ -716,6 +916,31 @@ var canvasSchema = z2.object({
716
916
  padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
717
917
  }).strict();
718
918
  var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
919
+ var diagramPositionSchema = z2.object({
920
+ x: z2.number(),
921
+ y: z2.number(),
922
+ width: z2.number().positive(),
923
+ height: z2.number().positive()
924
+ }).strict();
925
+ var diagramElementSchema = z2.discriminatedUnion("type", [
926
+ flowNodeElementSchema,
927
+ connectionElementSchema
928
+ ]);
929
+ var diagramLayoutSchema = z2.object({
930
+ mode: z2.enum(["manual", "auto"]).default("manual"),
931
+ positions: z2.record(z2.string(), diagramPositionSchema).optional(),
932
+ diagramCenter: diagramCenterSchema.optional()
933
+ }).strict();
934
+ var diagramSpecSchema = z2.object({
935
+ version: z2.literal(1),
936
+ canvas: z2.object({
937
+ width: z2.number().int().min(320).max(4096).default(1200),
938
+ height: z2.number().int().min(180).max(4096).default(675)
939
+ }).default({ width: 1200, height: 675 }),
940
+ theme: themeSchema.optional(),
941
+ elements: z2.array(diagramElementSchema).min(1),
942
+ layout: diagramLayoutSchema.default({ mode: "manual" })
943
+ }).strict();
719
944
  var designSpecSchema = z2.object({
720
945
  version: z2.literal(2).default(2),
721
946
  canvas: canvasSchema.default(defaultCanvas),
@@ -789,7 +1014,7 @@ async function runQa(options) {
789
1014
  const imagePath = resolve(options.imagePath);
790
1015
  const expectedSafeFrame = deriveSafeFrame(spec);
791
1016
  const expectedCanvas = canvasRect(spec);
792
- const imageMetadata = await sharp(imagePath).metadata();
1017
+ const imageMetadata = await sharp2(imagePath).metadata();
793
1018
  const issues = [];
794
1019
  const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
795
1020
  const expectedWidth = spec.canvas.width * expectedScale;
@@ -940,6 +1165,31 @@ async function runQa(options) {
940
1165
  });
941
1166
  }
942
1167
  }
1168
+ let referenceResult;
1169
+ if (options.referencePath) {
1170
+ const { compareImages: compareImages2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
1171
+ const comparison = await compareImages2(options.referencePath, imagePath);
1172
+ referenceResult = {
1173
+ similarity: comparison.similarity,
1174
+ verdict: comparison.verdict,
1175
+ regions: comparison.regions.map((region) => ({
1176
+ label: region.label,
1177
+ similarity: region.similarity
1178
+ }))
1179
+ };
1180
+ if (comparison.verdict === "mismatch") {
1181
+ const severity = comparison.similarity < 0.5 ? "error" : "warning";
1182
+ issues.push({
1183
+ code: "REFERENCE_MISMATCH",
1184
+ severity,
1185
+ message: `Reference image comparison ${severity === "error" ? "failed" : "warned"}: similarity ${comparison.similarity.toFixed(4)} with verdict "${comparison.verdict}".`,
1186
+ details: {
1187
+ similarity: comparison.similarity,
1188
+ verdict: comparison.verdict
1189
+ }
1190
+ });
1191
+ }
1192
+ }
943
1193
  const footerSpacingPx = options.metadata?.layout.elements ? (() => {
944
1194
  const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
945
1195
  if (!footer) {
@@ -972,7 +1222,8 @@ async function runQa(options) {
972
1222
  ...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
973
1223
  ...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
974
1224
  },
975
- issues
1225
+ issues,
1226
+ ...referenceResult ? { reference: referenceResult } : {}
976
1227
  };
977
1228
  }
978
1229
  export {
@@ -1,3 +1,3 @@
1
- export { h as DEFAULT_GENERATOR_VERSION, z as LayoutSnapshot, a as Rect, R as RenderMetadata, J as RenderResult, d as RenderedElement, W as WrittenArtifacts, X as computeSpecHash, a9 as inferSidecarPath, ab as renderDesign, ad as writeRenderArtifacts } from './spec.schema-BUTof436.js';
1
+ export { i as DEFAULT_GENERATOR_VERSION, O as LayoutSnapshot, a as Rect, R as RenderMetadata, S as RenderResult, d as RenderedElement, _ as WrittenArtifacts, a1 as computeSpecHash, ak as inferSidecarPath, an as renderDesign, ap as writeRenderArtifacts } from './spec.schema-BeFz_nk1.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';