@invarn/cibuild 2.3.6 → 2.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +221 -44
- package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts +54 -12
- package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts.map +1 -1
- package/dist/src/yaml/steps/fidelity-reference-provenance.js +74 -32
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts +14 -6
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts.map +1 -1
- package/dist/src/yaml/steps/fidelity-reference-provenance.test.js +92 -28
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +11 -6
- package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render-android.js +105 -39
- package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +93 -18
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts +10 -0
- package/dist/src/yaml/steps/ui-fidelity-render.d.ts.map +1 -1
- package/dist/src/yaml/steps/ui-fidelity-render.js +127 -1
- package/dist/src/yaml/steps/ui-fidelity-render.test.js +122 -0
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
16
|
-
* provenance METADATA (never
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
19
|
-
* of yielding a confident-but-false PASS.
|
|
20
|
-
*
|
|
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
|
|
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.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
'reference for ' + screen + '
|
|
225
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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('
|
|
579
|
-
expect(screen?.error
|
|
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
|
|
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('
|
|
591
|
-
expect(screen?.
|
|
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('
|
|
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('
|
|
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
|
|
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
|
-
|
|
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('
|
|
631
|
-
expect(y?.
|
|
674
|
+
expect(y?.status).toBe('rendered');
|
|
675
|
+
expect(y?.copy_advisory?.reason).toBe('unresolved_placeholders');
|
|
632
676
|
});
|
|
633
|
-
test('without a render_locale,
|
|
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
|
|
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"}
|
|
@@ -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
|
-
|
|
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',
|