@invarn/cibuild 2.2.1 → 2.2.2

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.
@@ -224,9 +224,25 @@ export type AppearanceFact = {
224
224
  kind: 'element_extra';
225
225
  elementId: string;
226
226
  };
227
+ /**
228
+ * A reference-shaped snapshot of the render's OWN appearance — the element-id
229
+ * set plus per-element text/colors it produced. Persisted in the structural
230
+ * block so a later no-Figma render can use it as the `previous_render` baseline
231
+ * (the colors/text/ids to diff against), mirroring how Signal B's geometry
232
+ * baseline reuses the previous render's persisted `geometry`. text/color are
233
+ * present only for the elements that exposed them.
234
+ */
235
+ export interface AppearanceRendered {
236
+ elementIds: string[];
237
+ text?: TextSource;
238
+ color?: ColorSource;
239
+ }
227
240
  export interface StructuralAppearance {
228
241
  baseline: AppearanceBaseline;
229
242
  facts: AppearanceFact[];
243
+ /** The render's own appearance, persisted as the baseline source for the next
244
+ * no-Figma render. Present whenever Signal C ran. */
245
+ rendered?: AppearanceRendered;
230
246
  }
231
247
  /**
232
248
  * The reference side of the appearance diff, passed into `buildStructuralBlock`.
@@ -242,6 +258,10 @@ export interface AppearanceReference {
242
258
  * previous render's text on `previous_render`). When supplied, copy facts are
243
259
  * derived against the render's per-node text. Absent → presence facts only. */
244
260
  text?: TextSource;
261
+ /** Reference fill/text color per element id (Figma `fills` on the figma
262
+ * baseline; the previous render's colors on `previous_render`). When supplied,
263
+ * color facts are derived against the render's per-node colors. */
264
+ color?: ColorSource;
245
265
  }
246
266
  /**
247
267
  * Element-presence facts from the set difference of the rendered element-id set
@@ -275,6 +295,42 @@ export type TextSource = Record<string, string | undefined>;
275
295
  * separately-wired concern.
276
296
  */
277
297
  export declare function deriveCopyFacts(rendered: TextSource, reference: TextSource): AppearanceFact[];
298
+ /** CIELAB triple [L, a, b]. */
299
+ export type Lab = [number, number, number];
300
+ /** One element's rendered/reference colors, by channel. A missing channel is
301
+ * "no color signal" for that element/channel — never a divergence. */
302
+ export interface ElementColors {
303
+ fill?: string;
304
+ text?: string;
305
+ }
306
+ export type ColorSource = Record<string, ElementColors>;
307
+ /**
308
+ * Convert an sRGB hex string to CIELAB (D65). White → ~[100,0,0], black → L 0.
309
+ * Returns [0,0,0] for an unparseable string so a malformed color never throws
310
+ * (the deriver guards on signal presence separately).
311
+ */
312
+ export declare function hexToLab(hex: string): Lab;
313
+ /**
314
+ * CIEDE2000 colour-difference ΔE00 between two CIELAB colours (kL=kC=kH=1).
315
+ * Implements Sharma, Wu & Dalal (2005); validated against their supplementary
316
+ * test data. Pure and symmetric; identical colours give 0.
317
+ */
318
+ export declare function ciede2000(lab1: Lab, lab2: Lab): number;
319
+ /**
320
+ * Color facts from the per-element, per-channel comparison of rendered vs
321
+ * reference color, keyed on the stable element id shared with Signal B. For each
322
+ * element/channel the REFERENCE declares a color for and the render also exposes
323
+ * one: identical color → no fact; any divergence → a `color` fact carrying the
324
+ * channel (fill vs text), both hex values, and the CIEDE2000 ΔE as the metric.
325
+ * Fill and text are distinct channels, so a correct background with a wrong text
326
+ * color and the reverse are separate facts, never collapsed. A missing rendered
327
+ * or reference color for a channel is no signal → no fact.
328
+ *
329
+ * Pure and platform-agnostic; order-independent and deterministic (output sorted
330
+ * by element id then channel, fill before text). Measurement only — significance
331
+ * is a later, separately-wired concern.
332
+ */
333
+ export declare function deriveColorFacts(rendered: ColorSource, reference: ColorSource): AppearanceFact[];
278
334
  /**
279
335
  * Assemble the inline `structural` block (ADR-0004): Signal-A facts always, plus
280
336
  * the Signal-B geometry block when `referenceGeometry` is supplied. Size (w/h)
@@ -361,5 +417,8 @@ export declare function getGeometryStageInternals(): {
361
417
  }) => ReviewScaffold;
362
418
  derivePresenceFacts: (renderedIds: Iterable<string>, referenceIds: Iterable<string>) => AppearanceFact[];
363
419
  deriveCopyFacts: (rendered: TextSource, reference: TextSource) => AppearanceFact[];
420
+ deriveColorFacts: (rendered: ColorSource, reference: ColorSource) => AppearanceFact[];
421
+ ciede2000: (lab1: Lab, lab2: Lab) => number;
422
+ hexToLab: (hex: string) => Lab;
364
423
  };
365
424
  //# sourceMappingURL=structural-facts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"structural-facts.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/structural-facts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"structural-facts.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/structural-facts.ts"],"names":[],"mappings":"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"}
@@ -8,7 +8,7 @@
8
8
  * overflowing voucher tag; the deriver must surface the bottom clip as a
9
9
  * fact so it is never again skipped by eye.
10
10
  */
