@invarn/cibuild 2.2.3 → 2.2.4

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.
@@ -741,6 +741,33 @@ describe('buildStructuralBlock — appearance container (Signal C, additive)', (
741
741
  },
742
742
  ]);
743
743
  });
744
+ test('with reference text color → a text-channel color fact (issue 02)', () => {
745
+ // COLOR_DUMP renders `card` text #000000; the reference declares #ffffff for
746
+ // the text channel and the matching fill — so exactly one fact, channel:'text'.
747
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
748
+ baseline: 'figma',
749
+ elementIds: ['card'],
750
+ color: { card: { fill: '#ff0000', text: '#ffffff' } },
751
+ });
752
+ expect(block.appearance?.facts).toEqual([
753
+ {
754
+ kind: 'color',
755
+ elementId: 'card',
756
+ channel: 'text',
757
+ rendered: '#000000',
758
+ reference: '#ffffff',
759
+ deltaE: ciede2000(hexToLab('#000000'), hexToLab('#ffffff')),
760
+ },
761
+ ]);
762
+ });
763
+ test('an identical text color → no text fact (issue 02)', () => {
764
+ const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
765
+ baseline: 'figma',
766
+ elementIds: ['card'],
767
+ color: { card: { fill: '#ff0000', text: '#000000' } },
768
+ });
769
+ expect(block.appearance?.facts).toEqual([]);
770
+ });
744
771
  test('an appearanceReference without color → no color facts (presence only)', () => {
745
772
  const block = buildStructuralBlock(COLOR_DUMP, null, 2, undefined, {
746
773
  baseline: 'figma',
@@ -822,6 +849,20 @@ describe('appearanceReferenceFromPreviousRender — previous render snapshot →
822
849
  color: { banner: { fill: '#fff0ee' } },
823
850
  });
824
851
  });
852
+ test('a previous render with per-element text colors → the text channel rides through into the reference (issue 02)', () => {
853
+ // The mapper carries the whole previous color map, so the foreground (text)
854
+ // channel reaches the reference exactly as the fill channel does — channel
855
+ // genericity, not a fill-only carry.
856
+ const previous = {
857
+ elementIds: ['banner', 'copy'],
858
+ color: { banner: { fill: '#fff0ee' }, copy: { text: '#1b1b1b' } },
859
+ };
860
+ expect(appearanceReferenceFromPreviousRender(previous)).toEqual({
861
+ baseline: 'previous_render',
862
+ elementIds: ['banner', 'copy'],
863
+ color: { banner: { fill: '#fff0ee' }, copy: { text: '#1b1b1b' } },
864
+ });
865
+ });
825
866
  test('a previous render with no extractable color → a presence-only reference (ids, no color)', () => {
826
867
  const previous = { elementIds: ['banner', 'voucher'] };
827
868
  expect(appearanceReferenceFromPreviousRender(previous)).toEqual({
@@ -870,6 +911,60 @@ describe('appearanceReferenceFromPreviousRender — previous render snapshot →
870
911
  ]);
871
912
  expect(colorFacts[0].kind === 'color' && colorFacts[0].deltaE).toBeGreaterThan(10);
872
913
  });
914
+ test('fed into buildStructuralBlock, a drifted text color produces a previous_render text-channel fact (issue 02)', () => {
915
+ // The render paints `copy` the wrong foreground color (#1e88e5); the previous
916
+ // render painted #1b1b1b. The mapper carries the text channel through, and the
917
+ // assembled block carries one text color fact with a large positive ΔE —
918
+ // distinct from fill, proven through the public interface.
919
+ const driftedDump = {
920
+ canvas: { left: 0, top: 0, right: 686, bottom: 282 },
921
+ nodes: [
922
+ { id: 'copy', bounds: { left: 0, top: 0, right: 343, bottom: 40 }, parentId: null, clipsChildren: false, textColor: '#1e88e5' },
923
+ ],
924
+ };
925
+ const previous = { elementIds: ['copy'], color: { copy: { text: '#1b1b1b' } } };
926
+ const reference = appearanceReferenceFromPreviousRender(previous);
927
+ const block = buildStructuralBlock(driftedDump, null, 2, undefined, reference);
928
+ expect(block.appearance?.baseline).toBe('previous_render');
929
+ const colorFacts = (block.appearance?.facts ?? []).filter((f) => f.kind === 'color');
930
+ expect(colorFacts).toEqual([
931
+ {
932
+ kind: 'color',
933
+ elementId: 'copy',
934
+ channel: 'text',
935
+ rendered: '#1e88e5',
936
+ reference: '#1b1b1b',
937
+ deltaE: ciede2000(hexToLab('#1e88e5'), hexToLab('#1b1b1b')),
938
+ },
939
+ ]);
940
+ expect(colorFacts[0].kind === 'color' && colorFacts[0].deltaE).toBeGreaterThan(10);
941
+ });
942
+ test('a correct fill with a wrong text color yields a text-channel fact only — the two never collapse (issue 02)', () => {
943
+ // One element carries both channels: fill matches the previous render, text
944
+ // drifts. Exactly one fact, channel:'text' — the correct fill produces none.
945
+ const dump = {
946
+ canvas: { left: 0, top: 0, right: 343, bottom: 72 },
947
+ nodes: [
948
+ { id: 'banner', bounds: { left: 0, top: 0, right: 343, bottom: 72 }, parentId: null, clipsChildren: false, fillColor: '#fff0ee', textColor: '#1e88e5' },
949
+ ],
950
+ };
951
+ const previous = {
952
+ elementIds: ['banner'],
953
+ color: { banner: { fill: '#fff0ee', text: '#1b1b1b' } },
954
+ };
955
+ const block = buildStructuralBlock(dump, null, 2, undefined, appearanceReferenceFromPreviousRender(previous));
956
+ const colorFacts = (block.appearance?.facts ?? []).filter((f) => f.kind === 'color');
957
+ expect(colorFacts).toEqual([
958
+ {
959
+ kind: 'color',
960
+ elementId: 'banner',
961
+ channel: 'text',
962
+ rendered: '#1e88e5',
963
+ reference: '#1b1b1b',
964
+ deltaE: ciede2000(hexToLab('#1e88e5'), hexToLab('#1b1b1b')),
965
+ },
966
+ ]);
967
+ });
873
968
  });
