@invarn/cibuild 2.1.0 → 2.1.2

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.
Files changed (39) hide show
  1. package/dist/cli.cjs +246 -8
  2. package/dist/src/lib.d.ts +2 -0
  3. package/dist/src/lib.d.ts.map +1 -1
  4. package/dist/src/lib.js +2 -0
  5. package/dist/src/yaml/fidelity-manifest.d.ts +126 -0
  6. package/dist/src/yaml/fidelity-manifest.d.ts.map +1 -0
  7. package/dist/src/yaml/fidelity-manifest.js +134 -0
  8. package/dist/src/yaml/fidelity-manifest.test.d.ts +12 -0
  9. package/dist/src/yaml/fidelity-manifest.test.d.ts.map +1 -0
  10. package/dist/src/yaml/fidelity-manifest.test.js +251 -0
  11. package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts +53 -0
  12. package/dist/src/yaml/steps/fidelity-aspect-guard.d.ts.map +1 -0
  13. package/dist/src/yaml/steps/fidelity-aspect-guard.js +72 -0
  14. package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts +11 -0
  15. package/dist/src/yaml/steps/fidelity-aspect-guard.test.d.ts.map +1 -0
  16. package/dist/src/yaml/steps/fidelity-aspect-guard.test.js +50 -0
  17. package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts +45 -0
  18. package/dist/src/yaml/steps/fidelity-reference-provenance.d.ts.map +1 -0
  19. package/dist/src/yaml/steps/fidelity-reference-provenance.js +69 -0
  20. package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts +14 -0
  21. package/dist/src/yaml/steps/fidelity-reference-provenance.test.d.ts.map +1 -0
  22. package/dist/src/yaml/steps/fidelity-reference-provenance.test.js +54 -0
  23. package/dist/src/yaml/steps/structural-facts.d.ts +101 -0
  24. package/dist/src/yaml/steps/structural-facts.d.ts.map +1 -0
  25. package/dist/src/yaml/steps/structural-facts.js +0 -0
  26. package/dist/src/yaml/steps/structural-facts.test.d.ts +12 -0
  27. package/dist/src/yaml/steps/structural-facts.test.d.ts.map +1 -0
  28. package/dist/src/yaml/steps/structural-facts.test.js +174 -0
  29. package/dist/src/yaml/steps/ui-fidelity-preview-android.d.ts.map +1 -1
  30. package/dist/src/yaml/steps/ui-fidelity-preview-android.js +2 -9
  31. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts +26 -0
  32. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
  33. package/dist/src/yaml/steps/ui-fidelity-preview.js +21 -9
  34. package/dist/src/yaml/steps/ui-fidelity-preview.test.js +31 -1
  35. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +9 -0
  36. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
  37. package/dist/src/yaml/steps/ui-fidelity-render-android.js +131 -1
  38. package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +170 -1
  39. package/package.json +1 -1
@@ -24,6 +24,7 @@ import { homedir, tmpdir } from 'node:os';
24
24
  import { basename, join, resolve } from 'node:path';
25
25
  import { parseScale, DEFAULT_SCALE } from './ui-fidelity-render.js';
26
26
  import { DEFAULT_GRADLE_TASK, PACKAGE_ARCHIVE_BASENAME, generateAndroidRenderScript, isValidGradleTask, } from './ui-fidelity-render-android.js';
27
+ import { buildPreviewScreenResult } from './ui-fidelity-preview.js';
27
28
  /**
28
29
  * Build error returned when the local Android toolchain is absent, so the caller
29
30
  * can fall back to a remote run instead of a confusing build failure. Generic
@@ -233,15 +234,7 @@ export function renderUiFidelityPreviewAndroid(options) {
233
234
  (detail ? `:\n${detail}` : ''));
234
235
  }
235
236
  const raw = JSON.parse(readFileSync(resultPath, 'utf-8'));
236
- const screens = (raw.screens || []).map((s) => ({
237
- screen: s.screen,
238
- status: s.status,
239
- error: s.error ?? null,
240
- referenceImagePath: absOrNull(artifactsDir, s.reference_image_path),
241
- renderedImagePath: absOrNull(artifactsDir, s.rendered_image_path),
242
- alignedImagePath: absOrNull(artifactsDir, s.aligned_image_path),
243
- dimensions: s.dimensions ?? null,
244
- }));
237
+ const screens = (raw.screens || []).map((s) => buildPreviewScreenResult(s, artifactsDir));
245
238
  const buildError = raw.error ?? null;
246
239
  const ok = buildError === null && screens.length > 0 && screens.every((s) => s.status === 'rendered');
247
240
  return { packageSource: 'inputs', buildError, screens, artifactsDir, ok };
@@ -73,6 +73,12 @@ export interface UiFidelityPreviewScreenResult {
73
73
  /** Absolute path to the reference-aligned image, or null. */