11
- import { deriveStructuralFacts, derivePresenceFacts, deriveCopyFacts, diffKeyGeometry, buildStructuralBlock, buildReviewScaffold, getStructuralFactsStageInternals, getGeometryStageInternals, } from './structural-facts.js';
11
+ import { deriveStructuralFacts, derivePresenceFacts, deriveCopyFacts, deriveColorFacts, ciede2000, hexToLab, diffKeyGeometry, buildStructuralBlock, buildReviewScaffold, getStructuralFactsStageInternals, getGeometryStageInternals, } from './structural-facts.js';
12
12
  // 200x400 canvas in px for all fixtures unless noted.
13
13
  const CANVAS = { left: 0, top: 0, right: 200, bottom: 400 };
14
14
  describe('deriveStructuralFacts — containment', () => {
@@ -518,6 +518,92 @@ describe('derivePresenceFacts — set difference over element ids', () => {
518
518
  ]);
519
519
  });
520
520
  });
521
+ describe('ciede2000 — CIEDE2000 ΔE against the Sharma supplementary test data', () => {
522
+ // Canonical test vectors from Sharma, Wu & Dalal, "The CIEDE2000 Color-
523
+ // Difference Formula: Implementation Notes, Supplementary Test Data, and
524
+ // Mathematical Observations" (2005). Each row: Lab1, Lab2, expected ΔE00.
525
+ // These exercise the hue-discontinuity and high-chroma edge cases an
526
+ // incorrect implementation gets wrong.
527
+ const SHARMA = [
528
+ [[50, 2.6772, -79.7751], [50, 0, -82.7485], 2.0425],
529
+ [[50, 3.1571, -77.2803], [50, 0, -82.7485], 2.8615],
530
+ [[50, 2.8361, -74.02], [50, 0, -82.7485], 3.4412],
531
+ [[50, -1.3802, -84.2814], [50, 0, -82.7485], 1.0],
532
+ [[50, -1.1848, -84.8006], [50, 0, -82.7485], 1.0],
533
+ [[50, 0, 0], [50, -1, 2], 2.3669],
534
+ [[50, 2.49, -0.001], [50, -2.49, 0.0009], 7.1792],
535
+ [[50, 2.49, -0.001], [50, -2.49, 0.0011], 7.2195],
536
+ [[50, -0.001, 2.49], [50, 0.0009, -2.49], 4.8045],
537
+ [[50, 2.5, 0], [50, 0, -2.5], 4.3065],
538
+ [[50, 2.5, 0], [73, 25, -18], 27.1492],
539
+ [[50, 2.5, 0], [50, 3.1736, 0.5854], 1.0],
540
+ [[60.2574, -34.0099, 36.2677], [60.4626, -34.1751, 39.4387], 1.2644],
541
+ [[63.0109, -31.0961, -5.8663], [62.8187, -29.7946, -4.0864], 1.263],
542
+ [[22.7233, 20.0904, -46.694], [23.0331, 14.973, -42.5619], 2.0373],
543
+ [[90.8027, -2.0831, 1.441], [91.1528, -1.6435, 0.0447], 1.4441],
544
+ [[2.0776, 0.0795, -1.135], [0.9033, -0.0636, -0.5514], 0.9082],
545
+ ];
546
+ for (const [lab1, lab2, expected] of SHARMA) {
547
+ test(`ΔE(${lab1.join(',')} ; ${lab2.join(',')}) ≈ ${expected}`, () => {
548
+ expect(ciede2000(lab1, lab2)).toBeCloseTo(expected, 4);
549
+ });
550
+ }
551
+ test('identical colors have ΔE 0 and the metric is symmetric', () => {
552
+ expect(ciede2000([42, 7, -13], [42, 7, -13])).toBe(0);
553
+ expect(ciede2000([50, 2.5, 0], [73, 25, -18])).toBeCloseTo(ciede2000([73, 25, -18], [50, 2.5, 0]), 10);
554
+ });
555
+ });
556
+ describe('hexToLab — sRGB hex → CIELAB (D65)', () => {
557
+ test('white maps to L≈100, a≈0, b≈0', () => {
558
+ const [L, a, b] = hexToLab('#ffffff');
559
+ expect(L).toBeCloseTo(100, 3);
560
+ expect(a).toBeCloseTo(0, 3);
561
+ expect(b).toBeCloseTo(0, 3);
562
+ });
563
+ test('black maps to L≈0', () => {
564
+ const [L] = hexToLab('#000000');
565
+ expect(L).toBeCloseTo(0, 3);
566
+ });
567
+ test('accepts hex with or without a leading # and is case-insensitive', () => {
568
+ expect(hexToLab('FFFFFF')).toEqual(hexToLab('#ffffff'));
569
+ });
570
+ });
571
+ describe('deriveColorFacts — rendered vs reference color per element/channel', () => {
572
+ test('matching colors produce no fact', () => {
573
+ expect(deriveColorFacts({ card: { fill: '#ff0000', text: '#000000' } }, { card: { fill: '#ff0000', text: '#000000' } })).toEqual([]);
574
+ // case-insensitive match
575
+ expect(deriveColorFacts({ card: { fill: '#FF0000' } }, { card: { fill: '#ff0000' } })).toEqual([]);
576
+ });
577
+ test('a fill divergence yields one fill-channel fact with the correct ΔE', () => {
578
+ const facts = deriveColorFacts({ card: { fill: '#ff0000' } }, { card: { fill: '#00ff00' } });
579
+ expect(facts).toHaveLength(1);
580
+ expect(facts[0]).toMatchObject({ kind: 'color', elementId: 'card', channel: 'fill', rendered: '#ff0000', reference: '#00ff00' });
581
+ expect(facts[0].deltaE).toBeCloseTo(ciede2000(hexToLab('#ff0000'), hexToLab('#00ff00')), 10);
582
+ });
583
+ test('fill and text are distinct channels — a wrong text color does not collapse with a correct fill', () => {
584
+ const facts = deriveColorFacts({ card: { fill: '#ffffff', text: '#111111' } }, { card: { fill: '#ffffff', text: '#000000' } });
585
+ expect(facts).toHaveLength(1);
586
+ expect(facts[0]).toMatchObject({ kind: 'color', elementId: 'card', channel: 'text', rendered: '#111111', reference: '#000000' });
587
+ });
588
+ test('both channels diverging yield two facts, fill before text', () => {
589
+ const facts = deriveColorFacts({ card: { fill: '#ff0000', text: '#111111' } }, { card: { fill: '#00ff00', text: '#000000' } });
590
+ expect(facts.map((f) => f.channel)).toEqual(['fill', 'text']);
591
+ });
592
+ test('an element/channel with no color signal produces no fact', () => {
593
+ // reference declares a fill the render never exposes → no signal → no fact
594
+ expect(deriveColorFacts({ card: {} }, { card: { fill: '#ff0000' } })).toEqual([]);
595
+ // render has a color the reference never declared → no reference signal → no fact
596
+ expect(deriveColorFacts({ card: { text: '#abcdef' } }, { card: { fill: '#ff0000' } })).toEqual([]);
597
+ // element absent from the reference entirely → no fact
598
+ expect(deriveColorFacts({ extra: { fill: '#ff0000' } }, {})).toEqual([]);
599
+ });
600
+ test('result is order-independent and deterministic (sorted by element id)', () => {
601
+ const a = deriveColorFacts({ b: { fill: '#ff0000' }, a: { fill: '#000000' } }, { a: { fill: '#ffffff' }, b: { fill: '#ff0000' } });
602
+ const b = deriveColorFacts({ a: { fill: '#000000' }, b: { fill: '#ff0000' } }, { b: { fill: '#ff0000' }, a: { fill: '#ffffff' } });
603
+ expect(a).toEqual(b);
604
+ expect(a.map((f) => f.elementId)).toEqual(['a']);
605
+ });
606
+ });
521
607
  describe('deriveCopyFacts — rendered vs reference text per element', () => {
522
608
  test('identical text produces no fact', () => {
523
609
  expect(deriveCopyFacts({ title: '5 EUR off' }, { title: '5 EUR off' })).toEqual([]);
@@ -572,6 +658,7 @@ describe('buildStructuralBlock — appearance container (Signal C, additive)', (
572
658
  { kind: 'element_extra', elementId: 'copy' },
573
659
  { kind: 'element_extra', elementId: 'node-1' },
574
660
  ],
661
+ rendered: { elementIds: ['banner', 'copy', 'node-1', 'voucher'] },
575
662
  });
576
663
  // Additive: the existing Signal A facts and Signal B geometry are untouched.
577
664
  expect(block.facts).toEqual(buildStructuralBlock(HUB_PROMO_DUMP, HUB_PROMO_REFERENCE, 2).facts);
@@ -582,7 +669,11 @@ describe('buildStructuralBlock — appearance container (Signal C, additive)', (
582
669
  baseline: 'figma',
583
670
  elementIds: ['node-1', 'banner', 'voucher', 'copy'],
584
671
  });
585
- expect(block.appearance).toEqual({ baseline: 'figma', facts: [] });
672
+ expect(block.appearance).toEqual({
673
+ baseline: 'figma',
674
+ facts: [],
675
+ rendered: { elementIds: ['banner', 'copy', 'node-1', 'voucher'] },
676
+ });
586
677
  });
587
678
  // A dump whose per-node `text` (Signal C render-side enrichment) drives the
588
679
  // copy-fact comparison: `title` renders the wrong string, `cta` renders no
@@ -610,6 +701,7 @@ describe('buildStructuralBlock — appearance container (Signal C, additive)', (
610
701
  { kind: 'text_missing', elementId: 'cta', reference: 'Shop now' },
611
702
  { kind: 'text_mismatch', elementId: 'title', rendered: '5 EUR', reference: '5 EUR off' },
612
703
  ],
704
+ rendered: { elementIds: ['cta', 'debugOverlay', 'title'], text: { title: '5 EUR' } },
613
705
  });