874
969
  // ---- Signal C: build the appearance reference from run params (issue 05) ----
875
970
  //
@@ -1059,6 +1154,22 @@ describe('embedded geometry stage parity', () => {
1059
1154
  };
1060
1155
  expect(embedded.buildStructuralBlock(dumpWithColor, null, 2, undefined, appearanceReference)).toEqual(buildStructuralBlock(dumpWithColor, null, 2, undefined, appearanceReference));
1061
1156
  });
1157
+ test('embedded buildStructuralBlock matches the module for a text-only color drift (issue 02)', () => {
1158
+ // Correct fill + wrong text: the embedded port must emit the same single
1159
+ // text-channel fact (no collapse), matching the typed module exactly.
1160
+ const dumpWithText = {
1161
+ canvas: { left: 0, top: 0, right: 343, bottom: 72 },
1162
+ nodes: [
1163
+ { id: 'banner', bounds: { left: 0, top: 0, right: 343, bottom: 72 }, parentId: null, clipsChildren: false, fillColor: '#fff0ee', textColor: '#1e88e5' },
1164
+ ],
1165
+ };
1166
+ const appearanceReference = {
1167
+ baseline: 'previous_render',
1168
+ elementIds: ['banner'],
1169
+ color: { banner: { fill: '#fff0ee', text: '#1b1b1b' } },
1170
+ };
1171
+ expect(embedded.buildStructuralBlock(dumpWithText, null, 2, undefined, appearanceReference)).toEqual(buildStructuralBlock(dumpWithText, null, 2, undefined, appearanceReference));
1172
+ });
1062
1173
  const parityCases = [
1063
1174
  { name: 'factsOnly', dump: HUB_PROMO_DUMP, ref: null },
1064
1175
  { name: 'withGeometry', dump: HUB_PROMO_DUMP, ref: HUB_PROMO_REFERENCE },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",