74
74
  alignedImagePath: string | null;
75
75
  dimensions: UiFidelityPreviewDimensions | null;
76
+ /**
77
+ * Advisory warning (never a failure) when the rendered aspect ratio diverges
78
+ * sharply from the reference's — the early signal that several states were
79
+ * stacked into one composable. Null when in tolerance or not comparable.
80
+ */
81
+ aspectWarning: string | null;
76
82
  }
77
83
  export interface UiFidelityPreviewResult {
78
84
  /** Always "inputs": the preview mirrors a shipped-package remote run. */
@@ -89,6 +95,26 @@ export interface UiFidelityPreviewResult {
89
95
  /** True only when every screen rendered and there was no build error. */
90
96
  ok: boolean;
91
97
  }
98
+ /** Shape of the protocol-result.json the render script writes. */
99
+ /** One screen as the render script writes it into protocol-result.json. */
100
+ export interface RawPreviewScreen {
101
+ screen: string;
102
+ status: string;
103
+ error: {
104
+ code: string;
105
+ message: string;
106
+ } | null;
107
+ reference_image_path: string | null;
108
+ rendered_image_path: string | null;
109
+ aligned_image_path?: string | null;
110
+ dimensions?: UiFidelityPreviewDimensions | null;
111
+ }
112
+ /**
113
+ * Map one raw render-script screen to a preview result, resolving image paths
114
+ * and attaching the aspect-ratio warning. Shared by the iOS and Android preview
115
+ * engines so the two paths produce identical result shapes and warnings.
116
+ */
117
+ export declare function buildPreviewScreenResult(raw: RawPreviewScreen, artifactsDir: string): UiFidelityPreviewScreenResult;
92
118
  /**
93
119
  * Render the requested screens locally and return the produced image paths and
94
120
  * dimensions. Throws only on caller errors (bad options) or when the render
@@ -1 +1 @@
1
- {"version":3,"file":"ui-fidelity-preview.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAwBH,+EAA+E;AAC/E,MAAM,WAAW,uBAAuB;IACtC,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,iEAAiE;IACjE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,4DAA4D;IAC5D,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,2BAA2B,GAAG,IAAI,CAAC;CAChD;AAED,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,aAAa,EAAE,QAAQ,CAAC;IACxB,oEAAoE;IACpE,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACrD,wDAAwD;IACxD,OAAO,EAAE,6BAA6B,EAAE,CAAC;IACzC,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,EAAE,EAAE,OAAO,CAAC;CACb;AAkCD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,uBAAuB,CAoIzB"}
1
+ {"version":3,"file":"ui-fidelity-preview.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-preview.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AA4BH,+EAA+E;AAC/E,MAAM,WAAW,uBAAuB;IACtC,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,wBAAwB;IACvC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,2BAA2B;IAC1C,WAAW,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,iEAAiE;IACjE,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,4DAA4D;IAC5D,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,6DAA6D;IAC7D,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,UAAU,EAAE,2BAA2B,GAAG,IAAI,CAAC;IAC/C;;;;OAIG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,aAAa,EAAE,QAAQ,CAAC;IACxB,oEAAoE;IACpE,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACrD,wDAAwD;IACxD,OAAO,EAAE,6BAA6B,EAAE,CAAC;IACzC,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,EAAE,EAAE,OAAO,CAAC;CACb;AAED,kEAAkE;AAClE,2EAA2E;AAC3E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAChD,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,kBAAkB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACnC,UAAU,CAAC,EAAE,2BAA2B,GAAG,IAAI,CAAC;CACjD;AAuBD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,gBAAgB,EACrB,YAAY,EAAE,MAAM,GACnB,6BAA6B,CAgB/B;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,wBAAwB,GAChC,uBAAuB,CA8HzB"}
@@ -20,6 +20,7 @@ import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, statSyn
20
20
  import { tmpdir } from 'node:os';
21
21
  import { basename, join, resolve } from 'node:path';
22
22
  import { DEFAULT_RENDER_SIZE, DEFAULT_SCALE, generateRenderScript, parseRenderSize, parseScale, } from './ui-fidelity-render.js';
23
+ import { evaluateAspectRatio, formatAspectRatioWarning, } from './fidelity-aspect-guard.js';
23
24
  function runTar(args, pathPrepend) {
24
25
  const env = pathPrepend
25
26
  ? { ...process.env, PATH: pathPrepend + ':' + (process.env.PATH ?? '') }
@@ -34,6 +35,25 @@ function runTar(args, pathPrepend) {
34
35
  function absOrNull(artifactsDir, relative) {
35
36
  return typeof relative === 'string' && relative !== '' ? resolve(artifactsDir, relative) : null;
36
37
  }
38
+ /**
39
+ * Map one raw render-script screen to a preview result, resolving image paths
40
+ * and attaching the aspect-ratio warning. Shared by the iOS and Android preview
41
+ * engines so the two paths produce identical result shapes and warnings.
42
+ */
43
+ export function buildPreviewScreenResult(raw, artifactsDir) {
44
+ const dimensions = raw.dimensions ?? null;
45
+ const aspect = evaluateAspectRatio(dimensions?.rendered_px ?? null, dimensions?.reference_px ?? null);
46
+ return {
47
+ screen: raw.screen,
48
+ status: raw.status,
49
+ error: raw.error ?? null,
50
+ referenceImagePath: absOrNull(artifactsDir, raw.reference_image_path),
51
+ renderedImagePath: absOrNull(artifactsDir, raw.rendered_image_path),
52
+ alignedImagePath: absOrNull(artifactsDir, raw.aligned_image_path),
53
+ dimensions,
54
+ aspectWarning: formatAspectRatioWarning(raw.screen, aspect),
55
+ };
56
+ }
37
57
  /**
38
58
  * Render the requested screens locally and return the produced image paths and
39
59
  * dimensions. Throws only on caller errors (bad options) or when the render
@@ -136,15 +156,7 @@ export function renderUiFidelityPreview(options) {
136
156
  (detail ? `:\n${detail}` : ''));
137
157
  }
138
158
  const raw = JSON.parse(readFileSync(resultPath, 'utf-8'));
139
- const screens = (raw.screens || []).map((s) => ({
140
- screen: s.screen,
141
- status: s.status,
142
- error: s.error ?? null,
143
- referenceImagePath: absOrNull(artifactsDir, s.reference_image_path),
144
- renderedImagePath: absOrNull(artifactsDir, s.rendered_image_path),
145
- alignedImagePath: absOrNull(artifactsDir, s.aligned_image_path),
146
- dimensions: s.dimensions ?? null,
147
- }));
159
+ const screens = (raw.screens || []).map((s) => buildPreviewScreenResult(s, artifactsDir));
148
160
  const buildError = raw.error ?? null;
149
161
  const ok = buildError === null && screens.length > 0 && screens.every((s) => s.status === 'rendered');
150
162
  return { packageSource: 'inputs', buildError, screens, artifactsDir, ok };
@@ -12,7 +12,7 @@ import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync
12
12
  import { tmpdir } from 'node:os';
13
13
  import { dirname, join, resolve } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
- import { renderUiFidelityPreview } from './ui-fidelity-preview.js';
15
+ import { renderUiFidelityPreview, buildPreviewScreenResult, } from './ui-fidelity-preview.js';
16
16
  const FIXTURE_PACKAGE = resolve(dirname(fileURLToPath(import.meta.url)), '../../../test/fixtures/ui-fidelity-package');
17
17
  const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
18
18
  // A real, decodable 1x1 transparent PNG, used as the reference for the
@@ -100,6 +100,36 @@ describe('renderUiFidelityPreview — option validation', () => {
100
100
  })).toThrow(/reference for HomeView not found/);
101
101
  });
102
102
  });
103
+ describe('buildPreviewScreenResult — aspect-ratio warning wiring', () => {
104
+ function rawScreen(rendered, reference) {
105
+ return {
106
+ screen: 'PromoBanner',
107
+ status: 'rendered',
108
+ error: null,
109
+ reference_image_path: 'references/PromoBanner.png',
110
+ rendered_image_path: 'rendered/PromoBanner.png',
111
+ aligned_image_path: 'aligned/PromoBanner.png',
112
+ dimensions: {
113
+ rendered_px: rendered,
114
+ logical_pt: [rendered[0] / 2, rendered[1] / 2],
115
+ reference_px: reference,
116
+ },
117
+ };
118
+ }
119
+ test('attaches a warning when a stacked render diverges from its reference', () => {
120
+ const result = buildPreviewScreenResult(rawScreen([880, 400], [952, 200]), '/tmp/art');
121
+ expect(result.aspectWarning).toMatch(/divergence/);
122
+ expect(result.aspectWarning).toContain('PromoBanner');
123
+ });
124
+ test('leaves no warning when the render matches the reference aspect ratio', () => {
125
+ const result = buildPreviewScreenResult(rawScreen([720, 1280], [360, 640]), '/tmp/art');
126
+ expect(result.aspectWarning).toBeNull();
127
+ });
128
+ test('leaves no warning when there is no reference to compare', () => {
129
+ const result = buildPreviewScreenResult(rawScreen([720, 1280], null), '/tmp/art');
130
+ expect(result.aspectWarning).toBeNull();
131
+ });
132
+ });
103
133
  describe('renderUiFidelityPreview — fake toolchain', () => {
104
134
  test('archives the package, renders in inputs mode, and returns image paths + dims', () => {
105
135
  const result = renderUiFidelityPreview({
@@ -9,6 +9,15 @@
9
9
  * 1. Reads `.ci/inputs/params.json`
10
10
  * ({ "screens": { "<ScreenName>": "<referenceFileBasename>" } });
11
11
  * reference images live at `.ci/inputs/<basename>`.
12
+ * Optional reference-provenance sidecar (manifest-driven multi-state path):
13
+ * `"render_locale": "<locale>"` plus
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.
12
21
  * 2. Extracts and validates the shipped Gradle project shipped as
13
22
  * `.ci/inputs/package.tar.gz` (single project root carrying a settings
14
23
  * 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;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,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,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;CACpB;AAqnBD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAUrF;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;CAgBpB"}
1
+ {"version":3,"file":"ui-fidelity-render-android.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/ui-fidelity-render-android.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAKpE,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,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;CACpB;AA4uBD;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,yBAAyB,GAAG,MAAM,CAWrF;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;CAgBpB"}
@@ -9,6 +9,15 @@
9
9
  * 1. Reads `.ci/inputs/params.json`
10
10
  * ({ "screens": { "<ScreenName>": "<referenceFileBasename>" } });
11
11
  * reference images live at `.ci/inputs/<basename>`.
12
+ * Optional reference-provenance sidecar (manifest-driven multi-state path):
13
+ * `"render_locale": "<locale>"` plus
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.
12
21
  * 2. Extracts and validates the shipped Gradle project shipped as
13
22
  * `.ci/inputs/package.tar.gz` (single project root carrying a settings
14
23
  * script, bounded extracted size, safe member paths).
@@ -41,6 +50,7 @@
41
50
  import { BaseStepExecutor } from './base.js';
42
51
  import { parseScale, DEFAULT_SCALE } from './ui-fidelity-render.js';
43
52
  import { RENDER_SCRIPT_TRIM_STAGE } from './render-post-processor.js';
53
+ import { RENDER_SCRIPT_FACTS_STAGE } from './structural-facts.js';
44
54
  /**
45
55
  * Default Roborazzi record task. Run from the project root so Gradle resolves
46
56
  * it in whichever subproject declares it.
@@ -136,6 +146,54 @@ function setError(entry, code, message) {
136
146
  entry.error = { code: code, message: sanitizeMessage(message) };
137
147
  }
138
148
 
149
+ // A reference the gate cannot honestly judge: a hard, mechanical metadata
150
+ // verdict (never pixels/OCR). Distinct status so a rotten reference fails loud
151
+ // instead of producing a confident-but-false PASS.
152
+ function setUnverifiable(entry, code, message) {
153
+ entry.status = 'unverifiable';
154
+ entry.error = { code: code, message: sanitizeMessage(message) };
155
+ }
156
+
157
+ // Inlined twin of src/yaml/steps/fidelity-reference-provenance.ts (this script
158
+ // ships self-contained and cannot import it). The render-step integration tests
159
+ // exercise BOTH against the same fixtures so they cannot silently drift. Returns
160
+ // a { code, message } fault, or null when the reference is verifiable.
161
+ function provenanceFault(screen, provenance, renderLocale, referenceExists) {
162
+ var hasProvenance =
163
+ provenance != null &&
164
+ typeof provenance === 'object' &&
165
+ typeof provenance.locale === 'string' &&
166
+ typeof provenance.resolved === 'boolean';
167
+ if (!hasProvenance) {
168
+ return {
169
+ code: 'NO_PROVENANCE',
170
+ message:
171
+ 'reference for ' + screen + ' has no export provenance; a reference is ' +
172
+ 'verifiable only when exported through the deterministic path',
173
+ };
174
+ }
175
+ if (provenance.resolved === false) {
176
+ return {
177
+ code: 'UNRESOLVED_REFERENCE',
178
+ message:
179
+ 'reference for ' + screen + ' still has unresolved copy placeholders ' +
180
+ '(resolved=false) — export it with the copy filled in',
181
+ };
182
+ }
183
+ if (provenance.locale !== renderLocale) {
184
+ return {
185
+ code: 'LOCALE_MISMATCH',
186
+ message:
187
+ 'reference for ' + screen + ' is locale "' + provenance.locale +
188
+ '" but the render locale is "' + renderLocale + '"',
189
+ };
190
+ }
191
+ if (!referenceExists) {
192
+ return { code: 'REFERENCE_ABSENT', message: 'reference image for ' + screen + ' is absent' };
193
+ }
194
+ return null;
195
+ }
196
+
139
197
  function outputTail(result) {
140
198
  var combined = ((result.stdout || '') + '\n' + (result.stderr || '')).trim();
141
199
  if (combined.length > 2000) combined = '...' + combined.slice(-2000);
@@ -404,6 +462,21 @@ function roborazziPngMatches(basename, screen) {
404
462
  return method === screen || method === 'render' + screen;
405
463
  }
406
464
 
465
+ // The structural-pass layout dump (Signal A) is emitted next to the PNG in a
466
+ // roborazzi output dir, but Roborazzi controls the PNG's golden filename and
467
+ // ignores explicit capture names, so the dump can't reliably share the PNG's
468
+ // stem. Match it the same collision-safe way as the PNG instead — by the final
469
+ // dot-delimited segment of "<...>.layout.json" — so the emitter can write a
470
+ // simple "<Screen>.layout.json" regardless of the PNG's golden name.
471
+ function roborazziLayoutMatches(basename, screen) {
472
+ var suffix = '.layout.json';
473
+ if (basename.slice(-suffix.length) !== suffix) return false;
474
+ var stem = basename.slice(0, -suffix.length);
475
+ var lastDot = stem.lastIndexOf('.');
476
+ var seg = lastDot === -1 ? stem : stem.slice(lastDot + 1);
477
+ return seg === screen || seg === 'render' + screen;
478
+ }
479
+
407
480
  // Roborazzi writes PNGs to <module>/build/outputs/roborazzi/. Collect every
408
481
  // such directory in the project so a screen's PNG can be located regardless of
409
482
  // which module rendered it.
@@ -504,6 +577,28 @@ function renderScreens(renderable, projectRoot) {
504
577
  entry.error = null;
505
578
  log('rendered ' + entry.screen + ' -> ' + relative);
506
579
  trimRenderedImage(entry, outputPath, workDir);
580
+ // Signal A: the render emits a per-node layout dump into a roborazzi
581
+ // output dir (named "<Screen>.layout.json" or "...render<Screen>.layout.json").
582
+ // Locate it the same way as the PNG and derive structural facts; absent or
583
+ // malformed is a clean no-op.
584
+ var layoutSource = null;
585
+ for (var li = 0; li < roborazziDirs.length && layoutSource === null; li++) {
586
+ var lnames;
587
+ try {
588
+ lnames = fs.readdirSync(roborazziDirs[li]);
589
+ } catch (lReadError) {
590
+ lnames = [];
591
+ }
592
+ for (var lj = 0; lj < lnames.length; lj++) {
593
+ if (roborazziLayoutMatches(lnames[lj], entry.screen)) {
594
+ layoutSource = path.join(roborazziDirs[li], lnames[lj]);
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ if (layoutSource !== null) {
600
+ deriveAndWriteFacts(entry, layoutSource);
601
+ }
507
602
  });
508
603
  } finally {
509
604
  if (process.env.UI_FIDELITY_KEEP_HARNESS) {
@@ -549,6 +644,18 @@ function main() {
549
644
  return 1;
550
645
  }
551
646
 
647
+ // Reference-provenance sidecar. Enforcement is ON only when the run declares
648
+ // its render locale (the manifest-driven multi-state path always does);
649
+ // legacy single-reference runs without it keep their existing behaviour.
650
+ var renderLocale =
651
+ typeof params.render_locale === 'string' && params.render_locale !== ''
652
+ ? params.render_locale
653
+ : null;
654
+ var provenanceMap =
655
+ params.provenance && typeof params.provenance === 'object' && !Array.isArray(params.provenance)
656
+ ? params.provenance
657
+ : {};
658
+
552
659
  var screens = Object.keys(params.screens);
553
660
  currentEntries = screens.map(function (screen) {
554
661
  return {
@@ -559,6 +666,8 @@ function main() {
559
666
  rendered_image_path: null,
560
667
  aligned_image_path: null,
561
668
  dimensions: null,
669
+ layout_path: null,
670
+ facts_path: null,
562
671
  };
563
672
  });
564
673
  log('screens: ' + (screens.join(', ') || '(none)'));
@@ -566,6 +675,8 @@ function main() {
566
675
  fs.mkdirSync(path.join(artifactsDir, 'ui-fidelity', 'rendered'), { recursive: true });
567
676
  fs.mkdirSync(path.join(artifactsDir, 'ui-fidelity', 'references'), { recursive: true });
568
677
  fs.mkdirSync(path.join(artifactsDir, 'ui-fidelity', 'aligned'), { recursive: true });
678
+ fs.mkdirSync(path.join(artifactsDir, 'ui-fidelity', 'layout'), { recursive: true });
679
+ fs.mkdirSync(path.join(artifactsDir, 'ui-fidelity', 'facts'), { recursive: true });
569
680
 
570
681
  // 2. Per-screen validation + reference copies. Each screen gets its OWN copy
571
682
  // named by screen, even when two screens share a reference basename.
@@ -600,7 +711,25 @@ function main() {
600
711
  } catch (statError) {
601
712
  sourceStat = null;
602
713
  }
603
- if (!sourceStat || !sourceStat.isFile()) {
714
+ var referenceExists = !!(sourceStat && sourceStat.isFile());
715
+
716
+ if (renderLocale) {
717
+ // Provenance enforcement: a reference that is absent, unprovenanced,
718
+ // unresolved, or in the wrong locale is UNVERIFIABLE (hard, loud) — not a
719
+ // render_failed and never a silent PASS.
720
+ var fault = provenanceFault(
721
+ entry.screen,
722
+ provenanceMap[entry.screen],
723
+ renderLocale,
724
+ referenceExists
725
+ );
726
+ if (fault) {
727
+ setUnverifiable(entry, fault.code, fault.message);
728
+ return;
729
+ }
730
+ } else if (!referenceExists) {
731
+ // Legacy (no render locale declared): an absent reference stays a
732
+ // render_failed REFERENCE_MISSING, exactly as before.
604
733
  setError(
605
734
  entry,
606
735
  'REFERENCE_MISSING',
@@ -696,6 +825,7 @@ export function generateAndroidRenderScript(config) {
696
825
  'var RENDERER = ' + JSON.stringify(ANDROID_RENDERER) + ';',
697
826
  'var PACKAGE_ARCHIVE_NAME = ' + JSON.stringify(PACKAGE_ARCHIVE_BASENAME) + ';',
698
827
  RENDER_SCRIPT_TRIM_STAGE,
828
+ RENDER_SCRIPT_FACTS_STAGE,
699
829
  ANDROID_RENDER_SCRIPT_MAIN,
700
830
  ].join('\n');
701
831
  }
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
- import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs';
3
3
  import { gzipSync } from 'node:zlib';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join, resolve } from 'node:path';
@@ -39,6 +39,16 @@ const FAKE_GRADLEW_LINES = [
39
39
  ' [ -z "$s" ] && continue',
40
40
  " printf '\\x89PNG\\r\\n\\x1a\\n' > \"$out/$s.png\"",
41
41
  ' printf \'roborazzi:%s\' "$s" >> "$out/$s.png"',
42
+ // When FAKE_ROBORAZZI_LAYOUT is set, also emit the layout-dump sidecar next
43
+ // to the PNG (matching basename, .layout.json) — a clipping card with an
44
+ // overflowing tag, so the deriver yields one clipped-by-parent fact.
45
+ ' if [ -n "$FAKE_ROBORAZZI_LAYOUT" ]; then',
46
+ // Layout sidecar basename: flat "<screen>.layout.json" by default, or an
47
+ // explicit golden-style dotted name via FAKE_ROBORAZZI_LAYOUT_NAME to prove
48
+ // the consumer finds it by dot-segment match, decoupled from the PNG stem.
49
+ ' lname="${FAKE_ROBORAZZI_LAYOUT_NAME:-$s.layout.json}"',
50
+ ` printf '%s' '{"canvas":{"left":0,"top":0,"right":200,"bottom":400},"nodes":[{"id":"card","bounds":{"left":20,"top":20,"right":180,"bottom":200},"parentId":null,"clipsChildren":true},{"id":"tag","bounds":{"left":60,"top":150,"right":140,"bottom":224},"parentId":"card","clipsChildren":false}]}' > "$out/$lname"`,
51
+ ' fi',
42
52
  'done',
43
53
  'exit 0',
44
54
  ];
@@ -275,6 +285,67 @@ describe('android render runtime — render + trim (happy path)', () => {
275
285
  // The rendered file holds the trim helper's output, not the raw render.
276
286
  expect(readFileSync(artifact(project, 'ui-fidelity/rendered/XProof.png'), 'utf-8')).toContain('trimmed:XProof.png');
277
287
  });
288
+ test('derives structural facts from the layout dump and records layout_path + facts_path', async () => {
289
+ const project = makeProject({ screens: { XProof: 'x.png' } });
290
+ writeReference(project, 'x.png');
291
+ stageArchive(project, gradleProjectEntries());
292
+ const run = runScript(project, await buildScript(), {
293
+ FAKE_ROBORAZZI_SCREENS: 'XProof',
294
+ FAKE_ROBORAZZI_LAYOUT: '1',
295
+ });
296
+ expect(run.status).toBe(0);
297
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
298
+ expect(screen?.status).toBe('rendered');
299
+ expect(screen?.layout_path).toBe('ui-fidelity/layout/XProof.json');
300
+ expect(screen?.facts_path).toBe('ui-fidelity/facts/XProof.json');
301
+ // The layout dump is copied verbatim as a build artifact.
302
+ expect(existsSync(artifact(project, 'ui-fidelity/layout/XProof.json'))).toBe(true);
303
+ // The facts artifact surfaces the bottom clip the eye would miss.
304
+ const facts = JSON.parse(readFileSync(artifact(project, 'ui-fidelity/facts/XProof.json'), 'utf-8'));
305
+ expect(facts).toEqual({
306
+ screen: 'XProof',
307
+ facts: [
308
+ {
309
+ kind: 'clipped_by_parent',
310
+ nodeId: 'tag',
311
+ clipperId: 'card',
312
+ edges: ['bottom'],
313
+ clippedPx: { bottom: 24 },
314
+ },
315
+ ],
316
+ });
317
+ });
318
+ test('finds the layout dump by dot-segment match even when its name differs from the PNG stem', async () => {
319
+ const project = makeProject({ screens: { XProof: 'x.png' } });
320
+ writeReference(project, 'x.png');
321
+ stageArchive(project, gradleProjectEntries());
322
+ // Golden-style dotted layout name (as real Roborazzi would name a sidecar),
323
+ // distinct from the flat PNG the fake writes — the consumer must still find it.
324
+ const run = runScript(project, await buildScript(), {
325
+ FAKE_ROBORAZZI_SCREENS: 'XProof',
326
+ FAKE_ROBORAZZI_LAYOUT: '1',
327
+ FAKE_ROBORAZZI_LAYOUT_NAME: 'com.example.XProofTest.renderXProof.layout.json',
328
+ });
329
+ expect(run.status).toBe(0);
330
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
331
+ expect(screen?.facts_path).toBe('ui-fidelity/facts/XProof.json');
332
+ const facts = JSON.parse(readFileSync(artifact(project, 'ui-fidelity/facts/XProof.json'), 'utf-8'));
333
+ expect(facts.facts).toHaveLength(1);
334
+ expect(facts.facts[0].kind).toBe('clipped_by_parent');
335
+ });
336
+ test('degrades cleanly when no layout dump is emitted (no facts, render still succeeds)', async () => {
337
+ const project = makeProject({ screens: { XProof: 'x.png' } });
338
+ writeReference(project, 'x.png');
339
+ stageArchive(project, gradleProjectEntries());
340
+ // No FAKE_ROBORAZZI_LAYOUT → the render emits a PNG but no sidecar.
341
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
342
+ expect(run.status).toBe(0);
343
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
344
+ expect(screen?.status).toBe('rendered');
345
+ expect(screen?.layout_path ?? null).toBeNull();
346
+ expect(screen?.facts_path ?? null).toBeNull();
347
+ expect(existsSync(artifact(project, 'ui-fidelity/facts/XProof.json'))).toBe(false);
348
+ });
278
349
  test('renders multiple screens independently in one Gradle run', async () => {
279
350
  const project = makeProject({ screens: { XProof: 'x.png', YProof: 'y.png' } });
280
351
  writeReference(project, 'x.png');
@@ -286,6 +357,104 @@ describe('android render runtime — render + trim (happy path)', () => {
286
357
  expect(doc.screens.map((s) => s.status)).toEqual(['rendered', 'rendered']);
287
358
  });
288
359
  });
360
+ describe('android render runtime — reference provenance (UNVERIFIABLE)', () => {
361
+ const GOOD_PROV = {
362
+ figmaNode: '12:345',
363
+ locale: 'en',
364
+ resolved: true,
365
+ exportedAt: '2026-06-18T10:00:00Z',
366
+ };
367
+ // Stage a valid project so per-build archive validation passes and the only
368
+ // signal under test is the per-screen provenance verdict.
369
+ function provProject(params) {
370
+ const project = makeProject(params);
371
+ stageArchive(project, gradleProjectEntries());
372
+ return project;
373
+ }
374
+ test('a reference with no provenance is unverifiable', async () => {
375
+ const project = provProject({
376
+ screens: { XProof: 'x.png' },
377
+ render_locale: 'en',
378
+ provenance: {},
379
+ });
380
+ writeReference(project, 'x.png');
381
+ runScript(project, await buildScript());
382
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
383
+ expect(screen?.status).toBe('unverifiable');
384
+ expect(screen?.error?.code).toBe('NO_PROVENANCE');
385
+ });
386
+ test('a placeholder reference (resolved=false) is unverifiable', async () => {
387
+ const project = provProject({
388
+ screens: { XProof: 'x.png' },
389
+ render_locale: 'en',
390
+ provenance: { XProof: { ...GOOD_PROV, resolved: false } },
391
+ });
392
+ writeReference(project, 'x.png');
393
+ runScript(project, await buildScript());
394
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
395
+ expect(screen?.status).toBe('unverifiable');
396
+ expect(screen?.error?.code).toBe('UNRESOLVED_REFERENCE');
397
+ });
398
+ test('an Italian reference against an English render is unverifiable', async () => {
399
+ const project = provProject({
400
+ screens: { XProof: 'x.png' },
401
+ render_locale: 'en',
402
+ provenance: { XProof: { ...GOOD_PROV, locale: 'it' } },
403
+ });
404
+ writeReference(project, 'x.png');
405
+ runScript(project, await buildScript());
406
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
407
+ expect(screen?.status).toBe('unverifiable');
408
+ expect(screen?.error?.code).toBe('LOCALE_MISMATCH');
409
+ });
410
+ test('an absent reference with good provenance is unverifiable', async () => {
411
+ const project = provProject({
412
+ screens: { XProof: 'x.png' },
413
+ render_locale: 'en',
414
+ provenance: { XProof: GOOD_PROV },
415
+ });
416
+ // No writeReference — the file is absent.
417
+ runScript(project, await buildScript());
418
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
419
+ expect(screen?.status).toBe('unverifiable');
420
+ expect(screen?.error?.code).toBe('REFERENCE_ABSENT');
421
+ });
422
+ test('a good provenanced reference renders normally (a content diff is left to the judge)', async () => {
423
+ const project = provProject({
424
+ screens: { XProof: 'x.png' },
425
+ render_locale: 'en',
426
+ provenance: { XProof: GOOD_PROV },
427
+ });
428
+ writeReference(project, 'x.png');
429
+ const run = runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
430
+ expect(run.status).toBe(0);
431
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
432
+ expect(screen?.status).toBe('rendered');
433
+ expect(screen?.error).toBeNull();
434
+ });
435
+ test('independent verdicts: a sibling stays rendered while one state is unverifiable', async () => {
436
+ const project = provProject({
437
+ screens: { XProof: 'x.png', YProof: 'y.png' },
438
+ render_locale: 'en',
439
+ provenance: { XProof: GOOD_PROV, YProof: { ...GOOD_PROV, resolved: false } },
440
+ });
441
+ writeReference(project, 'x.png');
442
+ writeReference(project, 'y.png');
443
+ runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
444
+ const doc = readResult(project);
445
+ expect(doc.screens.find((s) => s.screen === 'XProof')?.status).toBe('rendered');
446
+ const y = doc.screens.find((s) => s.screen === 'YProof');
447
+ expect(y?.status).toBe('unverifiable');
448
+ expect(y?.error?.code).toBe('UNRESOLVED_REFERENCE');
449
+ });
450
+ test('without a render_locale, provenance is not enforced (legacy behaviour)', async () => {
451
+ const project = provProject({ screens: { XProof: 'x.png' } });
452
+ writeReference(project, 'x.png');
453
+ runScript(project, await buildScript(), { FAKE_ROBORAZZI_SCREENS: 'XProof' });
454
+ const screen = readResult(project).screens.find((s) => s.screen === 'XProof');
455
+ expect(screen?.status).toBe('rendered');
456
+ });
457
+ });
289
458
  describe('android render runtime — error isolation', () => {
290
459
  function twoScreenProject() {
291
460
  const project = makeProject({ screens: { XProof: 'x.png', YProof: 'y.png' } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invarn/cibuild",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "CI Build CLI — local pipeline orchestration and validation",
5
5
  "type": "module",
6
6
  "main": "dist/cli.cjs",