614
706
  });
615
707
  test('an appearanceReference without text → presence only (issue-01 behavior unchanged)', () => {
@@ -617,7 +709,97 @@ describe('buildStructuralBlock — appearance container (Signal C, additive)', (
617
709
  baseline: 'figma',
618
710
  elementIds: ['title', 'cta', 'debugOverlay'],
619
711
  });
620
- expect(block.appearance).toEqual({ baseline: 'figma', facts: [] });
712
+ expect(block.appearance).toEqual({
713
+ baseline: 'figma',
714
+ facts: [],
715
+ rendered: { elementIds: ['cta', 'debugOverlay', 'title'], text: { title: '5 EUR' } },
716
+ });
717
+ });
718
+ // A dump whose per-node fill/text colors (Signal C render-side enrichment)
719
+ // drive the color-fact comparison: `card` renders the wrong fill, correct text.
720
+ const COLOR_DUMP = {
721
+ canvas: { left: 0, top: 0, right: 200, bottom: 400 },
722
+ nodes: [
723
+ { id: 'card', bounds: { left: 0, top: 0, right: 100, bottom: 40 }, parentId: null, clipsChildren: false, fillColor: '#ff0000', textColor: '#000000' },
724
+ ],
725
+ };
726
+ test('with reference color → color facts are appended after presence/copy facts', () => {
727
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
728
+ baseline: 'figma',
729
+ elementIds: ['card'],
730
+ color: { card: { fill: '#00ff00', text: '#000000' } },
731
+ });
732
+ expect(block.appearance?.baseline).toBe('figma');
733
+ expect(block.appearance?.facts).toEqual([
734
+ {
735
+ kind: 'color',
736
+ elementId: 'card',
737
+ channel: 'fill',
738
+ rendered: '#ff0000',
739
+ reference: '#00ff00',
740
+ deltaE: ciede2000(hexToLab('#ff0000'), hexToLab('#00ff00')),
741
+ },
742
+ ]);
743
+ });
744
+ test('an appearanceReference without color → no color facts (presence only)', () => {
745
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
746
+ baseline: 'figma',
747
+ elementIds: ['card'],
748
+ });
749
+ expect(block.appearance).toEqual({
750
+ baseline: 'figma',
751
+ facts: [],
752
+ rendered: { elementIds: ['card'], color: { card: { fill: '#ff0000', text: '#000000' } } },
753
+ });
754
+ });
755
+ // ---- Signal C no-Figma baseline (issue 04) ----
756
+ test('the appearance container persists a rendered snapshot (the baseline source for the next render)', () => {
757
+ // The snapshot captures THIS render's own element ids + text + colors so a
758
+ // later no-Figma render can diff against it. Text/color are present only for
759
+ // the nodes that expose them.
760
+ const block = buildStructuralBlock(COPY_DUMP, null, 2, undefined, {
761
+ baseline: 'figma',
762
+ elementIds: ['title', 'cta'],
763
+ });
764
+ expect(block.appearance?.rendered).toEqual({
765
+ elementIds: ['cta', 'debugOverlay', 'title'],
766
+ text: { title: '5 EUR' },
767
+ });
768
+ });
769
+ test('a previous_render baseline marks the facts as regressions (drift), derivers unchanged', () => {
770
+ // The same derivers run; only the reference source + baseline marking differ.
771
+ // Reference here is the *previous render*'s ids/text/color.
772
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
773
+ baseline: 'previous_render',
774
+ elementIds: ['card'],
775
+ color: { card: { fill: '#00ff00' } },
776
+ });
777
+ expect(block.appearance?.baseline).toBe('previous_render');
778
+ expect(block.appearance?.facts).toEqual([
779
+ {
780
+ kind: 'color',
781
+ elementId: 'card',
782
+ channel: 'fill',
783
+ rendered: '#ff0000',
784
+ reference: '#00ff00',
785
+ deltaE: ciede2000(hexToLab('#ff0000'), hexToLab('#00ff00')),
786
+ },
787
+ ]);
788
+ });
789
+ test('a previous_render baseline diffs presence against the previous element-id set', () => {
790
+ // COLOR_DUMP renders only `card`; the previous render had `card` + `legacyBadge`.
791
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
792
+ baseline: 'previous_render',
793
+ elementIds: ['card', 'legacyBadge'],
794
+ });
795
+ expect(block.appearance).toMatchObject({
796
+ baseline: 'previous_render',
797
+ facts: [{ kind: 'element_missing', elementId: 'legacyBadge' }],
798
+ });
799
+ });
800
+ test('no appearanceReference (neither Figma nor previous render) → no container, no rendered snapshot', () => {
801
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2);
802
+ expect(block.appearance).toBeUndefined();
621
803
  });
