@invarn/cibuild 2.1.0 → 2.1.1

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 (33) hide show
  1. package/dist/cli.cjs +86 -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/ui-fidelity-preview-android.d.ts.map +1 -1
  24. package/dist/src/yaml/steps/ui-fidelity-preview-android.js +2 -9
  25. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts +26 -0
  26. package/dist/src/yaml/steps/ui-fidelity-preview.d.ts.map +1 -1
  27. package/dist/src/yaml/steps/ui-fidelity-preview.js +21 -9
  28. package/dist/src/yaml/steps/ui-fidelity-preview.test.js +31 -1
  29. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts +9 -0
  30. package/dist/src/yaml/steps/ui-fidelity-render-android.d.ts.map +1 -1
  31. package/dist/src/yaml/steps/ui-fidelity-render-android.js +88 -1
  32. package/dist/src/yaml/steps/ui-fidelity-render-android.test.js +98 -0
  33. package/package.json +1 -1
package/dist/src/lib.d.ts CHANGED
@@ -16,6 +16,8 @@ import './yaml/steps/index.js';
16
16
  export { PipelineRunner, StepExecutionError } from './runner.js';
17
17
  export { loadConfig } from './config.js';
18
18
  export { loadYAMLPipeline, getWorkflowNames, hasWorkflow, YAMLParseError, YAMLValidationError, } from './yaml/parser.js';
19
+ export { loadFidelityManifest, parseFidelityManifest, buildSurfaceDispatchInputs, FidelityManifestError, } from './yaml/fidelity-manifest.js';
20
+ export type { FidelityManifest, FidelitySurface, FidelityState, StateReference, ReferenceProvenance, BaselineEntry, SurfaceDispatchInputs, } from './yaml/fidelity-manifest.js';
19
21
  export { convertYAMLToPipelineDef, getAvailableWorkflows, YAMLConversionError, } from './yaml/converter.js';
20
22
  export type { ConversionResult } from './yaml/converter.js';
21
23
  export { pipeline } from './pipeline.js';
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,uBAAuB,CAAC;AAI/B,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAIjE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAI5D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,6BAA6B,EAC7B,2BAA2B,GAC5B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,6CAA6C,CAAC;AACrD,YAAY,EAAE,+BAA+B,EAAE,MAAM,6CAA6C,CAAC;AAInG,OAAO,EACL,eAAe,EACf,OAAO,EACP,iBAAiB,EACjB,YAAY,EACZ,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EACV,OAAO,EACP,WAAW,EACX,QAAQ,EACR,eAAe,EACf,eAAe,EACf,YAAY,GACb,MAAM,YAAY,CAAC;AAIpB,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAI9B,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,UAAU,EACV,OAAO,EACP,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/lib.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,OAAO,uBAAuB,CAAC;AAI/B,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAIjE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAIzC,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,mBAAmB,GACpB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,EAC1B,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AACrC,YAAY,EACV,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,aAAa,EACb,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAIrC,OAAO,EACL,wBAAwB,EACxB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAI5D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAIzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,6BAA6B,EAC7B,2BAA2B,GAC5B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,6CAA6C,CAAC;AACrD,YAAY,EAAE,+BAA+B,EAAE,MAAM,6CAA6C,CAAC;AAInG,OAAO,EACL,eAAe,EACf,OAAO,EACP,iBAAiB,EACjB,YAAY,EACZ,eAAe,GAChB,MAAM,0BAA0B,CAAC;AAIlC,YAAY,EACV,OAAO,EACP,WAAW,EACX,QAAQ,EACR,eAAe,EACf,eAAe,EACf,YAAY,GACb,MAAM,YAAY,CAAC;AAIpB,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAI9B,YAAY,EACV,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,UAAU,EACV,OAAO,EACP,QAAQ,EACR,UAAU,EACV,YAAY,EACZ,QAAQ,EACR,WAAW,GACZ,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAE,+BAA+B,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/src/lib.js CHANGED
@@ -21,6 +21,8 @@ export { PipelineRunner, StepExecutionError } from './runner.js';
21
21
  export { loadConfig } from './config.js';
22
22
  // ─── YAML parser ────────────────────────────────────────────────────────
23
23
  export { loadYAMLPipeline, getWorkflowNames, hasWorkflow, YAMLParseError, YAMLValidationError, } from './yaml/parser.js';
