@vectoriox/iox-ui 5.54.0 → 5.55.0

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.
@@ -1,5 +1,5 @@
1
- import { MOOD_IDS, MOOD_LABELS, STYLE_PRESETS, flavorPackById, parseFontFamilies, SPEC_AXES, parseHexColor, rgbToOklab, presetChoices } from '@vectoriox/iox-ui/design-core';
2
- import { shapeKeys, requiredShapeKeys, PathIssueCollector, isPlainObject, pathAt, FACT_KEYS, SPEC_SITE_LIBRARY, patternById, SITE_FACT_LABELS, SITE_TEMPLATES, STUDIO_TEMPLATE } from '@vectoriox/iox-ui/generation-core';
1
+ import { MOOD_IDS, MOOD_LABELS, STYLE_PRESETS, flavorPackById, parseFontFamilies, SPEC_AXES, parseHexColor, rgbToOklab, presetChoices, toHexColor, stylePresetById, auditDesignContrast, designTokenRows } from '@vectoriox/iox-ui/design-core';
2
+ import { shapeKeys, requiredShapeKeys, PathIssueCollector, isPlainObject, pathAt, FACT_KEYS, jsonText, jsonOneOf, jsonListOf, jsonShapedObject, jsonClosedObject, describeShapeKey, SPEC_SITE_LIBRARY, patternById, SITE_FACT_LABELS, SITE_TEMPLATES, STUDIO_TEMPLATE, jsonInteger } from '@vectoriox/iox-ui/generation-core';
3
3
 
4
4
  // GENERATED from iox-ai-guidance flavor-spec.v1.json — do not edit; re-run scripts/gen-flavor-spec.mjs
5
5
  // (npm run gen:flavor-spec). The JSON is itself generated by iox-ai-guidance
@@ -3794,7 +3794,7 @@ const ALTERNATIVES = 2;
3794
3794
  const SHAPE = SPEC_ANALYSIS_SHAPE;
3795
3795
  const KEYS = Object.fromEntries(Object.keys(SHAPE).map(k => [k, shapeKeys(SHAPE[k])]));
3796
3796
  const REQUIRED = Object.fromEntries(Object.keys(SHAPE).map(k => [k, requiredShapeKeys(SHAPE[k])]));
3797
- class Collector extends PathIssueCollector {
3797
+ let Collector$1 = class Collector extends PathIssueCollector {
3798
3798
  constructor() {
3799
3799
  super('analysis', { shape: AnalysisIssueCode.Shape, unknownKey: AnalysisIssueCode.UnknownKey, duplicate: AnalysisIssueCode.Duplicate });
3800
3800
  }
@@ -3821,11 +3821,11 @@ class Collector extends PathIssueCollector {
3821
3821
  this.add(AnalysisIssueCode.Shape, `${pathAt(path, key)}[${i}]`, 'must be a string'); });
3822
3822
  return list.filter((v) => typeof v === 'string');
3823
3823
  }
3824
- }
3824
+ };
3825
3825
  /** Whitespace and case never decide whether words are the prompt's. */
3826
3826
  const normalise = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim();
