@spectratools/graphic-designer-cli 0.3.2 → 0.6.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-DhAI-tE8.js';
1
+ import { R as RenderMetadata, D as DesignSpec } from './spec.schema-Dm_wOLTd.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)";
@@ -62,7 +209,98 @@ import { z as z2 } from "zod";
62
209
 
63
210
  // src/themes/builtin.ts
64
211
  import { z } from "zod";
65
- var colorHexSchema = z.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
212
+
213
+ // src/utils/color.ts
214
+ function parseChannel(hex, offset) {
215
+ return Number.parseInt(hex.slice(offset, offset + 2), 16);
216
+ }
217
+ function parseHexColor(hexColor) {
218
+ const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
219
+ if (normalized.length !== 6 && normalized.length !== 8) {
220
+ throw new Error(`Unsupported color format: ${hexColor}`);
221
+ }
222
+ return {
223
+ r: parseChannel(normalized, 0),
224
+ g: parseChannel(normalized, 2),
225
+ b: parseChannel(normalized, 4)
226
+ };
227
+ }
228
+ var rgbaRegex = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*([01](?:\.\d+)?|0?\.\d+)\s*)?\)$/;
229
+ var hexColorRegex = /^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
230
+ function toHex(n) {
231
+ return n.toString(16).padStart(2, "0");
232
+ }
233
+ function parseRgbaToHex(color) {
234
+ const match = rgbaRegex.exec(color);
235
+ if (!match) {
236
+ throw new Error(`Invalid rgb/rgba color: ${color}`);
237
+ }
238
+ const r = Number.parseInt(match[1], 10);
239
+ const g = Number.parseInt(match[2], 10);
240
+ const b = Number.parseInt(match[3], 10);
241
+ if (r > 255 || g > 255 || b > 255) {
242
+ throw new Error(`RGB channel values must be 0-255, got: ${color}`);
243
+ }
244
+ if (match[4] !== void 0) {
245
+ const a = Number.parseFloat(match[4]);
246
+ if (a < 0 || a > 1) {
247
+ throw new Error(`Alpha value must be 0-1, got: ${a}`);
248
+ }
249
+ const alphaByte = Math.round(a * 255);
250
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}${toHex(alphaByte)}`;
251
+ }
252
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
253
+ }
254
+ function isRgbaColor(color) {
255
+ return rgbaRegex.test(color);
256
+ }
257
+ function isHexColor(color) {
258
+ return hexColorRegex.test(color);
259
+ }
260
+ function normalizeColor(color) {
261
+ if (isHexColor(color)) {
262
+ return color;
263
+ }
264
+ if (isRgbaColor(color)) {
265
+ return parseRgbaToHex(color);
266
+ }
267
+ throw new Error(`Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color, got: ${color}`);
268
+ }
269
+ function srgbToLinear(channel) {
270
+ const normalized = channel / 255;
271
+ if (normalized <= 0.03928) {
272
+ return normalized / 12.92;
273
+ }
274
+ return ((normalized + 0.055) / 1.055) ** 2.4;
275
+ }
276
+ function relativeLuminance(hexColor) {
277
+ const normalized = isRgbaColor(hexColor) ? parseRgbaToHex(hexColor) : hexColor;
278
+ const rgb = parseHexColor(normalized);
279
+ const r = srgbToLinear(rgb.r);
280
+ const g = srgbToLinear(rgb.g);
281
+ const b = srgbToLinear(rgb.b);
282
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
283
+ }
284
+ function contrastRatio(foreground, background) {
285
+ const fg = relativeLuminance(foreground);
286
+ const bg = relativeLuminance(background);
287
+ const lighter = Math.max(fg, bg);
288
+ const darker = Math.min(fg, bg);
289
+ return (lighter + 0.05) / (darker + 0.05);
290
+ }
291
+
292
+ // src/themes/builtin.ts
293
+ var colorHexSchema = z.string().refine(
294
+ (v) => {
295
+ try {
296
+ normalizeColor(v);
297
+ return true;
298
+ } catch {
299
+ return false;
300
+ }
301
+ },
302
+ { message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
303
+ ).transform((v) => normalizeColor(v));
66
304
  var fontFamilySchema = z.string().min(1).max(120);
67
305
  var codeThemeSchema = z.object({
68
306
  background: colorHexSchema,
@@ -235,7 +473,17 @@ var builtInThemes = {
235
473
  var defaultTheme = builtInThemes.dark;
236
474
 
237
475
  // src/spec.schema.ts
238
- var colorHexSchema2 = z2.string().regex(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, "Expected #RRGGBB or #RRGGBBAA color");
476
+ var colorHexSchema2 = z2.string().refine(
477
+ (v) => {
478
+ try {
479
+ normalizeColor(v);
480
+ return true;
481
+ } catch {
482
+ return false;
483
+ }
484
+ },
485
+ { message: "Expected #RRGGBB, #RRGGBBAA, rgb(), or rgba() color" }
486
+ ).transform((v) => normalizeColor(v));
239
487
  var gradientStopSchema = z2.object({
240
488
  offset: z2.number().min(0).max(1),
241
489
  color: colorHexSchema2
@@ -419,13 +667,32 @@ var cardElementSchema = z2.object({
419
667
  tone: z2.enum(["neutral", "accent", "success", "warning", "error"]).default("neutral"),
420
668
  icon: z2.string().min(1).max(64).optional()
421
669
  }).strict();
670
+ var flowNodeShadowSchema = z2.object({
671
+ color: colorHexSchema2.optional(),
672
+ blur: z2.number().min(0).max(64).default(8),
673
+ offsetX: z2.number().min(-32).max(32).default(0),
674
+ offsetY: z2.number().min(-32).max(32).default(0),
675
+ opacity: z2.number().min(0).max(1).default(0.3)
676
+ }).strict();
422
677
  var flowNodeElementSchema = z2.object({
423
678
  type: z2.literal("flow-node"),
424
679
  id: z2.string().min(1).max(120),
425
- shape: z2.enum(["box", "rounded-box", "diamond", "circle", "pill", "cylinder", "parallelogram"]),
680
+ shape: z2.enum([
681
+ "box",
682
+ "rounded-box",
683
+ "diamond",
684
+ "circle",
685
+ "pill",
686
+ "cylinder",
687
+ "parallelogram",
688
+ "hexagon"
689
+ ]).default("rounded-box"),
426
690
  label: z2.string().min(1).max(200),
427
691
  sublabel: z2.string().min(1).max(300).optional(),
428
692
  sublabelColor: colorHexSchema2.optional(),
693
+ sublabel2: z2.string().min(1).max(300).optional(),
694
+ sublabel2Color: colorHexSchema2.optional(),
695
+ sublabel2FontSize: z2.number().min(8).max(32).optional(),
429
696
  labelColor: colorHexSchema2.optional(),
430
697
  labelFontSize: z2.number().min(10).max(48).optional(),
431
698
  color: colorHexSchema2.optional(),
@@ -434,20 +701,30 @@ var flowNodeElementSchema = z2.object({
434
701
  cornerRadius: z2.number().min(0).max(64).optional(),
435
702
  width: z2.number().int().min(40).max(800).optional(),
436
703
  height: z2.number().int().min(30).max(600).optional(),
437
- opacity: z2.number().min(0).max(1).default(1)
704
+ fillOpacity: z2.number().min(0).max(1).default(1),
705
+ opacity: z2.number().min(0).max(1).default(1),
706
+ badgeText: z2.string().min(1).max(32).optional(),
707
+ badgeColor: colorHexSchema2.optional(),
708
+ badgeBackground: colorHexSchema2.optional(),
709
+ badgePosition: z2.enum(["top", "inside-top"]).default("inside-top"),
710
+ shadow: flowNodeShadowSchema.optional()
438
711
  }).strict();
439
712
  var connectionElementSchema = z2.object({
440
713
  type: z2.literal("connection"),
441
714
  from: z2.string().min(1).max(120),
442
715
  to: z2.string().min(1).max(120),
443
716
  style: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
717
+ strokeStyle: z2.enum(["solid", "dashed", "dotted"]).default("solid"),
444
718
  arrow: z2.enum(["end", "start", "both", "none"]).default("end"),
445
719
  label: z2.string().min(1).max(200).optional(),
446
720
  labelPosition: z2.enum(["start", "middle", "end"]).default("middle"),
447
721
  color: colorHexSchema2.optional(),
448
- width: z2.number().min(0.5).max(8).optional(),
722
+ width: z2.number().min(0.5).max(10).optional(),
723
+ strokeWidth: z2.number().min(0.5).max(10).default(2),
449
724
  arrowSize: z2.number().min(4).max(32).optional(),
450
- opacity: z2.number().min(0).max(1).default(1)
725
+ opacity: z2.number().min(0).max(1).default(1),
726
+ routing: z2.enum(["auto", "orthogonal", "curve", "arc"]).default("auto"),
727
+ tension: z2.number().min(0.1).max(0.8).default(0.35)
451
728
  }).strict();
452
729
  var codeBlockStyleSchema = z2.object({
453
730
  paddingVertical: z2.number().min(0).max(128).default(56),
@@ -516,6 +793,10 @@ var elementSchema = z2.discriminatedUnion("type", [
516
793
  shapeElementSchema,
517
794
  imageElementSchema
518
795
  ]);
796
+ var diagramCenterSchema = z2.object({
797
+ x: z2.number(),
798
+ y: z2.number()
799
+ }).strict();
519
800
  var autoLayoutConfigSchema = z2.object({
520
801
  mode: z2.literal("auto"),
521
802
  algorithm: z2.enum(["layered", "stress", "force", "radial", "box"]).default("layered"),
@@ -523,7 +804,17 @@ var autoLayoutConfigSchema = z2.object({
523
804
  nodeSpacing: z2.number().int().min(0).max(512).default(80),
524
805
  rankSpacing: z2.number().int().min(0).max(512).default(120),
525
806
  edgeRouting: z2.enum(["orthogonal", "polyline", "spline"]).default("polyline"),
526
- aspectRatio: z2.number().min(0.5).max(3).optional()
807
+ aspectRatio: z2.number().min(0.5).max(3).optional(),
808
+ /** ID of the root node for radial layout. Only relevant when algorithm is 'radial'. */
809
+ radialRoot: z2.string().min(1).max(120).optional(),
810
+ /** Fixed radius in pixels for radial layout. Only relevant when algorithm is 'radial'. */
811
+ radialRadius: z2.number().positive().optional(),
812
+ /** Compaction strategy for radial layout. Only relevant when algorithm is 'radial'. */
813
+ radialCompaction: z2.enum(["none", "radial", "wedge"]).optional(),
814
+ /** Sort strategy for radial layout node ordering. Only relevant when algorithm is 'radial'. */
815
+ radialSortBy: z2.enum(["id", "connections"]).optional(),
816
+ /** Explicit center used by curve/arc connection routing. */
817
+ diagramCenter: diagramCenterSchema.optional()
527
818
  }).strict();
528
819
  var gridLayoutConfigSchema = z2.object({
529
820
  mode: z2.literal("grid"),
@@ -531,13 +822,17 @@ var gridLayoutConfigSchema = z2.object({
531
822
  gap: z2.number().int().min(0).max(256).default(24),
532
823
  cardMinHeight: z2.number().int().min(32).max(4096).optional(),
533
824
  cardMaxHeight: z2.number().int().min(32).max(4096).optional(),
534
- equalHeight: z2.boolean().default(false)
825
+ equalHeight: z2.boolean().default(false),
826
+ /** Explicit center used by curve/arc connection routing. */
827
+ diagramCenter: diagramCenterSchema.optional()
535
828
  }).strict();
536
829
  var stackLayoutConfigSchema = z2.object({
537
830
  mode: z2.literal("stack"),
538
831
  direction: z2.enum(["vertical", "horizontal"]).default("vertical"),
539
832
  gap: z2.number().int().min(0).max(256).default(24),
540
- alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch")
833
+ alignment: z2.enum(["start", "center", "end", "stretch"]).default("stretch"),
834
+ /** Explicit center used by curve/arc connection routing. */
835
+ diagramCenter: diagramCenterSchema.optional()
541
836
  }).strict();
542
837
  var manualPositionSchema = z2.object({
543
838
  x: z2.number().int(),
@@ -547,7 +842,9 @@ var manualPositionSchema = z2.object({
547
842
  }).strict();
548
843
  var manualLayoutConfigSchema = z2.object({
549
844
  mode: z2.literal("manual"),
550
- positions: z2.record(z2.string().min(1), manualPositionSchema).default({})
845
+ positions: z2.record(z2.string().min(1), manualPositionSchema).default({}),
846
+ /** Explicit center used by curve/arc connection routing. */
847
+ diagramCenter: diagramCenterSchema.optional()
551
848
  }).strict();
552
849
  var layoutConfigSchema = z2.discriminatedUnion("mode", [
553
850
  autoLayoutConfigSchema,
@@ -599,6 +896,31 @@ var canvasSchema = z2.object({
599
896
  padding: z2.number().int().min(0).max(256).default(defaultCanvas.padding)
600
897
  }).strict();
601
898
  var themeInputSchema = z2.union([builtInThemeSchema, themeSchema]);
899
+ var diagramPositionSchema = z2.object({
900
+ x: z2.number(),
901
+ y: z2.number(),
902
+ width: z2.number().positive(),
903
+ height: z2.number().positive()
904
+ }).strict();
905
+ var diagramElementSchema = z2.discriminatedUnion("type", [
906
+ flowNodeElementSchema,
907
+ connectionElementSchema
908
+ ]);
909
+ var diagramLayoutSchema = z2.object({
910
+ mode: z2.enum(["manual", "auto"]).default("manual"),
911
+ positions: z2.record(z2.string(), diagramPositionSchema).optional(),
912
+ diagramCenter: diagramCenterSchema.optional()
913
+ }).strict();
914
+ var diagramSpecSchema = z2.object({
915
+ version: z2.literal(1),
916
+ canvas: z2.object({
917
+ width: z2.number().int().min(320).max(4096).default(1200),
918
+ height: z2.number().int().min(180).max(4096).default(675)
919
+ }).default({ width: 1200, height: 675 }),
920
+ theme: themeSchema.optional(),
921
+ elements: z2.array(diagramElementSchema).min(1),
922
+ layout: diagramLayoutSchema.default({ mode: "manual" })
923
+ }).strict();
602
924
  var designSpecSchema = z2.object({
603
925
  version: z2.literal(2).default(2),
604
926
  canvas: canvasSchema.default(defaultCanvas),
@@ -627,43 +949,6 @@ function parseDesignSpec(input) {
627
949
  return designSpecSchema.parse(input);
628
950
  }
629
951
 
630
- // src/utils/color.ts
631
- function parseChannel(hex, offset) {
632
- return Number.parseInt(hex.slice(offset, offset + 2), 16);
633
- }
634
- function parseHexColor(hexColor) {
635
- const normalized = hexColor.startsWith("#") ? hexColor.slice(1) : hexColor;
636
- if (normalized.length !== 6 && normalized.length !== 8) {
637
- throw new Error(`Unsupported color format: ${hexColor}`);
638
- }
639
- return {
640
- r: parseChannel(normalized, 0),
641
- g: parseChannel(normalized, 2),
642
- b: parseChannel(normalized, 4)
643
- };
644
- }
645
- function srgbToLinear(channel) {
646
- const normalized = channel / 255;
647
- if (normalized <= 0.03928) {
648
- return normalized / 12.92;
649
- }
650
- return ((normalized + 0.055) / 1.055) ** 2.4;
651
- }
652
- function relativeLuminance(hexColor) {
653
- const rgb = parseHexColor(hexColor);
654
- const r = srgbToLinear(rgb.r);
655
- const g = srgbToLinear(rgb.g);
656
- const b = srgbToLinear(rgb.b);
657
- return 0.2126 * r + 0.7152 * g + 0.0722 * b;
658
- }
659
- function contrastRatio(foreground, background) {
660
- const fg = relativeLuminance(foreground);
661
- const bg = relativeLuminance(background);
662
- const lighter = Math.max(fg, bg);
663
- const darker = Math.min(fg, bg);
664
- return (lighter + 0.05) / (darker + 0.05);
665
- }
666
-
667
952
  // src/qa.ts
668
953
  function rectWithin(outer, inner) {
669
954
  return inner.x >= outer.x && inner.y >= outer.y && inner.x + inner.width <= outer.x + outer.width && inner.y + inner.height <= outer.y + outer.height;
@@ -709,7 +994,7 @@ async function runQa(options) {
709
994
  const imagePath = resolve(options.imagePath);
710
995
  const expectedSafeFrame = deriveSafeFrame(spec);
711
996
  const expectedCanvas = canvasRect(spec);
712
- const imageMetadata = await sharp(imagePath).metadata();
997
+ const imageMetadata = await sharp2(imagePath).metadata();
713
998
  const issues = [];
714
999
  const expectedScale = options.metadata?.canvas.scale ?? resolveRenderScale(spec);
715
1000
  const expectedWidth = spec.canvas.width * expectedScale;
@@ -860,6 +1145,31 @@ async function runQa(options) {
860
1145
  });
861
1146
  }
862
1147
  }
1148
+ let referenceResult;
1149
+ if (options.referencePath) {
1150
+ const { compareImages: compareImages2 } = await Promise.resolve().then(() => (init_compare(), compare_exports));
1151
+ const comparison = await compareImages2(options.referencePath, imagePath);
1152
+ referenceResult = {
1153
+ similarity: comparison.similarity,
1154
+ verdict: comparison.verdict,
1155
+ regions: comparison.regions.map((region) => ({
1156
+ label: region.label,
1157
+ similarity: region.similarity
1158
+ }))
1159
+ };
1160
+ if (comparison.verdict === "mismatch") {
1161
+ const severity = comparison.similarity < 0.5 ? "error" : "warning";
1162
+ issues.push({
1163
+ code: "REFERENCE_MISMATCH",
1164
+ severity,
1165
+ message: `Reference image comparison ${severity === "error" ? "failed" : "warned"}: similarity ${comparison.similarity.toFixed(4)} with verdict "${comparison.verdict}".`,
1166
+ details: {
1167
+ similarity: comparison.similarity,
1168
+ verdict: comparison.verdict
1169
+ }
1170
+ });
1171
+ }
1172
+ }
863
1173
  const footerSpacingPx = options.metadata?.layout.elements ? (() => {
864
1174
  const footer = options.metadata.layout.elements.find((element) => element.id === "footer");
865
1175
  if (!footer) {
@@ -892,7 +1202,8 @@ async function runQa(options) {
892
1202
  ...imageMetadata.height !== void 0 ? { height: imageMetadata.height } : {},
893
1203
  ...footerSpacingPx !== void 0 ? { footerSpacingPx } : {}
894
1204
  },
895
- issues
1205
+ issues,
1206
+ ...referenceResult ? { reference: referenceResult } : {}
896
1207
  };
897
1208
  }
898
1209
  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-DhAI-tE8.js';
1
+ export { h as DEFAULT_GENERATOR_VERSION, N as LayoutSnapshot, a as Rect, R as RenderMetadata, Q as RenderResult, d as RenderedElement, Z as WrittenArtifacts, a0 as computeSpecHash, aj as inferSidecarPath, am as renderDesign, ao as writeRenderArtifacts } from './spec.schema-Dm_wOLTd.js';
2
2
  import 'zod';
3
3
  import '@napi-rs/canvas';