@invarn/cibuild 2.3.6 → 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.
@@ -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":"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"}
1
+ {"version":3,"file":"ui-fidelity-render.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render.ts"],"names":[],"mappings":"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"}
@@ -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)
@@ -317,6 +327,77 @@ function setError(entry, code, message) {
317
327
  entry.error = { code: code, message: sanitizeMessage(message) };
318
328
  }
319
329
 
330
+ // A reference the gate cannot honestly judge at all: a hard, mechanical
331
+ // metadata verdict (never pixels/OCR), distinct from render_failed so a rotten
332
+ // reference fails loud instead of producing a confident-but-false PASS.
333
+ function setUnverifiable(entry, code, message) {
334
+ entry.status = 'unverifiable';
335
+ entry.error = { code: code, message: sanitizeMessage(message) };
336
+ }
337
+
338
+ // Inlined twins of src/yaml/steps/fidelity-reference-provenance.ts (this script
339
+ // ships self-contained and cannot import it). The render-step integration tests
340
+ // exercise BOTH against the same fixtures so they cannot silently drift. The
341
+ // iOS gate arms ONLY from provenance presence — the step never refused
342
+ // unprovenanced screens, so NO_PROVENANCE (an Android-legacy arming via
343
+ // render_locale) is deliberately not wired here.
344
+
345
+ // Provenance is only "present" when the fields the gate reasons over are there.
346
+ // A partial object (e.g. a hand-dropped { figmaNode }) is treated as no
347
+ // provenance — verifiable is earned only through the full export path.
348
+ function hasReferenceProvenance(provenance) {
349
+ return (
350
+ provenance != null &&
351
+ typeof provenance === 'object' &&
352
+ typeof provenance.locale === 'string' &&
353
+ typeof provenance.resolved === 'boolean'
354
+ );
355
+ }
356
+
357
+ // Assesses one provenanced screen's reference. Returns { fault, copyAdvisory }:
358
+ // a fatal fault makes the screen unverifiable; a copy advisory downgrades only
359
+ // the copy channel (the screen still renders and is judged on every non-copy
360
+ // channel). An absent reference wins over a downgrade — there is nothing to
361
+ // judge against.
362
+ function assessReferenceProvenance(screen, provenance, renderLocale, referenceExists) {
363
+ if (!referenceExists) {
364
+ return {
365
+ fault: {
366
+ code: 'REFERENCE_ABSENT',
367
+ message: 'reference image for ' + screen + ' is absent',
368
+ },
369
+ copyAdvisory: null,
370
+ };
371
+ }
372
+ if (provenance.resolved === false) {
373
+ return {
374
+ fault: null,
375
+ copyAdvisory: {
376
+ code: 'copy_unverifiable',
377
+ reason: 'unresolved_placeholders',
378
+ message:
379
+ 'reference for ' + screen + ' carries unresolved copy placeholders ' +
380
+ '(resolved=false); copy comparisons are skipped — non-copy judging ' +
381
+ 'still holds',
382
+ },
383
+ };
384
+ }
385
+ if (renderLocale !== null && provenance.locale !== renderLocale) {
386
+ return {
387
+ fault: null,
388
+ copyAdvisory: {
389
+ code: 'copy_unverifiable',
390
+ reason: 'locale_mismatch',
391
+ message:
392
+ 'reference for ' + screen + ' is locale "' + provenance.locale +
393
+ '" but the render locale is "' + renderLocale + '"; copy comparisons ' +
394
+ 'are skipped — non-copy judging still holds',
395
+ },
396
+ };
397
+ }
398
+ return { fault: null, copyAdvisory: null };
399
+ }
400
+
320
401
  function outputTail(result) {
321
402
  var combined = ((result.stdout || '') + '\n' + (result.stderr || '')).trim();
322
403
  if (combined.length > 2000) combined = '...' + combined.slice(-2000);
@@ -488,12 +569,25 @@ function main() {
488
569
  return 1;
489
570
  }
490
571
 
572
+ // Reference-provenance sidecar (optional): per-screen export provenance plus
573
+ // the run's declared render locale. The gate arms per screen from provenance
574
+ // presence; the locale is only needed for the locale comparison itself.
575
+ var renderLocale =
576
+ typeof params.render_locale === 'string' && params.render_locale !== ''
577
+ ? params.render_locale
578
+ : null;
579
+ var provenanceMap =
580
+ params.provenance && typeof params.provenance === 'object' && !Array.isArray(params.provenance)
581
+ ? params.provenance
582
+ : {};
583
+
491
584
  var screens = Object.keys(params.screens);
492
585
  currentEntries = screens.map(function (screen) {
493
586
  return {
494
587
  screen: screen,
495
588
  status: 'render_failed',
496
589
  error: null,
590
+ copy_advisory: null,
497
591
  reference_image_path: null,
498
592
  rendered_image_path: null,
499
593
  };
@@ -536,7 +630,39 @@ function main() {
536
630
  } catch (statError) {
537
631
  sourceStat = null;
538
632
  }
539
- if (!sourceStat || !sourceStat.isFile()) {
633
+ var referenceExists = !!(sourceStat && sourceStat.isFile());
634
+
635
+ var screenProvenance = provenanceMap[entry.screen];
636
+ if (hasReferenceProvenance(screenProvenance)) {
637
+ // Provenance assessment (armed by provenance presence): an absent
638
+ // reference is FATAL (unverifiable — never a silent PASS); unresolved
639
+ // placeholders and locale mismatches downgrade ONLY the copy channel —
640
+ // the screen renders and is judged on every non-copy channel, and the
641
+ // downgrade rides in the result as a structured copy_advisory.
642
+ var assessment = assessReferenceProvenance(
643
+ entry.screen,
644
+ screenProvenance,
645
+ renderLocale,
646
+ referenceExists
647
+ );
648
+ if (assessment.fault) {
649
+ setUnverifiable(entry, assessment.fault.code, assessment.fault.message);
650
+ return;
651
+ }
652
+ if (assessment.copyAdvisory) {
653
+ entry.copy_advisory = {
654
+ code: assessment.copyAdvisory.code,
655
+ reason: assessment.copyAdvisory.reason,
656
+ message: sanitizeMessage(assessment.copyAdvisory.message),
657
+ };
658
+ log(
659
+ entry.screen + ': copy channel downgraded (' +
660
+ assessment.copyAdvisory.reason + '), non-copy judging proceeds'
661
+ );
662
+ }
663
+ } else if (!referenceExists) {
664
+ // Legacy (no provenance for this screen): an absent reference stays a
665
+ // render_failed REFERENCE_MISSING, exactly as before.
540
666
  setError(
541
667
  entry,
542
668
  'REFERENCE_MISSING',