@penpair/marwes-ui 0.2.0 → 0.3.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.
@@ -2,187 +2,453 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { createJiti } from "jiti";
5
- import { atomicWriteFile, parseCommandArgs, resolveProjectInput, resolveProjectOutput } from "@penpair/core";
5
+ import { atomicWriteFile, parseCommandArgs, resolveProjectInput, resolveProjectOutput, } from "@penpair/core";
6
6
  import { computeExpectation } from "./commands.js";
7
7
  import { canonical, toHexColor } from "./tokens.js";
8
- export const componentProperties = ["width", "height", "paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "rowGap", "columnGap", "borderRadius", "borderWidth", "borderColor", "backgroundColor", "color", "fontFamily", "fontSize", "fontWeight", "lineHeight", "boxShadow"];
8
+ export const componentProperties = [
9
+ "width",
10
+ "height",
11
+ "paddingTop",
12
+ "paddingRight",
13
+ "paddingBottom",
14
+ "paddingLeft",
15
+ "rowGap",
16
+ "columnGap",
17
+ "borderRadius",
18
+ "borderWidth",
19
+ "borderColor",
20
+ "backgroundColor",
21
+ "color",
22
+ "fontFamily",
23
+ "fontSize",
24
+ "fontWeight",
25
+ "lineHeight",
26
+ "boxShadow",
27
+ ];
9
28
  const digest = (v) => createHash("sha256").update(canonical(v)).digest("hex");
10
29
  const text = (v, label, max = 500) => {
11
- if (typeof v !== "string" || !v.trim() || v.length > max || /[\x00-\x1f]/.test(v))
30
+ if (typeof v !== "string" || !v.trim() || v.length > max || /[\x00-\x1f]/.test(v)) {
12
31
  throw new Error(`Invalid component ${label}`);
32
+ }
13
33
  return v;
14
34
  };
15
35
  export function readComponentPilot(raw) {
16
36
  const value = raw.marwes?.components;
17
- if (!value || typeof value !== "object" || Array.isArray(value))
37
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18
38
  throw new Error("Configure marwes.components with a trusted renderer, sources and explicit pilot cases");
39
+ }
19
40
  const v = value;
20
- if (v.schemaVersion !== 1 || !Array.isArray(v.sources) || !v.sources.length || v.sources.length > 100 || !Array.isArray(v.cases) || !v.cases.length || v.cases.length > 60)
41
+ if (v.schemaVersion !== 1 ||
42
+ !Array.isArray(v.sources) ||
43
+ !v.sources.length ||
44
+ v.sources.length > 100 ||
45
+ !Array.isArray(v.cases) ||
46
+ !v.cases.length ||
47
+ v.cases.length > 60) {
21
48
  throw new Error("Invalid schema-1 component pilot (max 100 sources, 60 cases)");
49
+ }
22
50
  const ids = new Set();
23
- const cases = v.cases.map(c => {
24
- if (!c || !["Button", "Card", "StatTile"].includes(c.component) || !["light", "dark"].includes(c.theme))
51
+ const cases = v.cases.map((c) => {
52
+ if (!c ||
53
+ ![
54
+ "Button",
55
+ "Card",
56
+ "StatTile",
57
+ ].includes(c.component) ||
58
+ ![
59
+ "light",
60
+ "dark",
61
+ ].includes(c.theme)) {
25
62
  throw new Error("Pilot supports Button/Card/StatTile and explicit light/dark themes");
63
+ }
26
64
  const id = text(c.id, "case id");
27
- if (ids.has(id))
65
+ if (ids.has(id)) {
28
66
  throw new Error("Duplicate component case id");
67
+ }
29
68
  ids.add(id);
30
- if (![c.viewport?.width, c.viewport?.height].every(n => Number.isInteger(n) && n > 0 && n <= 8192))
69
+ if (![
70
+ c.viewport?.width,
71
+ c.viewport?.height,
72
+ ].every((n) => Number.isInteger(n) && n > 0 && n <= 8192)) {
31
73
  throw new Error("Invalid component viewport");
32
- return { id, component: c.component, theme: c.theme, state: text(c.state, "state"), viewport: { ...c.viewport }, selector: text(c.selector, "selector") };
74
+ }
75
+ return {
76
+ id,
77
+ component: c.component,
78
+ theme: c.theme,
79
+ state: text(c.state, "state"),
80
+ viewport: {
81
+ ...c.viewport,
82
+ },
83
+ selector: text(c.selector, "selector"),
84
+ };
33
85
  });
34
- for (const component of ["Button", "Card", "StatTile"])
35
- for (const theme of ["light", "dark"])
36
- if (!cases.some(c => c.component === component && c.theme === theme))
86
+ for (const component of [
87
+ "Button",
88
+ "Card",
89
+ "StatTile",
90
+ ]) {
91
+ for (const theme of [
92
+ "light",
93
+ "dark",
94
+ ]) {
95
+ if (!cases.some((c) => c.component === component && c.theme === theme)) {
37
96
  throw new Error(`Pilot requires ${component} in ${theme}`);
38
- return { schemaVersion: 1, renderer: text(v.renderer, "renderer"), sources: v.sources.map(s => text(s, "source")), snapshot: text(v.snapshot, "snapshot"), cases };
97
+ }
98
+ }
99
+ }
100
+ return {
101
+ schemaVersion: 1,
102
+ renderer: text(v.renderer, "renderer"),
103
+ sources: v.sources.map((s) => text(s, "source")),
104
+ snapshot: text(v.snapshot, "snapshot"),
105
+ cases,
106
+ };
39
107
  }
40
108
  async function boundedRead(root, relative) {
41
109
  const file = await resolveProjectInput(root, relative);
42
- if ((await stat(file)).size > 4 * 1024 * 1024)
110
+ if ((await stat(file)).size > 4 * 1024 * 1024) {
43
111
  throw new Error("Component input exceeds 4 MiB");
112
+ }
44
113
  return readFile(file, "utf8");
45
114
  }
46
115
  /** Hash declared source bytes as well as effective tokens: comments and non-token anatomy changes invalidate cheap CI checks. */
47
116
  export async function componentSourceHash(root, pilot, tokens) {
48
- const files = [...new Set([pilot.renderer, ...pilot.sources])].sort();
49
- const sources = await Promise.all(files.map(async (file) => [file, digest(await boundedRead(root, file))]));
50
- return digest({ pilot, sources, tokens: tokens.hash, version: tokens.marwesVersion });
117
+ const files = [
118
+ ...new Set([
119
+ pilot.renderer,
120
+ ...pilot.sources,
121
+ ]),
122
+ ].sort();
123
+ const sources = await Promise.all(files.map(async (file) => [
124
+ file,
125
+ digest(await boundedRead(root, file)),
126
+ ]));
127
+ return digest({
128
+ pilot,
129
+ sources,
130
+ tokens: tokens.hash,
131
+ version: tokens.marwesVersion,
132
+ });
51
133
  }
52
134
  function validateMeasurement(raw, id) {
53
135
  const v = raw;
54
- if (!v || v.id !== id || !v.parts || Array.isArray(v.parts) || typeof v.parts !== "object" || !Object.keys(v.parts).length || Object.keys(v.parts).length > 32 || !v.environment || typeof v.environment.fontsReady !== "boolean")
136
+ if (!v ||
137
+ v.id !== id ||
138
+ !v.parts ||
139
+ Array.isArray(v.parts) ||
140
+ typeof v.parts !== "object" ||
141
+ !Object.keys(v.parts).length ||
142
+ Object.keys(v.parts).length > 32 ||
143
+ !v.environment ||
144
+ typeof v.environment.fontsReady !== "boolean") {
55
145
  throw new Error(`Incomplete component measurement: ${id}`);
146
+ }
56
147
  const parts = Object.create(null);
57
148
  for (const [name, fields] of Object.entries(v.parts)) {
58
149
  text(name, "part", 100);
59
- if (!fields || typeof fields !== "object" || Array.isArray(fields) || Object.keys(fields).some(k => !componentProperties.includes(k)))
150
+ if (!fields ||
151
+ typeof fields !== "object" ||
152
+ Array.isArray(fields) ||
153
+ Object.keys(fields).some((k) => !componentProperties.includes(k))) {
60
154
  throw new Error("Invalid measured anatomy properties");
61
- parts[name] = Object.fromEntries(componentProperties.map(key => {
155
+ }
156
+ parts[name] = Object.fromEntries(componentProperties.map((key) => {
62
157
  const value = fields[key];
63
- if (value !== null && (typeof value !== "string" || value.length > 2000))
158
+ if (value !== null && (typeof value !== "string" || value.length > 2000)) {
64
159
  throw new Error(`Missing/invalid ${id}/${name}/${key}; use null for unknown`);
65
- return [key, value];
160
+ }
161
+ return [
162
+ key,
163
+ value,
164
+ ];
66
165
  }));
67
166
  }
68
- return { id, parts, environment: { browser: text(v.environment.browser, "browser"), fontsReady: v.environment.fontsReady } };
167
+ return {
168
+ id,
169
+ parts,
170
+ environment: {
171
+ browser: text(v.environment.browser, "browser"),
172
+ fontsReady: v.environment.fontsReady,
173
+ },
174
+ };
69
175
  }
70
176
  /** Exact value candidates are evidence of equality only. Even one candidate is not a semantic binding. */
71
177
  export function componentTokenCandidates(fields, tokens, mode) {
72
- const selected = tokens.themes.find(t => t.group === "Mode" && t.name.toLowerCase() === mode);
73
- if (!selected)
178
+ const selected = tokens.themes.find((t) => t.group === "Mode" && t.name.toLowerCase() === mode);
179
+ if (!selected) {
74
180
  return {};
181
+ }
75
182
  const effective = new Map();
76
- for (const setName of selected.sets)
77
- for (const token of tokens.sets.find(s => s.name === setName)?.tokens ?? [])
78
- effective.set(token.name, { set: setName, value: token.value, type: token.type });
183
+ for (const setName of selected.sets) {
184
+ for (const token of tokens.sets.find((s) => s.name === setName)?.tokens ?? []) {
185
+ effective.set(token.name, {
186
+ set: setName,
187
+ value: token.value,
188
+ type: token.type,
189
+ });
190
+ }
191
+ }
79
192
  const result = Object.create(null);
80
193
  for (const [property, raw] of Object.entries(fields)) {
81
194
  if (raw === null) {
82
195
  result[property] = [];
83
196
  continue;
84
197
  }
85
- const color = ["color", "borderColor", "backgroundColor"].includes(property);
86
- const types = color ? ["color"] : property.startsWith("padding") || property.endsWith("Gap") ? ["spacing", "dimension"] : property === "borderRadius" ? ["borderRadius", "dimension"] : ["width", "height"].includes(property) ? ["sizing", "dimension"] : [];
87
- const normalize = (value) => { try {
88
- return color ? toHexColor(value).toLowerCase() : value.replace(/^(-?[\d.]+)px$/, "$1");
89
- }
90
- catch {
91
- return value;
92
- } };
93
- result[property] = [...effective].filter(([, t]) => types.includes(t.type) && typeof t.value === "string" && normalize(t.value) === normalize(raw)).map(([name, t]) => `${t.set}/${name}`).sort();
198
+ const color = [
199
+ "color",
200
+ "borderColor",
201
+ "backgroundColor",
202
+ ].includes(property);
203
+ const types = color
204
+ ? [
205
+ "color",
206
+ ]
207
+ : property.startsWith("padding") || property.endsWith("Gap")
208
+ ? [
209
+ "spacing",
210
+ "dimension",
211
+ ]
212
+ : property === "borderRadius"
213
+ ? [
214
+ "borderRadius",
215
+ "dimension",
216
+ ]
217
+ : [
218
+ "width",
219
+ "height",
220
+ ].includes(property)
221
+ ? [
222
+ "sizing",
223
+ "dimension",
224
+ ]
225
+ : [];
226
+ const normalize = (value) => {
227
+ try {
228
+ return color ? toHexColor(value).toLowerCase() : value.replace(/^(-?[\d.]+)px$/, "$1");
229
+ }
230
+ catch {
231
+ return value;
232
+ }
233
+ };
234
+ result[property] = [
235
+ ...effective,
236
+ ]
237
+ .filter(([, t]) => types.includes(t.type) &&
238
+ typeof t.value === "string" &&
239
+ normalize(t.value) === normalize(raw))
240
+ .map(([name, t]) => `${t.set}/${name}`)
241
+ .sort();
94
242
  }
95
243
  return result;
96
244
  }
97
245
  export async function measureComponentPilot(pilot, tokens, sourceHash, renderer) {
98
246
  const raw = await renderer(structuredClone(pilot.cases));
99
- if (!Array.isArray(raw) || raw.length !== pilot.cases.length || JSON.stringify(raw).length > 4 * 1024 * 1024)
247
+ if (!Array.isArray(raw) ||
248
+ raw.length !== pilot.cases.length ||
249
+ JSON.stringify(raw).length > 4 * 1024 * 1024) {
100
250
  throw new Error("Renderer must return exactly one bounded observation per case");
101
- const byId = new Map(raw.map(v => [v?.id, v]));
102
- if (byId.size !== raw.length)
251
+ }
252
+ const byId = new Map(raw.map((v) => [
253
+ v?.id,
254
+ v,
255
+ ]));
256
+ if (byId.size !== raw.length) {
103
257
  throw new Error("Duplicate renderer case");
104
- const cases = pilot.cases.map(c => {
258
+ }
259
+ const cases = pilot.cases.map((c) => {
105
260
  const measurement = validateMeasurement(byId.get(c.id), c.id);
106
- return { ...c, measurement, tokenCandidates: Object.fromEntries(Object.entries(measurement.parts).map(([name, fields]) => [name, componentTokenCandidates(fields, tokens, c.theme)])) };
261
+ return {
262
+ ...c,
263
+ measurement,
264
+ tokenCandidates: Object.fromEntries(Object.entries(measurement.parts).map(([name, fields]) => [
265
+ name,
266
+ componentTokenCandidates(fields, tokens, c.theme),
267
+ ])),
268
+ };
107
269
  });
108
- const body = { schemaVersion: 1, kind: "component-pilot", sourceHash, marwesVersion: tokens.marwesVersion, cases, nativeLibrary: "gated-on-instance-override-proof" };
109
- return { ...body, hash: digest(body) };
270
+ const body = {
271
+ schemaVersion: 1,
272
+ kind: "component-pilot",
273
+ sourceHash,
274
+ marwesVersion: tokens.marwesVersion,
275
+ cases,
276
+ nativeLibrary: "gated-on-instance-override-proof",
277
+ };
278
+ return {
279
+ ...body,
280
+ hash: digest(body),
281
+ };
110
282
  }
111
283
  export function validateComponentSnapshot(raw) {
112
284
  const v = raw;
113
- if (!v || v.schemaVersion !== 1 || v.kind !== "component-pilot" || v.nativeLibrary !== "gated-on-instance-override-proof" || !/^[a-f0-9]{64}$/.test(v.sourceHash) || !Array.isArray(v.cases) || v.cases.length > 60)
285
+ if (!v ||
286
+ v.schemaVersion !== 1 ||
287
+ v.kind !== "component-pilot" ||
288
+ v.nativeLibrary !== "gated-on-instance-override-proof" ||
289
+ !/^[a-f0-9]{64}$/.test(v.sourceHash) ||
290
+ !Array.isArray(v.cases) ||
291
+ v.cases.length > 60) {
114
292
  throw new Error("Unsupported component snapshot");
293
+ }
115
294
  const { hash, ...body } = v;
116
- if (hash !== digest(body))
295
+ if (hash !== digest(body)) {
117
296
  throw new Error("Component snapshot hash mismatch");
118
- const pilot = readComponentPilot({ marwes: { components: { schemaVersion: 1, renderer: "unused", sources: ["unused"], snapshot: "unused", cases: v.cases } } });
119
- for (const c of pilot.cases)
120
- validateMeasurement(v.cases.find(m => m.id === c.id)?.measurement, c.id);
297
+ }
298
+ const pilot = readComponentPilot({
299
+ marwes: {
300
+ components: {
301
+ schemaVersion: 1,
302
+ renderer: "unused",
303
+ sources: [
304
+ "unused",
305
+ ],
306
+ snapshot: "unused",
307
+ cases: v.cases,
308
+ },
309
+ },
310
+ });
311
+ for (const c of pilot.cases) {
312
+ validateMeasurement(v.cases.find((m) => m.id === c.id)?.measurement, c.id);
313
+ }
121
314
  return v;
122
315
  }
123
316
  export function diffComponentSnapshots(before, after) {
124
317
  const changes = [];
125
- const ids = new Set([...before.cases, ...after.cases].map(c => c.id));
318
+ const ids = new Set([
319
+ ...before.cases,
320
+ ...after.cases,
321
+ ].map((c) => c.id));
126
322
  for (const id of ids) {
127
- const a = before.cases.find(c => c.id === id), b = after.cases.find(c => c.id === id);
128
- if (canonical(a ?? null) !== canonical(b ?? null))
129
- changes.push({ id, before: a ?? null, after: b ?? null });
323
+ const a = before.cases.find((c) => c.id === id);
324
+ const b = after.cases.find((c) => c.id === id);
325
+ if (canonical(a ?? null) !== canonical(b ?? null)) {
326
+ changes.push({
327
+ id,
328
+ before: a ?? null,
329
+ after: b ?? null,
330
+ });
331
+ }
130
332
  }
131
- return { schemaVersion: 1, identical: before.hash === after.hash, sourceChanged: before.sourceHash !== after.sourceHash, changes, semanticBindings: "not-inferred", instanceOverrides: "not-tested" };
333
+ return {
334
+ schemaVersion: 1,
335
+ identical: before.hash === after.hash,
336
+ sourceChanged: before.sourceHash !== after.sourceHash,
337
+ changes,
338
+ semanticBindings: "not-inferred",
339
+ instanceOverrides: "not-tested",
340
+ };
132
341
  }
133
342
  export async function componentsCommand(context, args) {
134
343
  const [sub, ...rest] = args;
135
- const { values, positionals } = parseCommandArgs({ args: rest, allowPositionals: true, options: { write: { type: "boolean" }, measure: { type: "boolean" }, json: { type: "boolean" } } });
344
+ const { values, positionals } = parseCommandArgs({
345
+ args: rest,
346
+ allowPositionals: true,
347
+ options: {
348
+ write: {
349
+ type: "boolean",
350
+ },
351
+ measure: {
352
+ type: "boolean",
353
+ },
354
+ json: {
355
+ type: "boolean",
356
+ },
357
+ },
358
+ });
136
359
  if (sub === "schema") {
137
- if (positionals.length || Object.keys(values).some(k => k !== "json"))
360
+ if (positionals.length || Object.keys(values).some((k) => k !== "json")) {
138
361
  throw new Error("components schema accepts only --json");
362
+ }
139
363
  context.output?.result(componentSnapshotSchema);
140
364
  (context.output?.log ?? console.log)(JSON.stringify(componentSnapshotSchema));
141
365
  return 0;
142
366
  }
143
- if (!["snapshot", "check", "diff", "build"].includes(sub ?? "") || positionals.length || (values.write && sub !== "snapshot") || (values.measure && sub !== "check"))
367
+ if (![
368
+ "snapshot",
369
+ "check",
370
+ "diff",
371
+ "build",
372
+ ].includes(sub ?? "") ||
373
+ positionals.length ||
374
+ (values.write && sub !== "snapshot") ||
375
+ (values.measure && sub !== "check")) {
144
376
  throw new Error("Usage: marwes components snapshot [--write] | check [--measure] | diff | build");
145
- if (sub === "build")
377
+ }
378
+ if (sub === "build") {
146
379
  throw new Error("Native library generation is gated: first prove preserved text/style instance overrides with the pinned Penpot API in an owner-authorized scratch file. No library was generated.");
380
+ }
147
381
  const pilot = readComponentPilot(context.rawConfig);
148
382
  const expectation = await computeExpectation(context.root, context.rawConfig, context.config.instance);
149
- if (expectation.problems.length)
383
+ if (expectation.problems.length) {
150
384
  throw new Error(expectation.problems.join("; "));
385
+ }
151
386
  const sourceHash = await componentSourceHash(context.root, pilot, expectation.fresh);
152
387
  let prior;
153
388
  try {
154
389
  prior = validateComponentSnapshot(JSON.parse(await boundedRead(context.root, pilot.snapshot)));
155
390
  }
156
391
  catch (error) {
157
- if (error.code !== "ENOENT")
392
+ if (error.code !== "ENOENT") {
158
393
  throw error;
394
+ }
159
395
  }
160
396
  if (sub === "check" && !values.measure) {
161
397
  const current = prior?.sourceHash === sourceHash;
162
- const report = { schemaVersion: 1, current, kind: "source-freshness", anatomyMeasured: false, nativeLibrary: "gated-on-instance-override-proof" };
398
+ const report = {
399
+ schemaVersion: 1,
400
+ current,
401
+ kind: "source-freshness",
402
+ anatomyMeasured: false,
403
+ nativeLibrary: "gated-on-instance-override-proof",
404
+ };
163
405
  context.output?.result(report);
164
406
  (context.output?.log ?? console.log)(JSON.stringify(report));
165
407
  return current ? 0 : 2;
166
408
  }
167
409
  // Explicit component commands alone execute the project renderer. Like theme modules, this is trusted local code, not a sandbox.
168
410
  const file = await resolveProjectInput(context.root, pilot.renderer);
169
- const module = await createJiti(file, { interopDefault: true, moduleCache: false, tryNative: false }).evalModule(await boundedRead(context.root, pilot.renderer), { filename: file, async: true, forceTranspile: true });
170
- if (typeof module.measure !== "function")
411
+ const module = (await createJiti(file, {
412
+ interopDefault: true,
413
+ moduleCache: false,
414
+ tryNative: false,
415
+ }).evalModule(await boundedRead(context.root, pilot.renderer), {
416
+ filename: file,
417
+ async: true,
418
+ forceTranspile: true,
419
+ }));
420
+ if (typeof module.measure !== "function") {
171
421
  throw new Error("Component renderer must export async measure(cases); install its optional browser dependency in the project");
422
+ }
172
423
  const fresh = await measureComponentPilot(pilot, expectation.fresh, sourceHash, module.measure);
173
- if (await componentSourceHash(context.root, pilot, (await computeExpectation(context.root, context.rawConfig, context.config.instance)).fresh) !== sourceHash)
424
+ if ((await componentSourceHash(context.root, pilot, (await computeExpectation(context.root, context.rawConfig, context.config.instance)).fresh)) !== sourceHash) {
174
425
  throw new Error("Component source changed during measurement");
175
- const complete = fresh.cases.every(c => c.measurement.environment.fontsReady && Object.values(c.measurement.parts).every(fields => Object.values(fields).every(v => v !== null)));
176
- const report = { schemaVersion: 1, complete, current: prior?.hash === fresh.hash, comparison: prior ? diffComponentSnapshots(prior, fresh) : null, snapshot: fresh, written: false };
426
+ }
427
+ const complete = fresh.cases.every((c) => c.measurement.environment.fontsReady &&
428
+ Object.values(c.measurement.parts).every((fields) => Object.values(fields).every((v) => v !== null)));
429
+ const report = {
430
+ schemaVersion: 1,
431
+ complete,
432
+ current: prior?.hash === fresh.hash,
433
+ comparison: prior ? diffComponentSnapshots(prior, fresh) : null,
434
+ snapshot: fresh,
435
+ written: false,
436
+ };
177
437
  if (sub === "snapshot" && values.write) {
178
- if (!complete)
438
+ if (!complete) {
179
439
  throw new Error("Incomplete measurements/fonts: previous component snapshot preserved");
440
+ }
180
441
  const data = JSON.stringify(fresh, null, 2) + "\n";
181
- if (context.redact && context.redact(data) !== data)
442
+ if (context.redact && context.redact(data) !== data) {
182
443
  throw new Error("Component evidence requires redaction; snapshot not published");
444
+ }
183
445
  const target = await resolveProjectOutput(context.root, pilot.snapshot);
184
- await mkdir(path.dirname(target), { recursive: true });
185
- await atomicWriteFile(target, data, { replace: true });
446
+ await mkdir(path.dirname(target), {
447
+ recursive: true,
448
+ });
449
+ await atomicWriteFile(target, data, {
450
+ replace: true,
451
+ });
186
452
  report.written = true;
187
453
  }
188
454
  context.output?.result(report);
@@ -190,7 +456,42 @@ export async function componentsCommand(context, args) {
190
456
  return !complete ? 3 : report.written || report.current ? 0 : 2;
191
457
  }
192
458
  export const componentSnapshotSchema = {
193
- $schema: "https://json-schema.org/draft/2020-12/schema", $id: "urn:penpair:schema:v1:component-pilot", type: "object",
194
- required: ["schemaVersion", "kind", "sourceHash", "hash", "marwesVersion", "cases", "nativeLibrary"],
195
- properties: { schemaVersion: { const: 1 }, kind: { const: "component-pilot" }, sourceHash: { type: "string" }, hash: { type: "string" }, marwesVersion: { type: "string" }, cases: { type: "array", minItems: 6, maxItems: 60 }, nativeLibrary: { const: "gated-on-instance-override-proof" } }, additionalProperties: true,
459
+ $schema: "https://json-schema.org/draft/2020-12/schema",
460
+ $id: "urn:penpair:schema:v1:component-pilot",
461
+ type: "object",
462
+ required: [
463
+ "schemaVersion",
464
+ "kind",
465
+ "sourceHash",
466
+ "hash",
467
+ "marwesVersion",
468
+ "cases",
469
+ "nativeLibrary",
470
+ ],
471
+ properties: {
472
+ schemaVersion: {
473
+ const: 1,
474
+ },
475
+ kind: {
476
+ const: "component-pilot",
477
+ },
478
+ sourceHash: {
479
+ type: "string",
480
+ },
481
+ hash: {
482
+ type: "string",
483
+ },
484
+ marwesVersion: {
485
+ type: "string",
486
+ },
487
+ cases: {
488
+ type: "array",
489
+ minItems: 6,
490
+ maxItems: 60,
491
+ },
492
+ nativeLibrary: {
493
+ const: "gated-on-instance-override-proof",
494
+ },
495
+ },
496
+ additionalProperties: true,
196
497
  };