3827
3827
  function validateSiteAnalysis(value, ctx) {
3828
- const c = new Collector();
3828
+ const c = new Collector$1();
3829
3829
  const a = c.object(value, 'analysis', '');
3830
3830
  if (!a)
3831
3831
  return { valid: false, issues: c.issues };
@@ -3907,33 +3907,14 @@ function validateSiteAnalysis(value, ctx) {
3907
3907
  }
3908
3908
  /** The name a provider shows the format under (`json_schema.name`). */
3909
3909
  const SITE_ANALYSIS_SCHEMA_NAME = 'site_analysis';
3910
- const OPTIONAL_MARK = '?';
3911
- const text = () => ({ type: 'string' });
3912
- const oneOf = (values) => ({ type: 'string', enum: [...values] });
3913
- const listOf = (items) => ({ type: 'array', items });
3914
- /** The shape table's own words for a key — the description the model reads. */
3915
- function describe(shape, key) {
3916
- const entry = SHAPE[shape];
3917
- return String(entry[key] ?? entry[`${key}${OPTIONAL_MARK}`] ?? '');
3918
- }
3910
+ const text = jsonText;
3911
+ const oneOf = jsonOneOf;
3912
+ const listOf = jsonListOf;
3919
3913
  /** An object of `shape`: exactly its keys, its required ones required, each described by the table. */
3920
- function shaped(shape, properties) {
3921
- return {
3922
- type: 'object',
3923
- properties: Object.fromEntries(Object.entries(properties).map(([key, schema]) => [key, { ...schema, description: describe(shape, key) }])),
3924
- required: requiredShapeKeys(SHAPE[shape]),
3925
- additionalProperties: false,
3926
- };
3927
- }
3914
+ const shaped = (shape, properties) => jsonShapedObject(SHAPE[shape], properties, shape);
3928
3915
  /** `{ [fact]: factRead }`: the closed fact keys, each optional — a fact the prompt does not state is left out. */
3929
3916
  function factsSchema(factRead) {
3930
- return {
3931
- type: 'object',
3932
- properties: Object.fromEntries(FACT_KEYS.map(key => [key, factRead])),
3933
- required: [],
3934
- additionalProperties: false,
3935
- description: describe('analysis', 'facts'),
3936
- };
3917
+ return jsonClosedObject(Object.fromEntries(FACT_KEYS.map(key => [key, factRead])), [], describeShapeKey(SHAPE['analysis'], 'facts'));
3937
3918
  }
3938
3919
  function siteAnalysisJsonSchema() {
3939
3920
  const factRead = shaped('factRead', { value: text(), quote: text() });
@@ -4368,6 +4349,445 @@ function structureFor(typeId) {
4368
4349
  return { structure, template: own ?? STUDIO_TEMPLATE, fallback: !own };
4369
4350
  }
4370
4351
 
4352
+ /** At most this many colours (the StyleRead's palette). */
4353
+ const PALETTE_SIZE = 6;
4354
+ /** Channels are bucketed to this many bits before the cut, so noise never decides a colour. */
4355
+ const BUCKET_BITS = 5;
4356
+ const BUCKET_SHIFT = 8 - BUCKET_BITS;
4357
+ /** The sampling ceiling for a picture — every n-th pixel, so a 12 MP photo costs what a thumbnail does. */
4358
+ const MAX_PIXEL_SAMPLES = 20_000;
4359
+ /** A pixel under this alpha is not part of the picture. */
4360
+ const MIN_ALPHA = 128;
4361
+ /** OKLab lightness / chroma thresholds the reading speaks in. */
4362
+ const PALETTE_THRESHOLDS = {
4363
+ /** Above: a light colour. */
4364
+ light: 0.72,
4365
+ /** Below: a dark colour. */
4366
+ dark: 0.38,
4367
+ /** At least this share of light (or dark) makes the ground light (or dark); otherwise mixed. */
4368
+ groundShare: 0.55,
4369
+ /** A brand candidate is at least this colourful… */
4370
+ brandChroma: 0.08,
4371
+ /** …and neither near-white nor near-black. */
4372
+ brandMinL: 0.25,
4373
+ brandMaxL: 0.92,
4374
+ /** Two palette colours closer than this (OKLab distance) are one colour. */
4375
+ mergeDistance: 0.035,
4376
+ };
4377
+ /** Every n-th opaque pixel of an RGBA buffer (canvas `ImageData.data`, a decoder's pixels), weight 1 each. */
4378
+ function pixelSamples(rgba, width, height, max = MAX_PIXEL_SAMPLES) {
4379
+ const pixels = Math.max(0, Math.floor(width) * Math.floor(height));
4380
+ const step = Math.max(1, Math.ceil(pixels / max));
4381
+ const out = [];
4382
+ for (let p = 0; p < pixels; p += step) {
4383
+ const i = p * 4;
4384
+ if (rgba[i + 3] < MIN_ALPHA)
4385
+ continue;
4386
+ out.push({ r: rgba[i], g: rgba[i + 1], b: rgba[i + 2], weight: 1 });
4387
+ }
4388
+ return out;
4389
+ }
4390
+ /** A palette (hex + share) back to samples — what merging two readings measures. */
4391
+ function paletteSamples(palette, weight = 1) {
4392
+ return palette.flatMap(c => {
4393
+ const rgb = parseHexColor(c.hex);
4394
+ return rgb ? [{ r: Math.round(rgb.r * 255), g: Math.round(rgb.g * 255), b: Math.round(rgb.b * 255), weight: c.share * weight }] : [];
4395
+ });
4396
+ }
4397
+ const CHANNELS = ['r', 'g', 'b'];
4398
+ const boxOf = (colours) => ({ colours, weight: colours.reduce((s, c) => s + c.weight, 0) });
4399
+ function range(box, ch) {
4400
+ let lo = 255;
4401
+ let hi = 0;
4402
+ for (const c of box.colours) {
4403
+ lo = Math.min(lo, c[ch]);
4404
+ hi = Math.max(hi, c[ch]);
4405
+ }
4406
+ return hi - lo;
4407
+ }
4408
+ /** The channel a box is widest in (ties: r, then g, then b). */
4409
+ function widest(box) {
4410
+ return CHANNELS.reduce((best, ch) => {
4411
+ const span = range(box, ch);
4412
+ return span > best.span ? { ch, span } : best;
4413
+ }, { ch: 'r', span: -1 });
4414
+ }
4415
+ /** Split at the weighted median of its widest channel (a stable sort, so ties never depend on the runtime). */
4416
+ function split(box) {
4417
+ const { ch, span } = widest(box);
4418
+ if (span <= 0 || box.colours.length < 2)
4419
+ return null;
4420
+ const sorted = [...box.colours].sort((a, b) => a[ch] - b[ch] || a.r - b.r || a.g - b.g || a.b - b.b);
4421
+ let acc = 0;
4422
+ let cut = 1;
4423
+ for (let i = 0; i < sorted.length - 1; i++) {
4424
+ acc += sorted[i].weight;
4425
+ cut = i + 1;
4426
+ if (acc >= box.weight / 2)
4427
+ break;
4428
+ }
4429
+ return [boxOf(sorted.slice(0, cut)), boxOf(sorted.slice(cut))];
4430
+ }
4431
+ /** Samples bucketed (BUCKET_BITS per channel), each bucket's weighted mean colour kept. */
4432
+ function bucket(samples) {
4433
+ const buckets = new Map();
4434
+ for (const s of samples) {
4435
+ if (!(s.weight > 0))
4436
+ continue;
4437
+ const key = ((s.r >> BUCKET_SHIFT) << (2 * BUCKET_BITS)) | ((s.g >> BUCKET_SHIFT) << BUCKET_BITS) | (s.b >> BUCKET_SHIFT);
4438
+ const b = buckets.get(key) ?? { r: 0, g: 0, b: 0, weight: 0 };
4439
+ b.r += s.r * s.weight;
4440
+ b.g += s.g * s.weight;
4441
+ b.b += s.b * s.weight;
4442
+ b.weight += s.weight;
4443
+ buckets.set(key, b);
4444
+ }
4445
+ return [...buckets.entries()].sort(([a], [b]) => a - b).map(([, b]) => ({ r: b.r / b.weight, g: b.g / b.weight, b: b.b / b.weight, weight: b.weight }));
4446
+ }
4447
+ function meanHex(box) {
4448
+ const sum = box.colours.reduce((s, c) => ({ r: s.r + c.r * c.weight, g: s.g + c.g * c.weight, b: s.b + c.b * c.weight }), { r: 0, g: 0, b: 0 });
4449
+ return toHexColor({ r: sum.r / box.weight / 255, g: sum.g / box.weight / 255, b: sum.b / box.weight / 255 }).toUpperCase();
4450
+ }
4451
+ const oklabOf = (hex) => rgbToOklab(parseHexColor(hex));
4452
+ const distance = (a, b) => { const x = oklabOf(a); const y = oklabOf(b); return Math.hypot(x.L - y.L, x.a - y.a, x.b - y.b); };
4453
+ const SHARE_PRECISION = 1e4;
4454
+ /** Weighted samples → ≤ 6 colours, largest share first (ties by hex), shares adding up to 1. */
4455
+ function measurePalette(samples, size = PALETTE_SIZE) {
4456
+ const colours = bucket(samples);
4457
+ if (!colours.length)
4458
+ return [];
4459
+ let boxes = [boxOf(colours)];
4460
+ while (boxes.length < size) {
4461
+ // The box to cut: the most weight × span (ties: the earlier box).
4462
+ let best = -1;
4463
+ let bestScore = 0;
4464
+ boxes.forEach((box, i) => {
4465
+ const score = box.weight * widest(box).span;
4466
+ if (score > bestScore) {
4467
+ best = i;
4468
+ bestScore = score;
4469
+ }
4470
+ });
4471
+ const halves = best >= 0 ? split(boxes[best]) : null;
4472
+ if (!halves)
4473
+ break;
4474
+ boxes = [...boxes.slice(0, best), ...halves, ...boxes.slice(best + 1)];
4475
+ }
4476
+ const total = boxes.reduce((s, b) => s + b.weight, 0);
4477
+ const merged = [];
4478
+ for (const box of [...boxes].sort((a, b) => b.weight - a.weight)) {
4479
+ const hex = meanHex(box);
4480
+ const near = merged.find(m => distance(m.hex, hex) < PALETTE_THRESHOLDS.mergeDistance);
4481
+ if (near)
4482
+ near.weight += box.weight;
4483
+ else
4484
+ merged.push({ hex, weight: box.weight });
4485
+ }
4486
+ return merged
4487
+ .map(m => ({ hex: m.hex, share: Math.round((m.weight / total) * SHARE_PRECISION) / SHARE_PRECISION }))
4488
+ .sort((a, b) => b.share - a.share || a.hex.localeCompare(b.hex));
4489
+ }
4490
+ /** Light, dark or mixed: the share of the palette that is light (or dark). */
4491
+ function paletteGround(palette) {
4492
+ let light = 0;
4493
+ let dark = 0;
4494
+ for (const c of palette) {
4495
+ const { L } = oklabOf(c.hex);
4496
+ if (L >= PALETTE_THRESHOLDS.light)
4497
+ light += c.share;
4498
+ else if (L <= PALETTE_THRESHOLDS.dark)
4499
+ dark += c.share;
4500
+ }
4501
+ if (light >= PALETTE_THRESHOLDS.groundShare)
4502
+ return StyleReadGround.Light;
4503
+ if (dark >= PALETTE_THRESHOLDS.groundShare)
4504
+ return StyleReadGround.Dark;
4505
+ return StyleReadGround.Mixed;
4506
+ }
4507
+ /** The most frequent saturated colour that is neither near-white nor near-black, or undefined. */
4508
+ function paletteBrand(palette) {
4509
+ return palette.find(c => {
4510
+ const { L, a, b } = oklabOf(c.hex);
4511
+ return Math.hypot(a, b) >= PALETTE_THRESHOLDS.brandChroma && L >= PALETTE_THRESHOLDS.brandMinL && L <= PALETTE_THRESHOLDS.brandMaxL;
4512
+ })?.hex;
4513
+ }
4514
+ /** The whole reading of a set of samples: the palette, its ground, its brand candidate. */
4515
+ function readPalette(samples) {
4516
+ const palette = measurePalette(samples);
4517
+ const brand = paletteBrand(palette);
4518
+ return { palette, ground: paletteGround(palette), ...(brand ? { brand } : {}) };
4519
+ }
4520
+
4521
+ /** Every mode `presetId` offers passes the audit with `seed`. */
4522
+ function seedPassesPreset(presetId, seed) {
4523
+ const preset = stylePresetById(presetId);
4524
+ if (!preset || !parseHexColor(seed))
4525
+ return { passes: false, failsIn: [] };
4526
+ const failsIn = preset.modes.filter(mode => {
4527
+ const choices = presetChoices(preset.id, mode);
4528
+ return auditDesignContrast(designTokenRows({ ...choices, seed }), mode).some(f => !f.pass);
4529
+ });
4530
+ return { passes: failsIn.length === 0, failsIn: [...failsIn] };
4531
+ }
4532
+ /** The seed a preset starts from, given a reading (or none). Unknown preset → null. */
4533
+ function referenceSeed(read, presetId) {
4534
+ const preset = stylePresetById(presetId);
4535
+ if (!preset)
4536
+ return null;
4537
+ const reference = read?.brand && parseHexColor(read.brand) ? read.brand : null;
4538
+ if (!reference)
4539
+ return { seed: preset.seed, reference: null, used: false, failsIn: [] };
4540
+ const { passes, failsIn } = seedPassesPreset(preset.id, reference);
4541
+ return passes ? { seed: reference, reference, used: true, failsIn: [] } : { seed: preset.seed, reference, used: false, failsIn };
4542
+ }
4543
+
4544
+ /**
4545
+ * The READING call (AI plan §3d step 1, §3k R2, M1-1): ONE `text.fast` call reads the owner's words AND the pictures
4546
+ * they attached (image input) and answers with the SiteAnalysis and — when there are pictures — which ones are the
4547
+ * owner's own (Your photos) and which are pictures they like (references, style only), plus what the liked pictures
4548
+ * FEEL like: moods, the lettering, corners, spacing. Never a colour: the palette is MEASURED (`palette.ts`, R-D2).
4549
+ * The format is built from `READING_SHAPE` (keys + their explanations), the gate reads the same table. Pure.
4550
+ */
4551
+ /** Which group a picture belongs to (R-D3: from the words; own when the words don't say). */
4552
+ var PictureRole;
4553
+ (function (PictureRole) {
4554
+ PictureRole["Own"] = "own";
4555
+ PictureRole["Reference"] = "reference";
4556
+ })(PictureRole || (PictureRole = {}));
4557
+ /** At most this many pictures go to the reading call as image input (§3d: "up to 6 pictures"). */
4558
+ const MAX_READ_PICTURES = 6;
4559
+ const READING_SHAPE = {
4560
+ reading: {
4561
+ analysis: 'analysis — the reading of the words',
4562
+ pictures: 'pictures — the pictures attached, in order',
4563
+ },
4564
+ pictures: {
4565
+ files: 'fileRole[] — one per attached picture, in the order given',
4566
+ style: 'pictureStyle — what the pictures the owner LIKES (role reference) feel like; empty moods when there are none',
4567
+ },
4568
+ fileRole: {
4569
+ index: 'integer — the picture, 0-based, in the order given',
4570
+ role: "'own' | 'reference' — own: the owner's work, place, products or people, for the site; reference: a picture they like, for style only (\"a site like these\"); own when the words don't say",
4571
+ },
4572
+ pictureStyle: {
4573
+ moods: 'string[] — ≤ 3 moods the liked pictures feel like',
4574
+ 'headings?': "'serif' | 'sans' | 'display' | 'mono' — the lettering they show, when they show some",
4575
+ 'shape?': 'string — design axes.shapes: their corners',
4576
+ 'density?': 'string — design axes.densities: how much room things have',
4577
+ },
4578
+ };
4579
+ const SITE_READING_SCHEMA_NAME = 'site_reading';
4580
+ /** The reading's format: the analysis alone when no picture was attached, else the analysis + the pictures. */
4581
+ function siteReadingJsonSchema(pictureCount) {
4582
+ if (pictureCount <= 0)
4583
+ return siteAnalysisJsonSchema();
4584
+ const fileRole = jsonShapedObject(READING_SHAPE['fileRole'], { index: jsonInteger(), role: jsonOneOf(Object.values(PictureRole)) }, 'fileRole');
4585
+ const style = jsonShapedObject(READING_SHAPE['pictureStyle'], {
4586
+ moods: jsonListOf(jsonOneOf(MOOD_IDS)),
4587
+ headings: jsonOneOf(Object.values(StyleReadHeadings)),
4588
+ shape: jsonOneOf(Object.keys(SPEC_AXES.shapes)),
4589
+ density: jsonOneOf(Object.keys(SPEC_AXES.densities)),
4590
+ }, 'pictureStyle');
4591
+ const pictures = jsonShapedObject(READING_SHAPE['pictures'], { files: jsonListOf(fileRole), style }, 'pictures');
4592
+ return jsonShapedObject(READING_SHAPE['reading'], { analysis: siteAnalysisJsonSchema(), pictures }, 'reading');
4593
+ }
4594
+ var ReadingIssueCode;
4595
+ (function (ReadingIssueCode) {
4596
+ ReadingIssueCode["Shape"] = "shape";
4597
+ ReadingIssueCode["UnknownKey"] = "unknown-key";
4598
+ ReadingIssueCode["Duplicate"] = "duplicate";
4599
+ ReadingIssueCode["InvalidValue"] = "invalid-value";
4600
+ /** A picture named twice, or one left out. */
4601
+ ReadingIssueCode["Pictures"] = "pictures";
4602
+ })(ReadingIssueCode || (ReadingIssueCode = {}));
4603
+ class Collector extends PathIssueCollector {
4604
+ constructor() {
4605
+ super('reading', { shape: ReadingIssueCode.Shape, unknownKey: ReadingIssueCode.UnknownKey, duplicate: ReadingIssueCode.Duplicate });
4606
+ }
4607
+ object(value, shape, path) {
4608
+ if (!isPlainObject(value)) {
4609
+ this.add(ReadingIssueCode.Shape, path, 'must be an object');
4610
+ return null;
4611
+ }
4612
+ this.keys(value, shapeKeys(READING_SHAPE[shape]), path);
4613
+ for (const key of requiredShapeKeys(READING_SHAPE[shape]))
4614
+ if (value[key] === undefined)
4615
+ this.add(ReadingIssueCode.Shape, pathAt(path, key), 'is required');
4616
+ return value;
4617
+ }
4618
+ oneOf(obj, key, allowed, path) {
4619
+ const v = obj[key];
4620
+ if (v !== undefined && (typeof v !== 'string' || !allowed.includes(v)))
4621
+ this.add(ReadingIssueCode.InvalidValue, pathAt(path, key), `must be one of ${allowed.join(', ')}`);
4622
+ }
4623
+ }
4624
+ /** The pictures' part: every picture named once, a role each, moods from the closed list. */
4625
+ function validatePictures(c, raw, count) {
4626
+ const pictures = c.object(raw, 'pictures', 'pictures');
4627
+ if (!pictures)
4628
+ return;
4629
+ const files = c.array(pictures, 'files', 'pictures');
4630
+ const seen = new Set();
4631
+ files.forEach((f, i) => {
4632
+ const path = `pictures.files[${i}]`;
4633
+ const file = c.object(f, 'fileRole', path);
4634
+ if (!file)
4635
+ return;
4636
+ c.oneOf(file, 'role', Object.values(PictureRole), path);
4637
+ const index = file['index'];
4638
+ if (!Number.isInteger(index) || index < 0 || index >= count)
4639
+ c.add(ReadingIssueCode.Pictures, `${path}.index`, `must be a picture 0–${count - 1}`);
4640
+ else if (seen.has(index))
4641
+ c.add(ReadingIssueCode.Pictures, `${path}.index`, `picture ${index} is named twice`);
4642
+ else
4643
+ seen.add(index);
4644
+ });
4645
+ if (Array.isArray(pictures['files']) && seen.size !== count)
4646
+ c.add(ReadingIssueCode.Pictures, 'pictures.files', `name each of the ${count} pictures once`);
4647
+ const style = c.object(pictures['style'], 'pictureStyle', 'pictures.style');
4648
+ if (!style)
4649
+ return;
4650
+ const moods = c.array(style, 'moods', 'pictures.style');
4651
+ if (moods.length > MAX_ANALYSIS_MOODS)
4652
+ c.add(ReadingIssueCode.InvalidValue, 'pictures.style.moods', `at most ${MAX_ANALYSIS_MOODS}`);
4653
+ moods.forEach((m, i) => { if (!MOOD_IDS.includes(m))
4654
+ c.add(ReadingIssueCode.InvalidValue, `pictures.style.moods[${i}]`, `"${String(m)}" is not a mood`); });
4655
+ c.oneOf(style, 'headings', Object.values(StyleReadHeadings), 'pictures.style');
4656
+ c.oneOf(style, 'shape', Object.keys(SPEC_AXES.shapes), 'pictures.style');
4657
+ c.oneOf(style, 'density', Object.keys(SPEC_AXES.densities), 'pictures.style');
4658
+ }
4659
+ /** The reading's gate: the analysis by `validateSiteAnalysis` (paths under `analysis.` when pictures came too), the pictures by `READING_SHAPE`. */
4660
+ function validateSiteReading(value, ctx) {
4661
+ if (ctx.pictureCount <= 0)
4662
+ return validateSiteAnalysis(value, { prompt: ctx.prompt });
4663
+ const c = new Collector();
4664
+ const reading = c.object(value, 'reading', '');
4665
+ if (!reading)
4666
+ return { valid: false, issues: c.issues };
4667
+ const analysis = validateSiteAnalysis(reading['analysis'], { prompt: ctx.prompt });
4668
+ const issues = analysis.issues.map(i => ({ ...i, path: pathAt('analysis', i.path) }));
4669
+ validatePictures(c, reading['pictures'], ctx.pictureCount);
4670
+ issues.push(...c.issues);
4671
+ return { valid: issues.length === 0, issues };
4672
+ }
4673
+ /** A valid reading's answer → `SiteReading` (the analysis alone answers `{ analysis }`). */
4674
+ function decodeSiteReading(value, pictureCount) {
4675
+ if (pictureCount <= 0)
4676
+ return { analysis: value };
4677
+ return value;
4678
+ }
4679
+ /** The liked pictures as a StyleRead: their palettes measured together, what the model saw of them. */
4680
+ function pictureStyleRead(pictures, style) {
4681
+ if (!pictures.length)
4682
+ return null;
4683
+ const reading = readPalette(pictures.flatMap(p => paletteSamples(p.palette)));
4684
+ return {
4685
+ sources: pictures.map(p => ({ kind: StyleReadSourceKind.Image, ref: p.ref })),
4686
+ palette: reading.palette,
4687
+ ...(reading.brand ? { brand: reading.brand } : {}),
4688
+ ground: reading.ground,
4689
+ moods: [...(style?.moods ?? [])].slice(0, MAX_ANALYSIS_MOODS),
4690
+ ...(style?.headings ? { headings: style.headings } : {}),
4691
+ ...(style?.shape ? { shape: style.shape } : {}),
4692
+ ...(style?.density ? { density: style.density } : {}),
4693
+ };
4694
+ }
4695
+ /** The most frequent defined value (ties: the first reading's). */
4696
+ function mostCommon(values) {
4697
+ const counts = new Map();
4698
+ for (const v of values)
4699
+ if (v !== undefined)
4700
+ counts.set(v, (counts.get(v) ?? 0) + 1);
4701
+ let best;
4702
+ let n = 0;
4703
+ for (const [v, k] of counts)
4704
+ if (k > n) {
4705
+ best = v;
4706
+ n = k;
4707
+ }
4708
+ return best;
4709
+ }
4710
+ /**
4711
+ * Every reading of one generation (the pictures', each site's) → ONE StyleRead: palettes measured together (each
4712
+ * reading weighs the same), the ground re-read from that palette, moods by how often they were seen (≤ 3), the rest the
4713
+ * most common answer. Null when nothing was read.
4714
+ */
4715
+ function mergeStyleReads(reads) {
4716
+ const all = reads.filter((r) => !!r);
4717
+ if (!all.length)
4718
+ return null;
4719
+ if (all.length === 1)
4720
+ return all[0];
4721
+ const reading = readPalette(all.flatMap(r => paletteSamples(r.palette)));
4722
+ const moodCount = new Map();
4723
+ all.forEach(r => r.moods.forEach(m => moodCount.set(m, (moodCount.get(m) ?? 0) + 1)));
4724
+ const moods = [...moodCount.entries()].sort((a, b) => b[1] - a[1]).map(([m]) => m).slice(0, MAX_ANALYSIS_MOODS);
4725
+ const pick = (key) => mostCommon(all.map(r => r[key]));
4726
+ const headings = pick('headings');
4727
+ const shape = pick('shape');
4728
+ const density = pick('density');
4729
+ const motion = pick('motion');
4730
+ return {
4731
+ sources: all.flatMap(r => r.sources),
4732
+ palette: reading.palette,
4733
+ ...(reading.brand ? { brand: reading.brand } : {}),
4734
+ ground: reading.palette.length ? reading.ground : StyleReadGround.Mixed,
4735
+ moods,
4736
+ ...(headings ? { headings } : {}),
4737
+ ...(shape ? { shape } : {}),
4738
+ ...(density ? { density } : {}),
4739
+ ...(motion ? { motion } : {}),
4740
+ };
4741
+ }
4742
+
4743
+ /**
4744
+ * A generation as the screens read it (AI plan "M1 pipeline — the screens contract"): what iox-cms's
4745
+ * `GET …/ai/generations/:id` answers and the Thinking · Analysis · Design · Plan · Build screens render. ONE
4746
+ * definition: iox-cms builds it, cms-client types its client with it. Types and enums only — no transport.
4747
+ */
4748
+ /** Where one reader is (Thinking's three lines). */
4749
+ var ReaderState;
4750
+ (function (ReaderState) {
4751
+ ReaderState["Wait"] = "wait";
4752
+ ReaderState["Run"] = "run";
4753
+ ReaderState["Done"] = "done";
4754
+ /** Nothing to read (no pictures, no address). */
4755
+ ReaderState["Skipped"] = "skipped";
4756
+ /** Refused on purpose (robots.txt, a private address…) — said in one plain line, never blocks the reading. */
4757
+ ReaderState["Refused"] = "refused";
4758
+ ReaderState["Failed"] = "failed";
4759
+ })(ReaderState || (ReaderState = {}));
4760
+ /** Why a website address was not read (the screens' i18n `ai.siteRead.<reason>`). */
4761
+ var SiteReadRefusal;
4762
+ (function (SiteReadRefusal) {
4763
+ /** robots.txt disallows our user agent (`IOX-Import`). */
4764
+ SiteReadRefusal["Robots"] = "robots";
4765
+ SiteReadRefusal["InvalidAddress"] = "invalid-address";
4766
+ SiteReadRefusal["PrivateAddress"] = "private-address";
4767
+ SiteReadRefusal["Unreachable"] = "unreachable";
4768
+ SiteReadRefusal["Timeout"] = "timeout";
4769
+ SiteReadRefusal["TooLarge"] = "too-large";
4770
+ SiteReadRefusal["NotHtml"] = "not-html";
4771
+ SiteReadRefusal["HttpError"] = "http-error";
4772
+ SiteReadRefusal["TooManyRedirects"] = "redirects";
4773
+ })(SiteReadRefusal || (SiteReadRefusal = {}));
4774
+ /** What a sentence in the Plan prompt box did. */
4775
+ var PlanChangeOutcome;
4776
+ (function (PlanChangeOutcome) {
4777
+ PlanChangeOutcome["Applied"] = "applied";
4778
+ /** The closed operations cannot say it — one line, the map unchanged (the call still cost). */
4779
+ PlanChangeOutcome["Cannot"] = "cannot";
4780
+ /** Its edits did not make a valid plan, twice — the map unchanged. */
4781
+ PlanChangeOutcome["Invalid"] = "invalid";
4782
+ })(PlanChangeOutcome || (PlanChangeOutcome = {}));
4783
+ /** The longest message, the most files and addresses a generation starts from (Start's limits). */
4784
+ const GENERATION_LIMITS = {
4785
+ promptChars: 4_000,
4786
+ files: 12,
4787
+ addresses: 3,
4788
+ sentenceChars: 500,
4789
+ };
4790
+
4371
4791
  /**
4372
4792
  * @vectoriox/iox-ui/harness-core — the design HARNESS (AI plan rev 3 §3f, decision N18), FRAMEWORK-FREE: the 30
4373
4793
  * kinds of business and their rules (generated 1:1 from iox-ai-guidance `flavor-spec.v1.json` `harness`), the
@@ -4382,5 +4802,5 @@ function structureFor(typeId) {
4382
4802
  * Generated bundle index. Do not edit.
4383
4803
  */
4384
4804
 
4385
- export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, NEEDS, NEED_IDS, NeedSource, PresetFit, RANK_WEIGHTS, READ_ANIMATE, READ_WEIGHTS, RankReason, SITE_ANALYSIS_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisPromptContext, businessTypeById, calmerChoice, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, motionAdvice, motionFor, normalizeAddress, oklchHue, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, siteAnalysisJsonSchema, structureFor, userAvoidRule, validateSiteAnalysis, withoutAddress };
4805
+ export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, GENERATION_LIMITS, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, MAX_PIXEL_SAMPLES, MAX_READ_PICTURES, NEEDS, NEED_IDS, NeedSource, PALETTE_SIZE, PALETTE_THRESHOLDS, PictureRole, PlanChangeOutcome, PresetFit, RANK_WEIGHTS, READING_SHAPE, READ_ANIMATE, READ_WEIGHTS, RankReason, ReaderState, ReadingIssueCode, SITE_ANALYSIS_SCHEMA_NAME, SITE_READING_SCHEMA_NAME, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, SiteReadRefusal, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisPromptContext, businessTypeById, calmerChoice, decodeSiteReading, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, measurePalette, mergeStyleReads, motionAdvice, motionFor, normalizeAddress, oklchHue, paletteBrand, paletteGround, paletteSamples, pictureStyleRead, pixelSamples, presetHasSerifHeadings, presetLook, presetReason, rankStylePresets, rankWithReferences, readPalette, referenceSeed, seedPassesPreset, siteAnalysisJsonSchema, siteReadingJsonSchema, structureFor, userAvoidRule, validateSiteAnalysis, validateSiteReading, withoutAddress };
4386
4806
  //# sourceMappingURL=vectoriox-iox-ui-harness-core.mjs.map