@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
|
@@ -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',
|
|
@@ -529,6 +529,7 @@ describe('render script runtime', () => {
|
|
|
529
529
|
screen: 'HomeView',
|
|
530
530
|
status: 'rendered',
|
|
531
531
|
error: null,
|
|
532
|
+
copy_advisory: null,
|
|
532
533
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
533
534
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
534
535
|
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
@@ -537,6 +538,7 @@ describe('render script runtime', () => {
|
|
|
537
538
|
screen: 'SettingsView',
|
|
538
539
|
status: 'rendered',
|
|
539
540
|
error: null,
|
|
541
|
+
copy_advisory: null,
|
|
540
542
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
541
543
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
542
544
|
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
@@ -927,6 +929,7 @@ describe('render script runtime (package_source: repo)', () => {
|
|
|
927
929
|
screen: 'HomeView',
|
|
928
930
|
status: 'rendered',
|
|
929
931
|
error: null,
|
|
932
|
+
copy_advisory: null,
|
|
930
933
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
931
934
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
932
935
|
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
@@ -1107,6 +1110,7 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1107
1110
|
screen: 'HomeView',
|
|
1108
1111
|
status: 'rendered',
|
|
1109
1112
|
error: null,
|
|
1113
|
+
copy_advisory: null,
|
|
1110
1114
|
reference_image_path: 'ui-fidelity/references/HomeView.png',
|
|
1111
1115
|
rendered_image_path: 'ui-fidelity/rendered/HomeView.png',
|
|
1112
1116
|
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
@@ -1121,6 +1125,7 @@ describe('render script runtime (package_source: inputs)', () => {
|
|
|
1121
1125
|
screen: 'SettingsView',
|
|
1122
1126
|
status: 'rendered',
|
|
1123
1127
|
error: null,
|
|
1128
|
+
copy_advisory: null,
|
|
1124
1129
|
reference_image_path: 'ui-fidelity/references/SettingsView.png',
|
|
1125
1130
|
rendered_image_path: 'ui-fidelity/rendered/SettingsView.png',
|
|
1126
1131
|
source_digest: expect.stringMatching(/^[0-9a-f]{64}$/),
|
|
@@ -1580,4 +1585,121 @@ describe('reference_source: repo', () => {
|
|
|
1580
1585
|
expect(ghost?.error?.code).toBe('REFERENCE_MISSING');
|
|
1581
1586
|
});
|
|
1582
1587
|
});
|
|
1588
|
+
describe('render script runtime — reference provenance', () => {
|
|
1589
|
+
const GOOD_PROV = {
|
|
1590
|
+
figmaNode: '12:345',
|
|
1591
|
+
locale: 'en',
|
|
1592
|
+
resolved: true,
|
|
1593
|
+
exportedAt: '2026-06-18T10:00:00Z',
|
|
1594
|
+
};
|
|
1595
|
+
test('a placeholder reference (resolved=false) renders and downgrades only the copy channel', async () => {
|
|
1596
|
+
const project = makeProject({
|
|
1597
|
+
screens: { HomeView: 'home.png' },
|
|
1598
|
+
render_locale: 'en',
|
|
1599
|
+
provenance: { HomeView: { ...GOOD_PROV, resolved: false } },
|
|
1600
|
+
});
|
|
1601
|
+
writeReference(project, 'home.png');
|
|
1602
|
+
const run = runScript(project, await buildScript(project));
|
|
1603
|
+
expect(run.status).toBe(0);
|
|
1604
|
+
const home = readResult(project).screens[0];
|
|
1605
|
+
expect(home.status).toBe('rendered');
|
|
1606
|
+
expect(home.error).toBeNull();
|
|
1607
|
+
expect(home.copy_advisory?.code).toBe('copy_unverifiable');
|
|
1608
|
+
expect(home.copy_advisory?.reason).toBe('unresolved_placeholders');
|
|
1609
|
+
});
|
|
1610
|
+
test('the placeholder downgrade is identical without a render_locale (same input, same verdict)', async () => {
|
|
1611
|
+
const project = makeProject({
|
|
1612
|
+
screens: { HomeView: 'home.png' },
|
|
1613
|
+
provenance: { HomeView: { ...GOOD_PROV, resolved: false } },
|
|
1614
|
+
});
|
|
1615
|
+
writeReference(project, 'home.png');
|
|
1616
|
+
const run = runScript(project, await buildScript(project));
|
|
1617
|
+
expect(run.status).toBe(0);
|
|
1618
|
+
const home = readResult(project).screens[0];
|
|
1619
|
+
expect(home.status).toBe('rendered');
|
|
1620
|
+
expect(home.copy_advisory?.reason).toBe('unresolved_placeholders');
|
|
1621
|
+
});
|
|
1622
|
+
test('an Italian reference against an English render downgrades the copy channel', async () => {
|
|
1623
|
+
const project = makeProject({
|
|
1624
|
+
screens: { HomeView: 'home.png' },
|
|
1625
|
+
render_locale: 'en',
|
|
1626
|
+
provenance: { HomeView: { ...GOOD_PROV, locale: 'it' } },
|
|
1627
|
+
});
|
|
1628
|
+
writeReference(project, 'home.png');
|
|
1629
|
+
const run = runScript(project, await buildScript(project));
|
|
1630
|
+
expect(run.status).toBe(0);
|
|
1631
|
+
const home = readResult(project).screens[0];
|
|
1632
|
+
expect(home.status).toBe('rendered');
|
|
1633
|
+
expect(home.copy_advisory?.reason).toBe('locale_mismatch');
|
|
1634
|
+
});
|
|
1635
|
+
test('locale comparison requires a declared render_locale — its absence skips only that', async () => {
|
|
1636
|
+
const project = makeProject({
|
|
1637
|
+
screens: { HomeView: 'home.png' },
|
|
1638
|
+
provenance: { HomeView: { ...GOOD_PROV, locale: 'it' } },
|
|
1639
|
+
});
|
|
1640
|
+
writeReference(project, 'home.png');
|
|
1641
|
+
const run = runScript(project, await buildScript(project));
|
|
1642
|
+
expect(run.status).toBe(0);
|
|
1643
|
+
const home = readResult(project).screens[0];
|
|
1644
|
+
expect(home.status).toBe('rendered');
|
|
1645
|
+
expect(home.copy_advisory ?? null).toBeNull();
|
|
1646
|
+
});
|
|
1647
|
+
test('an absent reference with provenance is fatal (REFERENCE_ABSENT, unverifiable)', async () => {
|
|
1648
|
+
const project = makeProject({
|
|
1649
|
+
screens: { HomeView: 'home.png' },
|
|
1650
|
+
provenance: { HomeView: GOOD_PROV },
|
|
1651
|
+
});
|
|
1652
|
+
// No writeReference — the file is absent.
|
|
1653
|
+
const run = runScript(project, await buildScript(project));
|
|
1654
|
+
expect(run.status).not.toBe(0);
|
|
1655
|
+
const home = readResult(project).screens[0];
|
|
1656
|
+
expect(home.status).toBe('unverifiable');
|
|
1657
|
+
expect(home.error?.code).toBe('REFERENCE_ABSENT');
|
|
1658
|
+
});
|
|
1659
|
+
test('a clean provenanced reference renders with no copy advisory', async () => {
|
|
1660
|
+
const project = makeProject({
|
|
1661
|
+
screens: { HomeView: 'home.png' },
|
|
1662
|
+
render_locale: 'en',
|
|
1663
|
+
provenance: { HomeView: GOOD_PROV },
|
|
1664
|
+
});
|
|
1665
|
+
writeReference(project, 'home.png');
|
|
1666
|
+
const run = runScript(project, await buildScript(project));
|
|
1667
|
+
expect(run.status).toBe(0);
|
|
1668
|
+
const home = readResult(project).screens[0];
|
|
1669
|
+
expect(home.status).toBe('rendered');
|
|
1670
|
+
expect(home.copy_advisory ?? null).toBeNull();
|
|
1671
|
+
});
|
|
1672
|
+
test('an unprovenanced screen keeps its legacy behaviour even with a render_locale (no NO_PROVENANCE arming on iOS)', async () => {
|
|
1673
|
+
// NO_PROVENANCE hard-arming is Android-legacy only; the iOS step never
|
|
1674
|
+
// refused unprovenanced screens and must not start now.
|
|
1675
|
+
const project = makeProject({
|
|
1676
|
+
screens: { HomeView: 'home.png' },
|
|
1677
|
+
render_locale: 'en',
|
|
1678
|
+
provenance: {},
|
|
1679
|
+
});
|
|
1680
|
+
writeReference(project, 'home.png');
|
|
1681
|
+
const run = runScript(project, await buildScript(project));
|
|
1682
|
+
expect(run.status).toBe(0);
|
|
1683
|
+
const home = readResult(project).screens[0];
|
|
1684
|
+
expect(home.status).toBe('rendered');
|
|
1685
|
+
expect(home.copy_advisory ?? null).toBeNull();
|
|
1686
|
+
});
|
|
1687
|
+
test('independent verdicts: a clean sibling carries no advisory while the placeholder state does', async () => {
|
|
1688
|
+
const project = makeProject({
|
|
1689
|
+
screens: { HomeView: 'home.png', SettingsView: 'settings.png' },
|
|
1690
|
+
provenance: {
|
|
1691
|
+
HomeView: GOOD_PROV,
|
|
1692
|
+
SettingsView: { ...GOOD_PROV, resolved: false },
|
|
1693
|
+
},
|
|
1694
|
+
});
|
|
1695
|
+
writeReference(project, 'home.png');
|
|
1696
|
+
writeReference(project, 'settings.png');
|
|
1697
|
+
const run = runScript(project, await buildScript(project));
|
|
1698
|
+
expect(run.status).toBe(0);
|
|
1699
|
+
const [home, settings] = readResult(project).screens;
|
|
1700
|
+
expect(home.copy_advisory ?? null).toBeNull();
|
|
1701
|
+
expect(settings.status).toBe('rendered');
|
|
1702
|
+
expect(settings.copy_advisory?.reason).toBe('unresolved_placeholders');
|
|
1703
|
+
});
|
|
1704
|
+
});
|
|
1583
1705
|
//# sourceMappingURL=ui-fidelity-render.test.js.map
|