@invarn/cibuild 2.3.5 → 2.3.7

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.
@@ -801,6 +801,52 @@ describe('deriveSpanFacts — declared highlight extents vs rendered background
801
801
  { kind: 'span_mismatch', elementId: 'a', rendered: 'y', reference: 'right' },
802
802
  ]);
803
803
  });
804
+ // ── Foreground-color spans (partial-color emphasis), capability-gated. ──
805
+ test('a color span with the right extent but the wrong color yields span_color_mismatch', () => {
806
+ expect(deriveSpanFacts({ pillText: [{ start: 19, end: 30, text: 'special offer', color: '#ffffff' }] }, { pillText: { text: 'special offer', color: '#ffdc23' } }, true)).toEqual([
807
+ {
808
+ kind: 'span_color_mismatch',
809
+ elementId: 'pillText',
810
+ extent: 'special offer',
811
+ rendered: '#ffffff',
812
+ reference: '#ffdc23',
813
+ },
814
+ ]);
815
+ });
816
+ test('a color span matching extent AND color (case-insensitive) produces no fact', () => {
817
+ expect(deriveSpanFacts({ pillText: [{ start: 19, end: 30, text: 'special offer', color: '#FFDC23' }] }, { pillText: { text: 'special offer', color: '#ffdc23' } }, true)).toEqual([]);
818
+ });
819
+ test('a capable dump rendering NO span at all fails a color declaration (dropped emphasis)', () => {
820
+ expect(deriveSpanFacts({}, { pillText: { text: 'special offer', color: '#ffdc23' } }, true)).toEqual([
821
+ { kind: 'span_mismatch', elementId: 'pillText', rendered: '', reference: 'special offer' },
822
+ ]);
823
+ });
824
+ test('a color declaration against a color-blind dump stays silent (capability gate)', () => {
825
+ // Old emitter: no spanChannels marker → colorCapable false. Its silence
826
+ // is "cannot see", never "no color span rendered".
827
+ expect(deriveSpanFacts({}, { pillText: { text: 'special offer', color: '#ffdc23' } }, false)).toEqual([]);
828
+ expect(deriveSpanFacts({}, { pillText: { text: 'special offer', color: '#ffdc23' } })).toEqual([]);
829
+ });
830
+ test('a color-only rendered span never satisfies a BACKGROUND declaration (channel-scoped)', () => {
831
+ expect(deriveSpanFacts({ pillText: [{ start: 0, end: 5, text: 'offer', color: '#ffdc23' }] }, { pillText: { text: 'offer', background: '#ffe45e' } }, true)).toEqual([
832
+ { kind: 'span_mismatch', elementId: 'pillText', rendered: '', reference: 'offer' },
833
+ ]);
834
+ });
835
+ test('a declaration carrying BOTH channels needs both satisfied', () => {
836
+ const rendered = {
837
+ pillText: [{ start: 0, end: 5, text: 'offer', background: '#ffe45e', color: '#ffdc23' }],
838
+ };
839
+ expect(deriveSpanFacts(rendered, { pillText: { text: 'offer', background: '#ffe45e', color: '#ffdc23' } }, true)).toEqual([]);
840
+ expect(deriveSpanFacts(rendered, { pillText: { text: 'offer', background: '#ffe45e', color: '#1b1b1b' } }, true)).toEqual([
841
+ {
842
+ kind: 'span_color_mismatch',
843
+ elementId: 'pillText',
844
+ extent: 'offer',
845
+ rendered: '#ffdc23',
846
+ reference: '#1b1b1b',
847
+ },
848
+ ]);
849
+ });
804
850
  });