24
+ // ─── UI fidelity manifest ───────────────────────────────────────────────
25
+ export { loadFidelityManifest, parseFidelityManifest, buildSurfaceDispatchInputs, FidelityManifestError, } from './yaml/fidelity-manifest.js';
24
26
  // ─── YAML converter (non-interactive) ───────────────────────────────────
25
27
  export { convertYAMLToPipelineDef, getAvailableWorkflows, YAMLConversionError, } from './yaml/converter.js';
26
28
  // ─── Pipeline DSL factory ───────────────────────────────────────────────
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Multi-state UI-fidelity manifest parser/validator.
3
+ *
4
+ * The manifest lives at the app-repo root as `.invarn/fidelity.yml` and is the
5
+ * single seam the android-ui-fidelity skill and any orchestrator share. It
6
+ * declares each **Surface** (one app screen) and that surface's first-class
7
+ * **States** (guest vs logged-in; empty vs progress vs complete). Each State is
8
+ * one named proof composable paired with one **Reference**, and the Reference
9
+ * carries export-time **provenance** so the gate can assert validity by
10
+ * metadata rather than by reading pixels.
11
+ *
12
+ * This module owns the schema (the TypeScript types below) and a validator that
13
+ * rejects malformed entries with a clear, distinct message — the shared
14
+ * contract the reference-provenance (UNVERIFIABLE) and baseline-filter slices
15
+ * key on.
16
+ */
17
+ /**
18
+ * Export-time provenance stamped onto a Reference. The gate checks this
19
+ * metadata, never the PNG's pixels: a missing/placeholder/wrong-locale
20
+ * reference is caught here instead of by OCR.
21
+ */
22
+ export interface ReferenceProvenance {
23
+ /** The Figma node the reference was exported from (e.g. "12:345"). */
24
+ figmaNode: string;
25
+ /** The locale the reference was exported in (must match the render locale). */
26
+ locale: string;
27
+ /** False when copy placeholders (`{date}`/`{amount}`) are still unfilled. */
28
+ resolved: boolean;
29
+ /** ISO-8601 export timestamp. */
30
+ exportedAt: string;
31
+ }
32
+ /** A single-state design image plus its provenance. */
33
+ export interface StateReference {
34
+ /** Reference image path (repo-relative), or a Figma node URL. */
35
+ path: string;
36
+ /** Export-time provenance; absence makes the State UNVERIFIABLE. */
37
+ provenance: ReferenceProvenance;
38
+ }
39
+ /**
40
+ * One allowlisted, product-owned divergence for a State. Keyed on
41
+ * structured/normalized fields only; `actual` is the pinned render value, so a
42
+ * changed value re-flags by construction.
43
+ */
44
+ export interface BaselineEntry {
45
+ /** Divergence category (e.g. "copy", "geometry") — drives equality. */
46
+ category: string;
47
+ /** Stable element id from layout.json. Unpinnable divergences aren't here. */
48
+ element: string;
49
+ /** The reference/design value. */
50
+ expected: string;
51
+ /** The pinned actual render value — required. */
52
+ actual: string;
53
+ }
54
+ /** One addressable visual State of a Surface. */
55
+ export interface FidelityState {
56
+ /** State name (e.g. "Guest", "LoggedIn"); unique within the surface. */
57
+ name: string;
58
+ /** Named proof composable `<Surface><State>Proof`; 1:1 with a screens key. */
59
+ proof: string;
60
+ /** The single-state reference this State is paired against. */
61
+ reference: StateReference;
62
+ /** Per-state baseline of accepted divergences. */
63
+ accepted?: BaselineEntry[];
64
+ }
65
+ /** One app screen and its first-class States. One proof module per surface. */
66
+ export interface FidelitySurface {
67
+ /** Surface name. */
68
+ name: string;
69
+ /** Render platform (e.g. "android"). */
70
+ platform: string;
71
+ /** Proof module path (one module per surface, N composables inside). */
72
+ module?: string;
73
+ /** The real source files this surface proves. */
74
+ source?: string[];
75
+ /** Globs whose changes imply a re-gate. */
76
+ touchedBy?: string[];
77
+ /** The surface's States. */
78
+ states: FidelityState[];
79
+ }
80
+ /** The parsed `.invarn/fidelity.yml` manifest. */
81
+ export interface FidelityManifest {
82
+ version: number;
83
+ surfaces: FidelitySurface[];
84
+ }
85
+ /** Thrown when a manifest is absent, unparseable, or schema-invalid. */
86
+ export declare class FidelityManifestError extends Error {
87
+ readonly filePath?: string | undefined;
88
+ constructor(message: string, filePath?: string | undefined);
89
+ }
90
+ /** Reads and parses a manifest from disk. */
91
+ export declare function loadFidelityManifest(filePath: string): FidelityManifest;
92
+ /** Parses and validates manifest YAML text. */
93
+ export declare function parseFidelityManifest(text: string, filePath?: string): FidelityManifest;
94
+ /**
95
+ * The per-dispatch inputs for one surface, derived from its manifest entry.
96
+ * Shapes the `params` sidecar the render runtime and preflight already consume
97
+ * (`screens` + `render_locale` + `provenance`), the reference files to upload,
98
+ * and the per-proof `accepted[]` baseline the post-judgment filter reads.
99
+ */
100
+ export interface SurfaceDispatchInputs {
101
+ /** params.screens: proof composable name → reference basename. */
102
+ screens: Record<string, string>;
103
+ /** params.render_locale: the single locale every reference is checked against. */
104
+ render_locale: string;
105
+ /** params.provenance: proof composable name → its reference provenance. */
106
+ provenance: Record<string, ReferenceProvenance>;
107
+ /** Reference files to upload, one per state, addressed by repo path. */
108
+ references: {
109
+ proof: string;
110
+ path: string;
111
+ }[];
112
+ /** Per-proof accepted[] baseline (only states that declare one). */
113
+ accepted: Record<string, BaselineEntry[]>;
114
+ }
115
+ /**
116
+ * Build the per-dispatch inputs for one surface: one run carries all of the
117
+ * surface's states, each paired with its own reference and provenance. The
118
+ * render locale defaults to the references' shared locale (they must agree);
119
+ * pass `renderLocale` to override (a deliberate mismatch then surfaces as
120
+ * UNVERIFIABLE at the gate). Reference basenames must be unique, since the
121
+ * upload layer addresses files by basename.
122
+ */
123
+ export declare function buildSurfaceDispatchInputs(surface: FidelitySurface, opts?: {
124
+ renderLocale?: string;
125
+ }): SurfaceDispatchInputs;
126
+ //# sourceMappingURL=fidelity-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fidelity-manifest.d.ts","sourceRoot":"","sources":["../../../src/yaml/fidelity-manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,QAAQ,EAAE,OAAO,CAAC;IAClB,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,UAAU,EAAE,mBAAmB,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,iDAAiD;AACjD,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,SAAS,EAAE,cAAc,CAAC;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,4BAA4B;IAC5B,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,kDAAkD;AAClD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,wEAAwE;AACxE,qBAAa,qBAAsB,SAAQ,KAAK;aACD,QAAQ,CAAC,EAAE,MAAM;gBAAlD,OAAO,EAAE,MAAM,EAAkB,QAAQ,CAAC,EAAE,MAAM,YAAA;CAI/D;AAED,6CAA6C;AAC7C,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAQvE;AAED,+CAA+C;AAC/C,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,GAChB,gBAAgB,CAoBlB;AAwDD;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,kFAAkF;IAClF,aAAa,EAAE,MAAM,CAAC;IACtB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAChD,wEAAwE;IACxE,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC9C,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,CAAC;CAC3C;AAQD;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,eAAe,EACxB,IAAI,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAO,GACnC,qBAAqB,CA+BvB"}
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Multi-state UI-fidelity manifest parser/validator.
3
+ *
4
+ * The manifest lives at the app-repo root as `.invarn/fidelity.yml` and is the
5
+ * single seam the android-ui-fidelity skill and any orchestrator share. It
6
+ * declares each **Surface** (one app screen) and that surface's first-class
7
+ * **States** (guest vs logged-in; empty vs progress vs complete). Each State is
8
+ * one named proof composable paired with one **Reference**, and the Reference
9
+ * carries export-time **provenance** so the gate can assert validity by
10
+ * metadata rather than by reading pixels.
11
+ *
12
+ * This module owns the schema (the TypeScript types below) and a validator that
13
+ * rejects malformed entries with a clear, distinct message — the shared
14
+ * contract the reference-provenance (UNVERIFIABLE) and baseline-filter slices
15
+ * key on.
16
+ */
17
+ import * as yaml from 'js-yaml';
18
+ import { readFileSync, existsSync } from 'fs';
19
+ /** Thrown when a manifest is absent, unparseable, or schema-invalid. */
20
+ export class FidelityManifestError extends Error {
21
+ filePath;
22
+ constructor(message, filePath) {
23
+ super(message);
24
+ this.filePath = filePath;
25
+ this.name = 'FidelityManifestError';
26
+ }
27
+ }
28
+ /** Reads and parses a manifest from disk. */
29
+ export function loadFidelityManifest(filePath) {
30
+ if (!existsSync(filePath)) {
31
+ throw new FidelityManifestError(`Fidelity manifest not found: ${filePath}`, filePath);
32
+ }
33
+ return parseFidelityManifest(readFileSync(filePath, 'utf-8'), filePath);
34
+ }
35
+ /** Parses and validates manifest YAML text. */
36
+ export function parseFidelityManifest(text, filePath) {
37
+ let doc;
38
+ try {
39
+ doc = yaml.load(text);
40
+ }
41
+ catch (err) {
42
+ throw new FidelityManifestError(`Invalid YAML in fidelity manifest: ${err.message}`, filePath);
43
+ }
44
+ const root = doc;
45
+ const surfaces = root.surfaces ?? [];
46
+ validateSurfaces(surfaces, filePath);
47
+ return {
48
+ version: Number(root.version),
49
+ surfaces,
50
+ };
51
+ }
52
+ /** The provenance fields every Reference must carry to be verifiable. */
53
+ const REQUIRED_PROVENANCE_FIELDS = [
54
+ 'figmaNode',
55
+ 'locale',
56
+ 'resolved',
57
+ 'exportedAt',
58
+ ];
59
+ function validateSurfaces(surfaces, filePath) {
60
+ for (const surface of surfaces) {
61
+ const seenStateNames = new Set();
62
+ for (const state of surface.states ?? []) {
63
+ if (seenStateNames.has(state.name)) {
64
+ throw new FidelityManifestError(`surface "${surface.name}": duplicate state name "${state.name}" — state names must be unique within a surface.`, filePath);
65
+ }
66
+ seenStateNames.add(state.name);
67
+ const where = `surface "${surface.name}" state "${state.name}"`;
68
+ const provenance = state.reference?.provenance;
69
+ for (const field of REQUIRED_PROVENANCE_FIELDS) {
70
+ if (provenance == null || provenance[field] === undefined) {
71
+ throw new FidelityManifestError(`${where}: reference provenance is missing required field "${field}".`, filePath);
72
+ }
73
+ }
74
+ for (const entry of state.accepted ?? []) {
75
+ if (entry.actual === undefined ||
76
+ entry.actual === null ||
77
+ entry.actual === '') {
78
+ throw new FidelityManifestError(`${where}: accepted[] entry for element "${entry.element}" is missing a pinned "actual" value.`, filePath);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ /** POSIX basename of a repo-relative path (manifest paths are POSIX). */
85
+ function posixBasename(p) {
86
+ const parts = p.split('/');
87
+ return parts[parts.length - 1];
88
+ }
89
+ /**
90
+ * Build the per-dispatch inputs for one surface: one run carries all of the
91
+ * surface's states, each paired with its own reference and provenance. The
92
+ * render locale defaults to the references' shared locale (they must agree);
93
+ * pass `renderLocale` to override (a deliberate mismatch then surfaces as
94
+ * UNVERIFIABLE at the gate). Reference basenames must be unique, since the
95
+ * upload layer addresses files by basename.
96
+ */
97
+ export function buildSurfaceDispatchInputs(surface, opts = {}) {
98
+ const screens = {};
99
+ const provenance = {};
100
+ const references = [];
101
+ const accepted = {};
102
+ const basenameToPath = new Map();
103
+ for (const state of surface.states ?? []) {
104
+ const path = state.reference.path;
105
+ const base = posixBasename(path);
106
+ const existing = basenameToPath.get(base);
107
+ if (existing !== undefined && existing !== path) {
108
+ throw new FidelityManifestError(`surface "${surface.name}": states resolve to the same reference basename "${base}" ` +
109
+ `(${existing} vs ${path}); references are uploaded by basename, so they must be unique.`);
110
+ }
111
+ basenameToPath.set(base, path);
112
+ screens[state.proof] = base;
113
+ provenance[state.proof] = state.reference.provenance;
114
+ references.push({ proof: state.proof, path });
115
+ if (state.accepted && state.accepted.length > 0) {
116
+ accepted[state.proof] = state.accepted;
117
+ }
118
+ }
119
+ const renderLocale = opts.renderLocale ?? deriveRenderLocale(surface);
120
+ return { screens, render_locale: renderLocale, provenance, references, accepted };
121
+ }
122
+ /**
123
+ * The render locale a surface's references share. Throws when they disagree and
124
+ * no explicit locale was given — a coherent surface renders in one locale.
125
+ */
126
+ function deriveRenderLocale(surface) {
127
+ const locales = new Set((surface.states ?? []).map((s) => s.reference.provenance.locale));
128
+ if (locales.size > 1) {
129
+ throw new FidelityManifestError(`surface "${surface.name}": references span multiple locales ` +
130
+ `(${[...locales].join(', ')}); pass an explicit renderLocale to disambiguate.`);
131
+ }
132
+ return locales.values().next().value ?? '';
133
+ }
134
+ //# sourceMappingURL=fidelity-manifest.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Unit tests for the multi-state UI-fidelity manifest parser/validator.
3
+ *
4
+ * The manifest (`.invarn/fidelity.yml`) declares each Surface's States
5
+ * first-class: one named proof composable per State, each paired with its own
6
+ * Reference (carrying export-time provenance), plus a per-State `accepted[]`
7
+ * baseline of allowlisted divergences. These tests pin the schema and the
8
+ * validator's distinct rejections; fixtures are written to a temp dir and
9
+ * loaded by path, mirroring the YAML pipeline parser tests.
10
+ */
11
+ export {};
12
+ //# sourceMappingURL=fidelity-manifest.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fidelity-manifest.test.d.ts","sourceRoot":"","sources":["../../../src/yaml/fidelity-manifest.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG"}
@@ -0,0 +1,251 @@
1
+ /**
2
+ * Unit tests for the multi-state UI-fidelity manifest parser/validator.
3
+ *
4
+ * The manifest (`.invarn/fidelity.yml`) declares each Surface's States
5
+ * first-class: one named proof composable per State, each paired with its own
6
+ * Reference (carrying export-time provenance), plus a per-State `accepted[]`
7
+ * baseline of allowlisted divergences. These tests pin the schema and the
8
+ * validator's distinct rejections; fixtures are written to a temp dir and
9
+ * loaded by path, mirroring the YAML pipeline parser tests.
10
+ */
11
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
12
+ import { writeFileSync, mkdtempSync, rmSync } from 'fs';
13
+ import { tmpdir } from 'os';
14
+ import { join } from 'path';
15
+ import { loadFidelityManifest, parseFidelityManifest, buildSurfaceDispatchInputs, FidelityManifestError, } from './fidelity-manifest.js';
16
+ describe('fidelity manifest — parser/validator', () => {
17
+ let workDir;
18
+ beforeEach(() => {
19
+ workDir = mkdtempSync(join(tmpdir(), 'cibuild-fidelity-manifest-'));
20
+ });
21
+ afterEach(() => {
22
+ rmSync(workDir, { recursive: true, force: true });
23
+ });
24
+ function writeManifest(content) {
25
+ const path = join(workDir, 'fidelity.yml');
26
+ writeFileSync(path, content, 'utf-8');
27
+ return path;
28
+ }
29
+ // A well-formed two-state surface modelling the canonical promo banner
30
+ // (guest + logged-in), with provenance on each reference and one accepted
31
+ // baseline entry on the logged-in state.
32
+ const TWO_STATE_BANNER = `version: 1
33
+ surfaces:
34
+ - name: PromoBanner
35
+ platform: android
36
+ module: fidelity/promo-banner
37
+ source:
38
+ - feature/hub/**/PromoBanner.kt
39
+ touchedBy:
40
+ - feature/hub/**/PromoBanner*.kt
41
+ states:
42
+ - name: Guest
43
+ proof: PromoBannerGuestProof
44
+ reference:
45
+ path: fidelity/refs/promo_banner_guest.png
46
+ provenance:
47
+ figmaNode: "12:345"
48
+ locale: en
49
+ resolved: true
50
+ exportedAt: "2026-06-18T10:00:00Z"
51
+ - name: LoggedIn
52
+ proof: PromoBannerLoggedInProof
53
+ reference:
54
+ path: fidelity/refs/promo_banner_logged_in.png
55
+ provenance:
56
+ figmaNode: "12:346"
57
+ locale: en
58
+ resolved: true
59
+ exportedAt: "2026-06-18T10:00:00Z"
60
+ accepted:
61
+ - category: copy
62
+ element: banner_subtitle
63
+ expected: "Get 5 off when you spend 20,00"
64
+ actual: "Get 5 off your first order placed by {date}"
65
+ `;
66
+ test('parses a well-formed two-state surface into addressable states', () => {
67
+ const manifest = loadFidelityManifest(writeManifest(TWO_STATE_BANNER));
68
+ expect(manifest.version).toBe(1);
69
+ expect(manifest.surfaces).toHaveLength(1);
70
+ const surface = manifest.surfaces[0];
71
+ expect(surface.name).toBe('PromoBanner');
72
+ expect(surface.platform).toBe('android');
73
+ expect(surface.states.map((s) => s.name)).toEqual(['Guest', 'LoggedIn']);
74
+ const [guest, loggedIn] = surface.states;
75
+ expect(guest.proof).toBe('PromoBannerGuestProof');
76
+ expect(guest.reference.path).toBe('fidelity/refs/promo_banner_guest.png');
77
+ expect(guest.reference.provenance).toEqual({
78
+ figmaNode: '12:345',
79
+ locale: 'en',
80
+ resolved: true,
81
+ exportedAt: '2026-06-18T10:00:00Z',
82
+ });
83
+ expect(loggedIn.proof).toBe('PromoBannerLoggedInProof');
84
+ expect(loggedIn.accepted).toEqual([
85
+ {
86
+ category: 'copy',
87
+ element: 'banner_subtitle',
88
+ expected: 'Get 5 off when you spend 20,00',
89
+ actual: 'Get 5 off your first order placed by {date}',
90
+ },
91
+ ]);
92
+ });
93
+ test('rejects a reference missing a provenance field', () => {
94
+ // `exportedAt` omitted from the guest reference's provenance.
95
+ const yaml = `version: 1
96
+ surfaces:
97
+ - name: PromoBanner
98
+ platform: android
99
+ states:
100
+ - name: Guest
101
+ proof: PromoBannerGuestProof
102
+ reference:
103
+ path: fidelity/refs/promo_banner_guest.png
104
+ provenance:
105
+ figmaNode: "12:345"
106
+ locale: en
107
+ resolved: true
108
+ `;
109
+ expect(() => loadFidelityManifest(writeManifest(yaml))).toThrow(FidelityManifestError);
110
+ expect(() => loadFidelityManifest(writeManifest(yaml))).toThrow(/provenance.*exportedAt|exportedAt.*provenance/i);
111
+ });
112
+ test('rejects an accepted[] entry with no pinned actual', () => {
113
+ // The baseline entry omits `actual` — it cannot pin the render value, so a
114
+ // changed value could never re-flag. Reject loud.
115
+ const yaml = `version: 1
116
+ surfaces:
117
+ - name: PromoBanner
118
+ platform: android
119
+ states:
120
+ - name: LoggedIn
121
+ proof: PromoBannerLoggedInProof
122
+ reference:
123
+ path: fidelity/refs/promo_banner_logged_in.png
124
+ provenance:
125
+ figmaNode: "12:346"
126
+ locale: en
127
+ resolved: true
128
+ exportedAt: "2026-06-18T10:00:00Z"
129
+ accepted:
130
+ - category: copy
131
+ element: banner_subtitle
132
+ expected: "Get 5 off when you spend 20,00"
133
+ `;
134
+ expect(() => loadFidelityManifest(writeManifest(yaml))).toThrow(FidelityManifestError);
135
+ expect(() => loadFidelityManifest(writeManifest(yaml))).toThrow(/accepted.*actual|actual/i);
136
+ });
137
+ test('rejects duplicate state names within a surface', () => {
138
+ // Two states both named "Guest" — their proofs would collide on one
139
+ // params.screens key.
140
+ const yaml = `version: 1
141
+ surfaces:
142
+ - name: PromoBanner
143
+ platform: android
144
+ states:
145
+ - name: Guest
146
+ proof: PromoBannerGuestProof
147
+ reference:
148
+ path: fidelity/refs/promo_banner_guest.png
149
+ provenance:
150
+ figmaNode: "12:345"
151
+ locale: en
152
+ resolved: true
153
+ exportedAt: "2026-06-18T10:00:00Z"
154
+ - name: Guest
155
+ proof: PromoBannerGuestAltProof
156
+ reference:
157
+ path: fidelity/refs/promo_banner_guest_alt.png
158
+ provenance:
159
+ figmaNode: "12:347"
160
+ locale: en
161
+ resolved: true
162
+ exportedAt: "2026-06-18T10:00:00Z"
163
+ `;
164
+ expect(() => loadFidelityManifest(writeManifest(yaml))).toThrow(FidelityManifestError);
165
+ expect(() => loadFidelityManifest(writeManifest(yaml))).toThrow(/duplicate state.*Guest|state name "Guest"/i);
166
+ });
167
+ });
168
+ describe('buildSurfaceDispatchInputs', () => {
169
+ const BANNER = `version: 1
170
+ surfaces:
171
+ - name: PromoBanner
172
+ platform: android
173
+ module: fidelity/promo-banner
174
+ states:
175
+ - name: Guest
176
+ proof: PromoBannerGuestProof
177
+ reference:
178
+ path: fidelity/refs/promo_banner_guest.png
179
+ provenance:
180
+ figmaNode: "12:345"
181
+ locale: en
182
+ resolved: true
183
+ exportedAt: "2026-06-18T10:00:00Z"
184
+ - name: LoggedIn
185
+ proof: PromoBannerLoggedInProof
186
+ reference:
187
+ path: fidelity/refs/promo_banner_logged_in.png
188
+ provenance:
189
+ figmaNode: "12:346"
190
+ locale: en
191
+ resolved: true
192
+ exportedAt: "2026-06-18T10:00:00Z"
193
+ accepted:
194
+ - category: copy
195
+ element: banner_subtitle
196
+ expected: "A"
197
+ actual: "B"
198
+ `;
199
+ function bannerSurface(extra = '') {
200
+ return parseFidelityManifest(BANNER + extra).surfaces[0];
201
+ }
202
+ test('maps each state proof to its reference basename in screens', () => {
203
+ const inputs = buildSurfaceDispatchInputs(bannerSurface());
204
+ expect(inputs.screens).toEqual({
205
+ PromoBannerGuestProof: 'promo_banner_guest.png',
206
+ PromoBannerLoggedInProof: 'promo_banner_logged_in.png',
207
+ });
208
+ });
209
+ test('keys provenance by proof and derives the render locale from the references', () => {
210
+ const inputs = buildSurfaceDispatchInputs(bannerSurface());
211
+ expect(inputs.render_locale).toBe('en');
212
+ expect(inputs.provenance.PromoBannerGuestProof).toEqual({
213
+ figmaNode: '12:345',
214
+ locale: 'en',
215
+ resolved: true,
216
+ exportedAt: '2026-06-18T10:00:00Z',
217
+ });
218
+ });
219
+ test('lists the reference files to upload, one per state, by repo path', () => {
220
+ const inputs = buildSurfaceDispatchInputs(bannerSurface());
221
+ expect(inputs.references).toEqual([
222
+ { proof: 'PromoBannerGuestProof', path: 'fidelity/refs/promo_banner_guest.png' },
223
+ { proof: 'PromoBannerLoggedInProof', path: 'fidelity/refs/promo_banner_logged_in.png' },
224
+ ]);
225
+ });
226
+ test('carries each state baseline keyed by proof for the post-judgment filter', () => {
227
+ const inputs = buildSurfaceDispatchInputs(bannerSurface());
228
+ expect(inputs.accepted.PromoBannerLoggedInProof).toEqual([
229
+ { category: 'copy', element: 'banner_subtitle', expected: 'A', actual: 'B' },
230
+ ]);
231
+ // States with no accepted[] don't appear.
232
+ expect(inputs.accepted.PromoBannerGuestProof).toBeUndefined();
233
+ });
234
+ test('honours an explicit render locale override', () => {
235
+ const inputs = buildSurfaceDispatchInputs(bannerSurface(), { renderLocale: 'it' });
236
+ expect(inputs.render_locale).toBe('it');
237
+ });
238
+ test('throws when references disagree on locale and none is given', () => {
239
+ // Flip the logged-in reference to Italian.
240
+ const surface = bannerSurface();
241
+ surface.states[1].reference.provenance.locale = 'it';
242
+ expect(() => buildSurfaceDispatchInputs(surface)).toThrow(/locale/i);
243
+ });
244
+ test('throws when two states resolve to the same reference basename', () => {
245
+ const surface = bannerSurface();
246
+ // Same basename, different dirs → upload-layer collision.
247
+ surface.states[1].reference.path = 'other/promo_banner_guest.png';
248
+ expect(() => buildSurfaceDispatchInputs(surface)).toThrow(/basename/i);
249
+ });
250
+ });
251
+ //# sourceMappingURL=fidelity-manifest.test.js.map
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Fidelity aspect-ratio guard.
3
+ *
4
+ * One named composable per state is the authoring norm; the failure this
5
+ * catches is the opposite — several states stacked into one composable, which
6
+ * renders far taller (or wider) than the single-state reference it is paired
7
+ * against. That shows up as a sharp divergence between the rendered and
8
+ * reference aspect ratios, well before a human reads the diff.
9
+ *
10
+ * The guard is a pure comparison over the dimensions the render already
11
+ * reports (`rendered_px`, `reference_px`). It emits a WARNING when the
12
+ * divergence exceeds a documented fold factor — it never fails the render, and
13
+ * it stays silent when there is no reference to compare against.
14
+ */
15
+ /** Pixel dimensions as `[width, height]`. */
16
+ export type PixelSize = [number, number];
17
+ /**
18
+ * Fold factor beyond which a rendered/reference aspect-ratio divergence is
19
+ * worth a warning. A faithful single-state wrapper sits near 1.0; trim and
20
+ * reference alignment nudge it a few percent; a re-stacked 2-up wrapper roughly
21
+ * doubles one dimension and lands near 2.0. 1.5 catches the stack while
22
+ * tolerating the wobble.
23
+ */
24
+ export declare const ASPECT_RATIO_WARN_FACTOR = 1.5;
25
+ /** The outcome of comparing a rendered aspect ratio to its reference. */
26
+ export interface AspectRatioCheck {
27
+ /** True only when both sizes are present and strictly positive. */
28
+ comparable: boolean;
29
+ /** Rendered width/height, or null when not comparable. */
30
+ renderedAspect: number | null;
31
+ /** Reference width/height, or null when not comparable. */
32
+ referenceAspect: number | null;
33
+ /**
34
+ * Symmetric divergence `max(r/ref, ref/r)` (≥ 1), or null when not
35
+ * comparable. 1.0 means identical proportions.
36
+ */
37
+ divergence: number | null;
38
+ /** True when comparable and the divergence exceeds the fold factor. */
39
+ warn: boolean;
40
+ }
41
+ /**
42
+ * Compares a rendered size against its reference size. Returns a
43
+ * non-warning, non-comparable result when either size is missing or
44
+ * non-positive — the guard only speaks when it has two real frames to compare.
45
+ */
46
+ export declare function evaluateAspectRatio(renderedPx: PixelSize | null, referencePx: PixelSize | null, factor?: number): AspectRatioCheck;
47
+ /**
48
+ * Human/agent-readable warning line for a divergent screen, or null when the
49
+ * screen does not warrant one. The caller decides where to surface it (preview
50
+ * result, preflight output) — it is advisory, never a failure.
51
+ */
52
+ export declare function formatAspectRatioWarning(screen: string, check: AspectRatioCheck): string | null;
53
+ //# sourceMappingURL=fidelity-aspect-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fidelity-aspect-guard.d.ts","sourceRoot":"","sources":["../../../../src/yaml/steps/fidelity-aspect-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,6CAA6C;AAC7C,MAAM,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEzC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,yEAAyE;AACzE,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,UAAU,EAAE,OAAO,CAAC;IACpB,0DAA0D;IAC1D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,2DAA2D;IAC3D,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;OAGG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,uEAAuE;IACvE,IAAI,EAAE,OAAO,CAAC;CACf;AAaD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,GAAG,IAAI,EAC5B,WAAW,EAAE,SAAS,GAAG,IAAI,EAC7B,MAAM,GAAE,MAAiC,GACxC,gBAAgB,CAyBlB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,GACtB,MAAM,GAAG,IAAI,CAUf"}