622
804
  });
623
805
  describe('embedded geometry stage parity', () => {
@@ -676,6 +858,45 @@ describe('embedded geometry stage parity', () => {
676
858
  };
677
859
  expect(embedded.buildStructuralBlock(dumpWithText, null, 2, undefined, appearanceReference)).toEqual(buildStructuralBlock(dumpWithText, null, 2, undefined, appearanceReference));
678
860
  });
861
+ test('embedded ciede2000 + hexToLab match the module (Signal C)', () => {
862
+ const labCases = [
863
+ [[50, 2.6772, -79.7751], [50, 0, -82.7485]],
864
+ [[50, 2.5, 0], [73, 25, -18]],
865
+ [[60.2574, -34.0099, 36.2677], [60.4626, -34.1751, 39.4387]],
866
+ ];
867
+ for (const [a, b] of labCases) {
868
+ expect(embedded.ciede2000(a, b)).toBeCloseTo(ciede2000(a, b), 12);
869
+ }
870
+ for (const hex of ['#ffffff', '#000000', '#ff0000', '#3a7bd5', 'abcdef']) {
871
+ expect(embedded.hexToLab(hex)).toEqual(hexToLab(hex));
872
+ }
873
+ });
874
+ test('embedded deriveColorFacts matches the module (Signal C)', () => {
875
+ const cases = [
876
+ [{ card: { fill: '#ff0000' } }, { card: { fill: '#ff0000' } }],
877
+ [{ card: { fill: '#ff0000' } }, { card: { fill: '#00ff00' } }],
878
+ [{ card: { fill: '#ffffff', text: '#111111' } }, { card: { fill: '#ffffff', text: '#000000' } }],
879
+ [{ card: {} }, { card: { fill: '#ff0000' } }],
880
+ [{ extra: { fill: '#ff0000' } }, {}],
881
+ ];
882
+ for (const [rendered, reference] of cases) {
883
+ expect(embedded.deriveColorFacts(rendered, reference)).toEqual(deriveColorFacts(rendered, reference));
884
+ }
885
+ });
886
+ test('embedded buildStructuralBlock matches the module with an appearanceReference carrying color', () => {
887
+ const dumpWithColor = {
888
+ canvas: { left: 0, top: 0, right: 200, bottom: 400 },
889
+ nodes: [
890
+ { id: 'card', bounds: { left: 0, top: 0, right: 100, bottom: 40 }, parentId: null, clipsChildren: false, fillColor: '#ff0000', textColor: '#000000' },
891
+ ],
892
+ };
893
+ const appearanceReference = {
894
+ baseline: 'figma',
895
+ elementIds: ['card'],
896
+ color: { card: { fill: '#00ff00', text: '#000000' } },
897
+ };
898
+ expect(embedded.buildStructuralBlock(dumpWithColor, null, 2, undefined, appearanceReference)).toEqual(buildStructuralBlock(dumpWithColor, null, 2, undefined, appearanceReference));
899
+ });
679
900
  const parityCases = [
680
901
  { name: 'factsOnly', dump: HUB_PROMO_DUMP, ref: null },
681
902
  { name: 'withGeometry', dump: HUB_PROMO_DUMP, ref: HUB_PROMO_REFERENCE },
@@ -1 +1 @@
1
- {"version":3,"file":"ui-fidelity-render-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAKpE,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,8EAA8E;AAC9E,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AA0wBD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAYrF;AAED;;;GAGG;AACH,qBAAa,mCAAoC,SAAQ,gBAAgB;IACvE,yBAAyB,CACvB,OAAO,EAAE,6BAA6B,EACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAepB,OAAO,CACX,MAAM,EAAE,6BAA6B,EACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAgBpB"}
1
+ {"version":3,"file":"ui-fidelity-render-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAKpE,sDAAsD;AACtD,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAExD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,8EAA8E;AAC9E,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB;AA8xBD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAYrF;AAED;;;GAGG;AACH,qBAAa,mCAAoC,SAAQ,gBAAgB;IACvE,yBAAyB,CACvB,OAAO,EAAE,6BAA6B,EACtC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAepB,OAAO,CACX,MAAM,EAAE,6BAA6B,EACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAgBpB"}
@@ -99,6 +99,14 @@ var referenceGeometry = null;
99
99
  // set in main() and read by the structural assembler. Null when none declared →
100
100
  // the appearance container carries presence facts only (no copy facts).
101
101
  var referenceText = null;
102
+ // params.reference_color (Signal C: Figma fills per element id per screen, as
103
+ // { fill, text } hex), set in main() and read by the structural assembler. Null
104
+ // when none declared → no color facts.
105
+ var referenceColor = null;
106
+ // params.reference_baseline (Signal C: 'figma' (default) or 'previous_render').
107
+ // Marks whether the appearance reference is the design (divergences) or the
108
+ // previous catalog render (regressions/drift, the no-Figma path).
109
+ var referenceBaseline = null;
102
110
 
103
111
  function log(message) {
104
112
  console.log('[ui-fidelity-render-android] ' + message);
@@ -611,7 +619,7 @@ function renderScreens(renderable, projectRoot) {
611
619
  // geometry deltas vs reference_geometry) onto the screen entry, so the
612
620
  // judge reads the signals from protocol-result.json without fetching
613
621
  // artifacts. CONFIG.scale is the render density (dp = px / scale).
614
- assembleStructural(entry, layoutSource, referenceGeometry, CONFIG.scale, referenceText);
622
+ assembleStructural(entry, layoutSource, referenceGeometry, CONFIG.scale, referenceText, referenceColor, referenceBaseline);
615
623
  }
616
624
  });
617
625
  } finally {
@@ -685,6 +693,18 @@ function main() {
685
693
  !Array.isArray(params.reference_text)
686
694
  ? params.reference_text
687
695
  : null;
696
+ // Signal-C color reference: { "<Screen>": { "<elementId>": { fill, text } } } —
697
+ // the Figma fills mapped to element ids. Absent → no color facts.
698
+ referenceColor =
699
+ params.reference_color &&
700
+ typeof params.reference_color === 'object' &&
701
+ !Array.isArray(params.reference_color)
702
+ ? params.reference_color
703
+ : null;
704
+ // Signal-C baseline marker: 'previous_render' when the appearance reference was
705
+ // sourced from the previous catalog render (no-Figma drift path), else 'figma'.
706
+ referenceBaseline =
707
+ params.reference_baseline === 'previous_render' ? 'previous_render' : 'figma';
688
708
 
689
709
  var screens = Object.keys(params.screens);
690
710
  currentEntries = screens.map(function (screen) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",