805
851
  describe('deriveLineFacts — declared line split vs rendered laid-out lines', () => {
806
852
  test('rendered lines matching the declared split produce no fact', () => {
@@ -12,12 +12,17 @@
12
12
  * Optional reference-provenance sidecar (manifest-driven multi-state path):
13
13
  * `"render_locale": "<locale>"` plus
14
14
  * `"provenance": { "<ScreenName>": { figmaNode, locale, resolved, exportedAt } }`.
15
- * When `render_locale` is present, each screen's reference is gated on its
16
- * provenance METADATA (never pixels/OCR): absent, unprovenanced,
17
- * `resolved == false`, or `locale != render_locale` → status
18
- * `unverifiable` for that screen, so a rotten reference fails loud instead
19
- * of yielding a confident-but-false PASS. Content differences (copy,
20
- * geometry) are NOT gated here they stay soft, judged findings.
15
+ * The gate arms whenever a screen's provenance is present (or, legacy, when
16
+ * `render_locale` is declared) and checks provenance METADATA only (never
17
+ * pixels/OCR). An absent reference — or an unprovenanced screen while
18
+ * `render_locale` is declared is FATAL: status `unverifiable`, failing
19
+ * loud instead of yielding a confident-but-false PASS. `resolved == false`
20
+ * (placeholder copy) or `locale != render_locale` downgrade ONLY the copy
21
+ * channel: the screen renders and is judged on geometry, colors, typeface,
22
+ * spans, and lines, while copy comparisons are skipped and reported as a
23
+ * per-screen `copy_advisory` (`copy_unverifiable` + reason) in
24
+ * protocol-result.json. Content differences (copy, geometry) are NOT gated
25
+ * here — they stay soft, judged findings.
21
26
  * 2. Extracts and validates the shipped Gradle project shipped as
22
27
  * `.ci/inputs/package.tar.gz` (single project root carrying a settings
23
28
  * script, bounded extracted size, safe member paths).
@@ -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;AAMpE,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,iDAAiD;AACjD,eAAO,MAAM,eAAe,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAAwB,CAAC;AAE9D,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,6BAA8B,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,eAA0B,CAAC;AAElE,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;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAq+BD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAqBrF;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;CA0EpB"}
1
+ {"version":3,"file":"ui-fidelity-render-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;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;AAMpE,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,iDAAiD;AACjD,eAAO,MAAM,eAAe,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAAwB,CAAC;AAE9D,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,6BAA8B,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,eAA0B,CAAC;AAElE,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;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAkiCD;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAqBrF;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;CA0EpB"}
@@ -12,12 +12,17 @@
12
12
  * Optional reference-provenance sidecar (manifest-driven multi-state path):
13
13
  * `"render_locale": "<locale>"` plus
14
14
  * `"provenance": { "<ScreenName>": { figmaNode, locale, resolved, exportedAt } }`.
15
- * When `render_locale` is present, each screen's reference is gated on its
16
- * provenance METADATA (never pixels/OCR): absent, unprovenanced,
17
- * `resolved == false`, or `locale != render_locale` → status
18
- * `unverifiable` for that screen, so a rotten reference fails loud instead
19
- * of yielding a confident-but-false PASS. Content differences (copy,
20
- * geometry) are NOT gated here they stay soft, judged findings.
15
+ * The gate arms whenever a screen's provenance is present (or, legacy, when
16
+ * `render_locale` is declared) and checks provenance METADATA only (never
17
+ * pixels/OCR). An absent reference — or an unprovenanced screen while
18
+ * `render_locale` is declared is FATAL: status `unverifiable`, failing
19
+ * loud instead of yielding a confident-but-false PASS. `resolved == false`
20
+ * (placeholder copy) or `locale != render_locale` downgrade ONLY the copy
21
+ * channel: the screen renders and is judged on geometry, colors, typeface,
22
+ * spans, and lines, while copy comparisons are skipped and reported as a
23
+ * per-screen `copy_advisory` (`copy_unverifiable` + reason) in
24
+ * protocol-result.json. Content differences (copy, geometry) are NOT gated
25
+ * here — they stay soft, judged findings.
21
26
  * 2. Extracts and validates the shipped Gradle project shipped as
22
27
  * `.ci/inputs/package.tar.gz` (single project root carrying a settings
23
28
  * script, bounded extracted size, safe member paths).
@@ -207,44 +212,80 @@ function setUnverifiable(entry, code, message) {
207
212
  entry.error = { code: code, message: sanitizeMessage(message) };
208
213
  }
209
214
 
210
- // Inlined twin of src/yaml/steps/fidelity-reference-provenance.ts (this script
215
+ // Inlined twins of src/yaml/steps/fidelity-reference-provenance.ts (this script
211
216
  // ships self-contained and cannot import it). The render-step integration tests
212
- // exercise BOTH against the same fixtures so they cannot silently drift. Returns
213
- // a { code, message } fault, or null when the reference is verifiable.
214
- function provenanceFault(screen, provenance, renderLocale, referenceExists) {
215
- var hasProvenance =
217
+ // exercise BOTH against the same fixtures so they cannot silently drift.
218
+
219
+ // Provenance is only "present" when the fields the gate reasons over are there.
220
+ // A partial object (e.g. a hand-dropped { figmaNode }) is treated as no
221
+ // provenance — verifiable is earned only through the full export path.
222
+ function hasReferenceProvenance(provenance) {
223
+ return (
216
224
  provenance != null &&
217
225
  typeof provenance === 'object' &&
218
226
  typeof provenance.locale === 'string' &&
219
- typeof provenance.resolved === 'boolean';
220
- if (!hasProvenance) {
227
+ typeof provenance.resolved === 'boolean'
228
+ );
229
+ }
230
+
231
+ // Assesses one screen's reference. Returns { fault, copyAdvisory }: a fatal
232
+ // fault makes the screen unverifiable; a copy advisory downgrades only the
233
+ // copy channel (the screen still renders and is judged on geometry, colors,
234
+ // typeface, spans, and lines). An absent reference wins over a downgrade —
235
+ // there is nothing to judge against.
236
+ function assessReferenceProvenance(screen, provenance, renderLocale, referenceExists) {
237
+ if (!hasReferenceProvenance(provenance)) {
238
+ if (renderLocale !== null) {
239
+ return {
240
+ fault: {
241
+ code: 'NO_PROVENANCE',
242
+ message:
243
+ 'reference for ' + screen + ' has no export provenance; a reference is ' +
244
+ 'verifiable only when exported through the deterministic path',
245
+ },
246
+ copyAdvisory: null,
247
+ };
248
+ }
249
+ // Unarmed: the caller keeps legacy behaviour (including its own
250
+ // reference-existence check).
251
+ return { fault: null, copyAdvisory: null };
252
+ }
253
+ if (!referenceExists) {
221
254
  return {
222
- code: 'NO_PROVENANCE',
223
- message:
224
- 'reference for ' + screen + ' has no export provenance; a reference is ' +
225
- 'verifiable only when exported through the deterministic path',
255
+ fault: {
256
+ code: 'REFERENCE_ABSENT',
257
+ message: 'reference image for ' + screen + ' is absent',
258
+ },
259
+ copyAdvisory: null,
226
260
  };
227
261
  }
228
262
  if (provenance.resolved === false) {
229
263
  return {
230
- code: 'UNRESOLVED_REFERENCE',
231
- message:
232
- 'reference for ' + screen + ' still has unresolved copy placeholders ' +
233
- '(resolved=false) — export it with the copy filled in',
264
+ fault: null,
265
+ copyAdvisory: {
266
+ code: 'copy_unverifiable',
267
+ reason: 'unresolved_placeholders',
268
+ message:
269
+ 'reference for ' + screen + ' carries unresolved copy placeholders ' +
270
+ '(resolved=false); copy comparisons are skipped — geometry, color, ' +
271
+ 'typeface, span, and line judging still hold',
272
+ },
234
273
  };
235
274
  }
236
- if (provenance.locale !== renderLocale) {
275
+ if (renderLocale !== null && provenance.locale !== renderLocale) {
237
276
  return {
238
- code: 'LOCALE_MISMATCH',
239
- message:
240
- 'reference for ' + screen + ' is locale "' + provenance.locale +
241
- '" but the render locale is "' + renderLocale + '"',
277
+ fault: null,
278
+ copyAdvisory: {
279
+ code: 'copy_unverifiable',
280
+ reason: 'locale_mismatch',
281
+ message:
282
+ 'reference for ' + screen + ' is locale "' + provenance.locale +
283
+ '" but the render locale is "' + renderLocale + '"; copy comparisons ' +
284
+ 'are skipped — geometry, color, typeface, span, and line judging still hold',
285
+ },
242
286
  };
243
287
  }
244
- if (!referenceExists) {
245
- return { code: 'REFERENCE_ABSENT', message: 'reference image for ' + screen + ' is absent' };
246
- }
247
- return null;
288
+ return { fault: null, copyAdvisory: null };
248
289
  }
249
290
 
250
291
  function outputTail(result) {
@@ -808,6 +849,7 @@ function main() {
808
849
  screen: screen,
809
850
  status: 'render_failed',
810
851
  error: null,
852
+ copy_advisory: null,
811
853
  reference_image_path: null,
812
854
  rendered_image_path: null,
813
855
  aligned_image_path: null,
@@ -859,22 +901,46 @@ function main() {
859
901
  }
860
902
  var referenceExists = !!(sourceStat && sourceStat.isFile());
861
903
 
862
- if (renderLocale) {
863
- // Provenance enforcement: a reference that is absent, unprovenanced,
864
- // unresolved, or in the wrong locale is UNVERIFIABLE (hard, loud) not a
865
- // render_failed and never a silent PASS.
866
- var fault = provenanceFault(
904
+ // Provenance assessment, armed whenever this screen's provenance rides in
905
+ // params (the references/ snapshot lifts it automatically) or the run
906
+ // declares a render locale (legacy arming, which keeps NO_PROVENANCE
907
+ // fatal). Fatal faults (reference absent, no provenance while armed) make
908
+ // the screen UNVERIFIABLE — hard, loud, never a silent PASS. Unresolved
909
+ // placeholders and locale mismatches downgrade ONLY the copy channel: the
910
+ // screen renders and is judged on geometry, colors, typeface, spans, and
911
+ // lines, while copy comparisons are skipped and reported as a structured
912
+ // copy_unverifiable advisory.
913
+ var screenProvenance = provenanceMap[entry.screen];
914
+ if (renderLocale !== null || hasReferenceProvenance(screenProvenance)) {
915
+ var assessment = assessReferenceProvenance(
867
916
  entry.screen,
868
- provenanceMap[entry.screen],
917
+ screenProvenance,
869
918
  renderLocale,
870
919
  referenceExists
871
920
  );
872
- if (fault) {
873
- setUnverifiable(entry, fault.code, fault.message);
921
+ if (assessment.fault) {
922
+ setUnverifiable(entry, assessment.fault.code, assessment.fault.message);
874
923
  return;
875
924
  }
925
+ if (assessment.copyAdvisory) {
926
+ entry.copy_advisory = {
927
+ code: assessment.copyAdvisory.code,
928
+ reason: assessment.copyAdvisory.reason,
929
+ message: sanitizeMessage(assessment.copyAdvisory.message),
930
+ };
931
+ // Copy comparisons are unverifiable for this screen: drop its
932
+ // reference text so no copy facts (text_mismatch / text_missing) are
933
+ // derived downstream.
934
+ if (referenceText && referenceText[entry.screen]) {
935
+ delete referenceText[entry.screen];
936
+ }
937
+ log(
938
+ entry.screen + ': copy channel downgraded (' +
939
+ assessment.copyAdvisory.reason + '), non-copy judging proceeds'
940
+ );
941
+ }
876
942
  } else if (!referenceExists) {
877
- // Legacy (no render locale declared): an absent reference stays a
943
+ // Legacy (no provenance, no render locale): an absent reference stays a
878
944
  // render_failed REFERENCE_MISSING, exactly as before.
879
945
  setError(
880
946
  entry,
@@ -540,7 +540,7 @@ describe('android render runtime — render + trim (happy path)', () => {
540
540
  expect(doc.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
541
541
  });
542
542
  });
543
- describe('android render runtime — reference provenance (UNVERIFIABLE)', () => {
543
+ describe('android render runtime — reference provenance', () => {
544
544
  const GOOD_PROV = {
545
545
  figmaNode: '12:345',
546
546
  locale: 'en',
@@ -554,7 +554,7 @@ describe('android render runtime — reference provenance (UNVERIFIABLE)', () =>
554
554
  stageArchive(project, gradleProjectEntries());
555
555
  return project;
556
556
  }
557
- test('a reference with no provenance is unverifiable', async () => {
557
+ test('a reference with no provenance stays unverifiable when a render locale is declared', async () => {
558
558
  const project = provProject({
559
559
  screens: { XProof: 'x.png' },
560
560
  render_locale: 'en',
@@ -566,31 +566,62 @@ describe('android render runtime — reference provenance (UNVERIFIABLE)', () =>
566
566
  expect(screen?.status).toBe('unverifiable');
567
567
  expect(screen?.error?.code).toBe('NO_PROVENANCE');
568
568
  });
569
- test('a placeholder reference (resolved=false) is unverifiable', async () => {
569
+ test('a placeholder reference (resolved=false) renders and downgrades only the copy channel', async () => {
570
570
  const project = provProject({
571
571
  screens: { XProof: 'x.png' },
572
572
  render_locale: 'en',
573
573
  provenance: { XProof: { ...GOOD_PROV, resolved: false } },
574
574
  });
575
575
  writeReference(project, 'x.png');
576
- runScript(project, await buildScript());
576
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
577
+ expect(run.status).toBe(0);
577
578
  const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
578
- expect(screen?.status).toBe('unverifiable');
579
- expect(screen?.error?.code).toBe('UNRESOLVED_REFERENCE');
579
+ expect(screen?.status).toBe('rendered');
580
+ expect(screen?.error).toBeNull();
581
+ expect(screen?.copy_advisory?.code).toBe('copy_unverifiable');
582
+ expect(screen?.copy_advisory?.reason).toBe('unresolved_placeholders');
583
+ });
584
+ test('the placeholder downgrade is identical without a render_locale (same input, same verdict)', async () => {
585
+ // Live evidence bld_5f8dc6330c9e61f578297f00 / bld_c92bc240ab8aac7dfe258b27:
586
+ // the same resolved:false reference was judged in one run and refused in
587
+ // the other purely because of the optional render_locale param.
588
+ const project = provProject({
589
+ screens: { XProof: 'x.png' },
590
+ provenance: { XProof: { ...GOOD_PROV, resolved: false } },
591
+ });
592
+ writeReference(project, 'x.png');
593
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
594
+ expect(run.status).toBe(0);
595
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
596
+ expect(screen?.status).toBe('rendered');
597
+ expect(screen?.copy_advisory?.reason).toBe('unresolved_placeholders');
580
598
  });
581
- test('an Italian reference against an English render is unverifiable', async () => {
599
+ test('an Italian reference against an English render downgrades the copy channel', async () => {
582
600
  const project = provProject({
583
601
  screens: { XProof: 'x.png' },
584
602
  render_locale: 'en',
585
603
  provenance: { XProof: { ...GOOD_PROV, locale: 'it' } },
586
604
  });
587
605
  writeReference(project, 'x.png');
588
- runScript(project, await buildScript());
606
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
607
+ expect(run.status).toBe(0);
589
608
  const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
590
- expect(screen?.status).toBe('unverifiable');
591
- expect(screen?.error?.code).toBe('LOCALE_MISMATCH');
609
+ expect(screen?.status).toBe('rendered');
610
+ expect(screen?.copy_advisory?.code).toBe('copy_unverifiable');
611
+ expect(screen?.copy_advisory?.reason).toBe('locale_mismatch');
592
612
  });
593
- test('an absent reference with good provenance is unverifiable', async () => {
613
+ test('locale comparison requires a declared render_locale its absence skips only that', async () => {
614
+ const project = provProject({
615
+ screens: { XProof: 'x.png' },
616
+ provenance: { XProof: { ...GOOD_PROV, locale: 'it' } },
617
+ });
618
+ writeReference(project, 'x.png');
619
+ runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
620
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
621
+ expect(screen?.status).toBe('rendered');
622
+ expect(screen?.copy_advisory ?? null).toBeNull();
623
+ });
624
+ test('an absent reference with good provenance stays fatal (REFERENCE_ABSENT)', async () => {
594
625
  const project = provProject({
595
626
  screens: { XProof: 'x.png' },
596
627
  render_locale: 'en',
@@ -602,7 +633,17 @@ describe('android render runtime — reference provenance (UNVERIFIABLE)', () =>
602
633
  expect(screen?.status).toBe('unverifiable');
603
634
  expect(screen?.error?.code).toBe('REFERENCE_ABSENT');
604
635
  });
605
- test('a good provenanced reference renders normally (a content diff is left to the judge)', async () => {
636
+ test('an absent reference is fatal even without a render_locale (armed by provenance presence)', async () => {
637
+ const project = provProject({
638
+ screens: { XProof: 'x.png' },
639
+ provenance: { XProof: GOOD_PROV },
640
+ });
641
+ runScript(project, await buildScript());
642
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
643
+ expect(screen?.status).toBe('unverifiable');
644
+ expect(screen?.error?.code).toBe('REFERENCE_ABSENT');
645
+ });
646
+ test('a good provenanced reference renders normally with no copy advisory', async () => {
606
647
  const project = provProject({
607
648
  screens: { XProof: 'x.png' },
608
649
  render_locale: 'en',
@@ -614,8 +655,9 @@ describe('android render runtime — reference provenance (UNVERIFIABLE)', () =>
614
655
  const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
615
656
  expect(screen?.status).toBe('rendered');
616
657
  expect(screen?.error).toBeNull();
658
+ expect(screen?.copy_advisory ?? null).toBeNull();
617
659
  });
618
- test('independent verdicts: a sibling stays rendered while one state is unverifiable', async () => {
660
+ test('independent verdicts: a clean sibling carries no advisory while the placeholder state does', async () => {
619
661
  const project = provProject({
620
662
  screens: { XProof: 'x.png', YProof: 'y.png' },
621
663
  render_locale: 'en',
@@ -623,19 +665,52 @@ describe('android render runtime — reference provenance (UNVERIFIABLE)', () =>
623
665
  });
624
666
  writeReference(project, 'x.png');
625
667
  writeReference(project, 'y.png');
626
- runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
668
+ runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof,YProof' });
627
669
  const doc = readResult(project);
628
- expect(doc.screens.find((s) => s.screen === 'XProof')?.status).toBe('rendered');
670
+ const x = doc.screens.find((s) => s.screen === 'XProof');
671
+ expect(x?.status).toBe('rendered');
672
+ expect(x?.copy_advisory ?? null).toBeNull();
629
673
  const y = doc.screens.find((s) => s.screen === 'YProof');
630
- expect(y?.status).toBe('unverifiable');
631
- expect(y?.error?.code).toBe('UNRESOLVED_REFERENCE');
674
+ expect(y?.status).toBe('rendered');
675
+ expect(y?.copy_advisory?.reason).toBe('unresolved_placeholders');
632
676
  });
633
- test('without a render_locale, provenance is not enforced (legacy behaviour)', async () => {
677
+ test('without provenance and without a render_locale, nothing is enforced (legacy behaviour)', async () => {
634
678
  const project = provProject({ screens: { XProof: 'x.png' } });
635
679
  writeReference(project, 'x.png');
636
680
  runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
637
681
  const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
638
682
  expect(screen?.status).toBe('rendered');
683
+ expect(screen?.copy_advisory ?? null).toBeNull();
684
+ });
685
+ test('a downgraded copy channel derives no copy facts while geometry facts still hold', async () => {
686
+ // The fake layout dump carries a text node ("tag": "TAG") that clips its
687
+ // parent. With reference text that mismatches, a clean run derives a
688
+ // text_mismatch fact — a downgraded run must derive NONE while keeping
689
+ // the clipped_by_parent geometry fact.
690
+ const factKinds = async (resolved) => {
691
+ const project = provProject({
692
+ screens: { XProof: 'x.png' },
693
+ render_locale: 'en',
694
+ provenance: { XProof: { ...GOOD_PROV, resolved } },
695
+ reference_text: { XProof: { tag: 'DIFFERENT' } },
696
+ });
697
+ writeReference(project, 'x.png');
698
+ runScript(project, await buildScript(), {
699
+ FAKE_ROBORAZZI_SCREENS: 'XProof',
700
+ FAKE_ROBORAZZI_LAYOUT: '1',
701
+ });
702
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
703
+ const structural = (screen?.structural?.facts ?? []).map((f) => f.kind);
704
+ const appearance = (screen?.structural?.appearance?.facts ?? []).map((f) => f.kind);
705
+ return [...structural, ...appearance];
706
+ };
707
+ const clean = await factKinds(true);
708
+ expect(clean).toContain('text_mismatch');
709
+ expect(clean).toContain('clipped_by_parent');
710
+ const downgraded = await factKinds(false);
711
+ expect(downgraded).not.toContain('text_mismatch');
712
+ expect(downgraded).not.toContain('text_missing');
713
+ expect(downgraded).toContain('clipped_by_parent');
639
714
  });
640
715
  });
641
716
  describe('android render runtime — error isolation', () => {
@@ -8,6 +8,16 @@
8
8
  * 1. Reads `.ci/inputs/params.json`
9
9
  * ({ "screens": { "<ViewTypeName>": "<referenceFileBasename>" } });
10
10
  * reference images live at `.ci/inputs/<basename>`.
11
+ * Optional reference-provenance sidecar: `"render_locale": "<locale>"`
12
+ * plus `"provenance": { "<ViewTypeName>": { figmaNode, locale, resolved,
13
+ * exportedAt } }`. The gate arms per screen from provenance presence and
14
+ * checks METADATA only (never pixels/OCR): an absent reference is FATAL
15
+ * (status `unverifiable`, REFERENCE_ABSENT); `resolved == false` or
16
+ * `locale != render_locale` downgrade ONLY the copy channel — the screen
17
+ * renders and is judged on every non-copy channel, and the downgrade
18
+ * rides as a per-screen `copy_advisory` (`copy_unverifiable` + reason).
19
+ * Unprovenanced screens keep legacy behaviour (no NO_PROVENANCE arming
20
+ * on iOS).
11
21
  * 2. Synthesizes a throwaway SwiftPM harness package that depends on the
12
22
  * user's package and contains ONE executable target per screen, so a
13
23
  * compile failure for one screen (unknown view type, unavailable init)
@@ -1 +1 @@
1
- {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;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;AAIpE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACrC,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iDAAiD;AACjD,eAAO,MAAM,eAAe,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,EAAE,aAAsB,CAAC;AAE5D,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,6BAA8B,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,EAAE,eAA0B,CAAC;AAElE,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,QAAmB,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAQzD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IACzF,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IAC7D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;CAC/E;AAmnCD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,qBAAqB,CAShE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAkBvE;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAoCpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAmFpB"}
1
+ {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;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;AAIpE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;IACrC,mFAAmF;IACnF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAE7C,mFAAmF;AACnF,eAAO,MAAM,aAAa,IAAI,CAAC;AAE/B,iDAAiD;AACjD,eAAO,MAAM,eAAe,6BAA8B,CAAC;AAC3D,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7D,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,EAAE,aAAsB,CAAC;AAE5D,mDAAmD;AACnD,eAAO,MAAM,iBAAiB,6BAA8B,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEjE,gFAAgF;AAChF,eAAO,MAAM,wBAAwB,EAAE,eAA0B,CAAC;AAElE,+EAA+E;AAC/E,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AAEzD;;;;;;;GAOG;AACH,eAAO,MAAM,2BAA2B,QAAmB,CAAC;AAE5D,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAWzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAQzD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAED,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1C,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IACzF,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;IAC7D,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,CAAC;CAC/E;AAuuCD;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,qBAAqB,CAShE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAkBvE;AAED;;;;GAIG;AACH,qBAAa,4BAA6B,SAAQ,gBAAgB;IAChE,yBAAyB,CACvB,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,qBAAqB,EAAE;IAoCpB,OAAO,CACX,MAAM,EAAE,sBAAsB,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC5B,OAAO,EAAE,QAAQ,GAChB,OAAO,CAAC,OAAO,CAAC;CAmFpB"}