@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.
- package/dist/cli.cjs +305 -60
- 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/structural-facts.d.ts +40 -11
- package/dist/src/yaml/steps/structural-facts.d.ts.map +1 -1
- package/dist/src/yaml/steps/structural-facts.js +0 -0
- package/dist/src/yaml/steps/structural-facts.test.js +46 -0
- 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
|
@@ -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
|
-
*
|
|
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).
|
|
@@ -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
|
|
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
|
-
*
|
|
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":"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"}
|