@neatico/visual-core 0.14.3 → 0.14.64

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 (68) hide show
  1. package/dist/candidateRanking.d.ts +31 -0
  2. package/dist/candidateRanking.js +55 -0
  3. package/dist/cjs/candidateRanking.js +61 -0
  4. package/dist/cjs/composition.js +62 -36
  5. package/dist/cjs/compositionNumbers.js +111 -0
  6. package/dist/cjs/compositionScenes.js +91 -38
  7. package/dist/cjs/compositionUseCases.js +145 -0
  8. package/dist/cjs/content.js +574 -120
  9. package/dist/cjs/contentBrief.js +82 -0
  10. package/dist/cjs/designBlueprint.js +94 -19
  11. package/dist/cjs/designSystem.js +536 -77
  12. package/dist/cjs/editorial.js +76 -20
  13. package/dist/cjs/fontCatalog.js +81 -23
  14. package/dist/cjs/formalCompositions.js +120 -0
  15. package/dist/cjs/index.js +13 -0
  16. package/dist/cjs/layoutContracts.js +57 -11
  17. package/dist/cjs/layouts.js +5 -92
  18. package/dist/cjs/nativeSocialParameters.js +145 -0
  19. package/dist/cjs/pagination.js +27 -7
  20. package/dist/cjs/parameterDefaults.js +20 -0
  21. package/dist/cjs/recipeParameters.js +304 -0
  22. package/dist/cjs/socialLayouts.js +127 -387
  23. package/dist/cjs/styleManifest.js +107 -0
  24. package/dist/cjs/text.js +12 -2
  25. package/dist/cjs/textPresentation.js +68 -0
  26. package/dist/composition.d.ts +4 -4
  27. package/dist/composition.js +62 -36
  28. package/dist/compositionNumbers.d.ts +4 -0
  29. package/dist/compositionNumbers.js +108 -0
  30. package/dist/compositionScenes.d.ts +2 -2
  31. package/dist/compositionScenes.js +91 -38
  32. package/dist/compositionUseCases.d.ts +1 -0
  33. package/dist/compositionUseCases.js +142 -0
  34. package/dist/content.d.ts +65 -20
  35. package/dist/content.js +575 -121
  36. package/dist/contentBrief.d.ts +16 -0
  37. package/dist/contentBrief.js +78 -0
  38. package/dist/designBlueprint.d.ts +4 -4
  39. package/dist/designBlueprint.js +95 -20
  40. package/dist/designSystem.d.ts +11 -7
  41. package/dist/designSystem.js +537 -79
  42. package/dist/editorial.d.ts +1 -1
  43. package/dist/editorial.js +77 -21
  44. package/dist/fontCatalog.d.ts +1 -1
  45. package/dist/fontCatalog.js +81 -23
  46. package/dist/formalCompositions.d.ts +26 -0
  47. package/dist/formalCompositions.js +116 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.js +10 -0
  50. package/dist/layoutContracts.d.ts +3 -3
  51. package/dist/layoutContracts.js +58 -12
  52. package/dist/layouts.d.ts +2 -2
  53. package/dist/layouts.js +5 -92
  54. package/dist/nativeSocialParameters.d.ts +38 -0
  55. package/dist/nativeSocialParameters.js +139 -0
  56. package/dist/pagination.js +27 -7
  57. package/dist/parameterDefaults.d.ts +2 -0
  58. package/dist/parameterDefaults.js +17 -0
  59. package/dist/recipeParameters.d.ts +34 -0
  60. package/dist/recipeParameters.js +300 -0
  61. package/dist/socialLayouts.d.ts +40 -232
  62. package/dist/socialLayouts.js +127 -387
  63. package/dist/styleManifest.d.ts +20 -0
  64. package/dist/styleManifest.js +103 -0
  65. package/dist/text.js +12 -2
  66. package/dist/textPresentation.d.ts +29 -0
  67. package/dist/textPresentation.js +64 -0
  68. package/package.json +1 -1
@@ -5,7 +5,18 @@ exports.parseSource = parseSource;
5
5
  exports.validateContentPlan = validateContentPlan;
6
6
  exports.contentBlockText = contentBlockText;
7
7
  exports.validateEditableContentPage = validateEditableContentPage;
8
+ const textPresentation_js_1 = require("./textPresentation.js");
8
9
  const compositionScenes_js_1 = require("./compositionScenes.js");
10
+ function emojiField(value, path) {
11
+ if (value === undefined || value === "")
12
+ return {};
13
+ if (typeof value !== "string" ||
14
+ Array.from(value).length > 16 ||
15
+ !/^[\p{Extended_Pictographic}\p{Emoji_Component}\u200d\uFE0F]+$/u.test(value) ||
16
+ !/\p{Extended_Pictographic}/u.test(value))
17
+ return fail("CONTENT_INVALID_EMOJI", path);
18
+ return { emoji: value };
19
+ }
9
20
  class ContentContractError extends Error {
10
21
  code;
11
22
  path;
@@ -16,63 +27,183 @@ class ContentContractError extends Error {
16
27
  }
17
28
  }
18
29
  exports.ContentContractError = ContentContractError;
19
- const fail = (code, path) => { throw new ContentContractError(code, path); };
20
- const sceneField = (value, path) => { if (value === undefined)
21
- return {}; if (typeof value !== 'string' || !compositionScenes_js_1.COMPOSITION_SCENES.some(scene => scene.id === value))
22
- return fail('CONTENT_INVALID_SCENE', path); return { scene: value }; };
30
+ const fail = (code, path) => {
31
+ throw new ContentContractError(code, path);
32
+ };
33
+ /** AI-selected exact title phrases; no positional or last-words heuristic. */
34
+ const titleEmphasis = (value, title, path) => {
35
+ if (value === undefined)
36
+ return {};
37
+ if (!Array.isArray(value) || value.length > 3 || typeof title !== "string")
38
+ return fail("CONTENT_INVALID_EMPHASIS", path);
39
+ const phrases = [];
40
+ for (const phrase of value) {
41
+ if (typeof phrase !== "string" ||
42
+ !phrase.trim() ||
43
+ phrase !== phrase.trim() ||
44
+ phrase.length > 80 ||
45
+ !title.includes(phrase) ||
46
+ phrases.some((p) => p.includes(phrase) || phrase.includes(p)))
47
+ return fail("CONTENT_INVALID_EMPHASIS", path);
48
+ phrases.push(phrase);
49
+ }
50
+ return { titleEmphasis: phrases };
51
+ };
52
+ const sceneField = (value, path) => {
53
+ if (value === undefined)
54
+ return {};
55
+ if (typeof value !== "string" ||
56
+ !compositionScenes_js_1.COMPOSITION_SCENES.some((scene) => scene.id === value))
57
+ return fail("CONTENT_INVALID_SCENE", path);
58
+ return { scene: value };
59
+ };
23
60
  const object = (raw, path) => {
24
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
25
- return fail('CONTENT_INVALID_OBJECT', path);
61
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
62
+ return fail("CONTENT_INVALID_OBJECT", path);
26
63
  return raw;
27
64
  };
28
65
  const text = (raw, path, max = 1000, empty = false) => {
29
- if (typeof raw !== 'string' || raw.length > max || (!empty && !raw.trim()))
30
- return fail('CONTENT_INVALID_TEXT', path);
66
+ if (typeof raw !== "string" || raw.length > max || (!empty && !raw.trim()))
67
+ return fail("CONTENT_INVALID_TEXT", path);
31
68
  return raw;
32
69
  };
33
70
  const list = (raw, path, min, max) => {
34
71
  if (!Array.isArray(raw) || raw.length < min || raw.length > max)
35
- return fail('CONTENT_INVALID_LIST', path);
72
+ return fail("CONTENT_INVALID_LIST", path);
36
73
  return raw;
37
74
  };
38
75
  const identity = (raw, path) => {
39
76
  const id = text(raw, path, 80);
40
77
  if (!/^[a-z][a-z0-9-]*$/.test(id))
41
- fail('CONTENT_INVALID_ID', path);
78
+ fail("CONTENT_INVALID_ID", path);
42
79
  return id;
43
80
  };
81
+ /** Publication metadata is optional and source-backed; never infer engagement figures. */
82
+ function publication(raw, path, sourceText) {
83
+ if (raw === undefined)
84
+ return {};
85
+ const v = object(raw, path);
86
+ const field = (value, key, max = 100) => {
87
+ const result = text(value, `${path}.${key}`, max);
88
+ if (sourceText !== undefined && !sourceText.includes(result))
89
+ fail("CONTENT_UNSUPPORTED_PUBLICATION", `${path}.${key}`);
90
+ return result;
91
+ };
92
+ const result = {};
93
+ if (v.tags !== undefined)
94
+ result.tags = list(v.tags, `${path}.tags`, 0, 3).map((tag, i) => field(tag, `tags.${i}`, 32));
95
+ if (v.date !== undefined)
96
+ result.date = field(v.date, "date", 40);
97
+ if (v.source !== undefined)
98
+ result.source = field(v.source, "source", 120);
99
+ if (v.metrics !== undefined)
100
+ result.metrics = list(v.metrics, `${path}.metrics`, 0, 3).map((raw, i) => {
101
+ const m = object(raw, `${path}.metrics.${i}`);
102
+ return {
103
+ label: field(m.label, `metrics.${i}.label`, 24),
104
+ value: field(m.value, `metrics.${i}.value`, 24),
105
+ };
106
+ });
107
+ if (v.author !== undefined) {
108
+ const a = object(v.author, `${path}.author`);
109
+ result.author = {
110
+ name: field(a.name, "author.name", 60),
111
+ ...(a.role === undefined
112
+ ? {}
113
+ : { role: field(a.role, "author.role", 80) }),
114
+ ...(a.avatarAssetId === undefined
115
+ ? {}
116
+ : {
117
+ avatarAssetId: identity(a.avatarAssetId, `${path}.author.avatarAssetId`),
118
+ }),
119
+ };
120
+ }
121
+ return { publication: result };
122
+ }
123
+ /** Identity copy is source-backed; asset IDs are editor-owned, never AI URLs. */
124
+ function bookend(raw, path, sourceText) {
125
+ if (raw === undefined)
126
+ return {};
127
+ const v = object(raw, path), result = {};
128
+ for (const key of [
129
+ "organization",
130
+ "presenter",
131
+ "role",
132
+ "date",
133
+ "contact",
134
+ ]) {
135
+ if (v[key] === undefined)
136
+ continue;
137
+ const value = text(v[key], `${path}.${key}`, key === "contact" ? 160 : 80);
138
+ if (sourceText !== undefined && !sourceText.includes(value))
139
+ fail("CONTENT_UNSUPPORTED_IDENTITY", `${path}.${key}`);
140
+ result[key] = value;
141
+ }
142
+ if (v.logoAssetId !== undefined) {
143
+ if (sourceText !== undefined)
144
+ fail("CONTENT_UNSUPPORTED_ASSET", `${path}.logoAssetId`);
145
+ result.logoAssetId = identity(v.logoAssetId, `${path}.logoAssetId`);
146
+ }
147
+ return { bookend: result };
148
+ }
149
+ /** Optional comparison facts stay aligned to their rows. Values/prices are source-verbatim. */
150
+ function comparisonColumn(raw, path, sourceText, allowIncomplete = false) {
151
+ const draft = (value, key, max, empty = false) => text(value, key, allowIncomplete ? 20000 : max, empty || allowIncomplete);
152
+ const col = object(raw, path), points = list(col.points, `${path}.points`, 1, 8).map((v, i) => draft(v, `${path}.points.${i}`, 500));
153
+ const fact = (v, p) => {
154
+ const result = draft(v, p, 80, true);
155
+ if (sourceText !== undefined && result && !sourceText.includes(result))
156
+ fail("CONTENT_UNSUPPORTED_COMPARISON", p);
157
+ return result;
158
+ };
159
+ const result = {
160
+ title: draft(col.title, `${path}.title`, 160),
161
+ points,
162
+ };
163
+ if (col.price !== undefined)
164
+ result.price = fact(col.price, `${path}.price`);
165
+ if (col.values !== undefined)
166
+ result.values = list(col.values, `${path}.values`, points.length, points.length).map((v, i) => fact(v, `${path}.values.${i}`));
167
+ if (col.statuses !== undefined)
168
+ result.statuses = list(col.statuses, `${path}.statuses`, points.length, points.length).map((v, i) => {
169
+ if (!["included", "excluded", "partial", "unknown"].includes(String(v)))
170
+ fail("CONTENT_INVALID_COMPARISON_STATUS", `${path}.statuses.${i}`);
171
+ return v;
172
+ });
173
+ return result;
174
+ }
44
175
  /** Lossless ranges. File-name placeholders are references, never fabricated assets. */
45
176
  function parseSource(input) {
46
- if (typeof input !== 'string' || !input.trim() || input.length > 20000)
47
- fail('SOURCE_INVALID_LENGTH', 'source');
177
+ if (typeof input !== "string" || !input.trim() || input.length > 20000)
178
+ fail("SOURCE_INVALID_LENGTH", "source");
48
179
  const segments = [];
49
- const lines = [...input.matchAll(/[^\n]*(?:\n|$)/g)].filter(match => match[0].length);
180
+ const lines = [...input.matchAll(/[^\n]*(?:\n|$)/g)].filter((match) => match[0].length);
50
181
  for (let i = 0; i < lines.length; i++) {
51
182
  const line = lines[i], raw = line[0], trimmed = raw.trim();
52
183
  if (!trimmed)
53
184
  continue;
54
- let start = line.index, end = start + raw.replace(/\r?\n$/, '').length, kind = 'text', language;
185
+ let start = line.index, end = start + raw.replace(/\r?\n$/, "").length, kind = "text", language;
55
186
  const fence = trimmed.match(/^(`{3,}|~{3,})([\w+-]*)\s*$/);
56
187
  if (fence) {
57
- kind = 'code';
58
- language = fence[2] || 'text';
188
+ kind = "code";
189
+ language = fence[2] || "text";
59
190
  start = line.index + raw.length;
60
191
  let j = i + 1;
61
192
  for (; j < lines.length; j++)
62
193
  if (lines[j][0].trim() === fence[1])
63
194
  break;
64
195
  end = j < lines.length ? lines[j].index : input.length;
65
- if (input.slice(start, end).endsWith('\n'))
196
+ if (input.slice(start, end).endsWith("\n"))
66
197
  end--;
67
- if (input.slice(start, end).endsWith('\r'))
198
+ if (input.slice(start, end).endsWith("\r"))
68
199
  end--;
69
200
  i = j < lines.length ? j : lines.length - 1;
70
201
  }
71
202
  else if (/^curl(?:\s|$)/i.test(trimmed)) {
72
- kind = 'code';
73
- language = 'bash';
203
+ kind = "code";
204
+ language = "bash";
74
205
  // Track multiline quoted arguments and escaped physical newlines without executing code.
75
- let quote = '', escaped = false, j = i;
206
+ let quote = "", escaped = false, j = i;
76
207
  for (; j < lines.length; j++) {
77
208
  const current = lines[j][0];
78
209
  for (const char of current) {
@@ -80,18 +211,18 @@ function parseSource(input) {
80
211
  escaped = false;
81
212
  continue;
82
213
  }
83
- if (char === '\\' && quote !== "'") {
214
+ if (char === "\\" && quote !== "'") {
84
215
  escaped = true;
85
216
  continue;
86
217
  }
87
218
  if (quote) {
88
219
  if (char === quote)
89
- quote = '';
220
+ quote = "";
90
221
  }
91
222
  else if (char === '"' || char === "'")
92
223
  quote = char;
93
224
  }
94
- end = lines[j].index + current.replace(/\r?\n$/, '').length;
225
+ end = lines[j].index + current.replace(/\r?\n$/, "").length;
95
226
  if (!quote && !/\\\s*(?:\r?\n)?$/.test(current))
96
227
  break;
97
228
  escaped = false;
@@ -99,144 +230,467 @@ function parseSource(input) {
99
230
  i = Math.min(j, lines.length - 1);
100
231
  }
101
232
  else if (/^(?:!\[[^\]]*\]\([^\n]+\)|\[[^\]]*\]\(https?:\/\/[^\s)]+\.(?:png|jpe?g|webp|gif|svg)(?:[?#][^\s)]*)?\)|[^\s<>]+\.(?:png|jpe?g|webp|gif|svg))$/i.test(trimmed))
102
- kind = 'image-reference';
233
+ kind = "image-reference";
103
234
  if (end > start)
104
- segments.push({ id: `source-${segments.length + 1}`, kind, text: input.slice(start, end), start, end, ...(language ? { language } : {}) });
235
+ segments.push({
236
+ id: `source-${segments.length + 1}`,
237
+ kind,
238
+ text: input.slice(start, end),
239
+ start,
240
+ end,
241
+ ...(language ? { language } : {}),
242
+ });
105
243
  }
106
244
  if (segments.length > 800)
107
- fail('SOURCE_TOO_MANY_SEGMENTS', 'source');
245
+ fail("SOURCE_TOO_MANY_SEGMENTS", "source");
108
246
  return { version: 1, text: input, segments };
109
247
  }
110
248
  /** Validate untrusted AI output; strip unknown properties and never trust generated code or asset URLs. */
111
249
  function validateContentPlan(raw, source, expectedKind) {
112
- const root = object(raw, 'plan');
113
- if (root.version !== 1 || !['presentation', 'social'].includes(String(root.kind)) || (expectedKind && root.kind !== expectedKind))
114
- fail('CONTENT_INVALID_VERSION', 'plan');
115
- const sources = new Map(source.segments.map(segment => [segment.id, segment]));
250
+ const root = object(raw, "plan");
251
+ if (root.version !== 1 ||
252
+ !["presentation", "social"].includes(String(root.kind)) ||
253
+ (expectedKind && root.kind !== expectedKind))
254
+ fail("CONTENT_INVALID_VERSION", "plan");
255
+ const sources = new Map(source.segments.map((segment) => [segment.id, segment]));
116
256
  const evidence = (raw, path) => list(raw, path, 1, 32).map((entry, index) => {
117
257
  const item = object(entry, `${path}.${index}`), sourceId = text(item.sourceId, `${path}.${index}.sourceId`, 80);
118
258
  const segment = sources.get(sourceId);
119
259
  if (!segment)
120
- fail('CONTENT_UNKNOWN_SOURCE', path);
121
- const quote = item.quote === undefined ? undefined : text(item.quote, `${path}.${index}.quote`, 20000);
260
+ fail("CONTENT_UNKNOWN_SOURCE", path);
261
+ const quote = item.quote === undefined
262
+ ? undefined
263
+ : text(item.quote, `${path}.${index}.quote`, 20000);
122
264
  if (quote && !segment.text.includes(quote))
123
- fail('CONTENT_UNSUPPORTED_QUOTE', path);
265
+ fail("CONTENT_UNSUPPORTED_QUOTE", path);
124
266
  return { sourceId, ...(quote ? { quote } : {}) };
125
267
  });
126
268
  const ids = new Set();
127
- const unique = (raw, path) => { const id = identity(raw, path); if (ids.has(id))
128
- fail('CONTENT_DUPLICATE_ID', path); ids.add(id); return id; };
269
+ const unique = (raw, path) => {
270
+ const id = identity(raw, path);
271
+ if (ids.has(id))
272
+ fail("CONTENT_DUPLICATE_ID", path);
273
+ ids.add(id);
274
+ return id;
275
+ };
129
276
  const block = (raw, path) => {
130
- const value = object(raw, path), base = { id: unique(value.id, `${path}.id`), evidence: evidence(value.evidence, `${path}.evidence`) };
277
+ const value = object(raw, path), base = {
278
+ id: unique(value.id, `${path}.id`),
279
+ evidence: evidence(value.evidence, `${path}.evidence`),
280
+ };
131
281
  const field = (key, max = 1000, empty = false) => text(value[key], `${path}.${key}`, max, empty);
132
282
  switch (value.kind) {
133
- case 'paragraph': return { ...base, kind: 'paragraph', text: field('text', 1600), ...(value.heading === undefined ? {} : { heading: field('heading', 160), level: value.level === 3 ? 3 : 2 }) };
134
- case 'steps':
135
- case 'features': return { ...base, kind: value.kind, entries: list(value.entries, `${path}.entries`, 1, 12).map((raw, i) => { const item = object(raw, `${path}.entries.${i}`); return { title: text(item.title, path, 160), description: text(item.description, path, 600, true) }; }) };
136
- case 'comparison': return { ...base, kind: 'comparison', columns: list(value.columns, `${path}.columns`, 2, 4).map(raw => { const col = object(raw, path); return { title: text(col.title, path, 160), points: list(col.points, path, 1, 8).map(raw => text(raw, path, 500)) }; }) };
137
- case 'code': {
138
- const sourceId = field('sourceId', 80), segment = sources.get(sourceId);
139
- if (segment?.kind !== 'code' || !base.evidence.some(e => e.sourceId === sourceId))
140
- return fail('CONTENT_INVALID_CODE_SOURCE', path);
283
+ case "paragraph":
284
+ return {
285
+ ...base,
286
+ kind: "paragraph",
287
+ text: field("text", 1600),
288
+ ...(value.heading === undefined
289
+ ? {}
290
+ : {
291
+ heading: field("heading", 160),
292
+ level: value.level === 3 ? 3 : 2,
293
+ }),
294
+ };
295
+ case "steps":
296
+ case "features":
297
+ return {
298
+ ...base,
299
+ kind: value.kind,
300
+ entries: list(value.entries, `${path}.entries`, 1, 12).map((raw, i) => {
301
+ const item = object(raw, `${path}.entries.${i}`);
302
+ return {
303
+ title: text(item.title, path, 160),
304
+ description: text(item.description, path, 600, true),
305
+ ...visualDescriptor(item),
306
+ };
307
+ }),
308
+ };
309
+ case "comparison":
310
+ return {
311
+ ...base,
312
+ kind: "comparison",
313
+ columns: list(value.columns, `${path}.columns`, 2, 4).map((raw, i) => comparisonColumn(raw, `${path}.columns.${i}`, base.evidence
314
+ .map((e) => sources.get(e.sourceId).text)
315
+ .join("\n"))),
316
+ };
317
+ case "code": {
318
+ const sourceId = field("sourceId", 80), segment = sources.get(sourceId);
319
+ if (segment?.kind !== "code" ||
320
+ !base.evidence.some((e) => e.sourceId === sourceId))
321
+ return fail("CONTENT_INVALID_CODE_SOURCE", path);
141
322
  if (value.code !== undefined && value.code !== segment.text)
142
- fail('CONTENT_CODE_CHANGED', path);
143
- return { ...base, kind: 'code', sourceId, language: segment.language || 'text', code: segment.text };
323
+ fail("CONTENT_CODE_CHANGED", path);
324
+ return {
325
+ ...base,
326
+ kind: "code",
327
+ sourceId,
328
+ language: segment.language || "text",
329
+ code: segment.text,
330
+ };
144
331
  }
145
- case 'parameters': return { ...base, kind: 'parameters', rows: list(value.rows, `${path}.rows`, 1, 20).map(raw => { const row = object(raw, path); return { name: text(row.name, path, 160), value: text(row.value, path, 1000, true), description: text(row.description, path, 500, true) }; }) };
146
- case 'quote': {
147
- const copy = field('text', 1600);
148
- if (!base.evidence.some(e => sources.get(e.sourceId).text.includes(copy)))
149
- fail('CONTENT_UNSUPPORTED_QUOTE', path);
150
- const attribution = field('attribution', 160, true);
151
- if (attribution && !base.evidence.some(e => sources.get(e.sourceId).text.includes(attribution)))
152
- fail('CONTENT_UNSUPPORTED_ATTRIBUTION', path);
153
- return { ...base, kind: 'quote', text: copy, attribution };
332
+ case "parameters":
333
+ return {
334
+ ...base,
335
+ kind: "parameters",
336
+ rows: list(value.rows, `${path}.rows`, 1, 20).map((raw) => {
337
+ const row = object(raw, path);
338
+ return {
339
+ name: text(row.name, path, 160),
340
+ value: text(row.value, path, 1000, true),
341
+ description: text(row.description, path, 500, true),
342
+ };
343
+ }),
344
+ };
345
+ case "quote": {
346
+ const copy = field("text", 1600);
347
+ if (!base.evidence.some((e) => sources.get(e.sourceId).text.includes(copy)))
348
+ fail("CONTENT_UNSUPPORTED_QUOTE", path);
349
+ const attribution = field("attribution", 160, true);
350
+ if (attribution &&
351
+ !base.evidence.some((e) => sources.get(e.sourceId).text.includes(attribution)))
352
+ fail("CONTENT_UNSUPPORTED_ATTRIBUTION", path);
353
+ const role = value.role === undefined ? undefined : field("role", 160, true);
354
+ if (role &&
355
+ !base.evidence.some((e) => sources.get(e.sourceId).text.includes(role)))
356
+ fail("CONTENT_UNSUPPORTED_ATTRIBUTION", path);
357
+ return {
358
+ ...base,
359
+ kind: "quote",
360
+ text: copy,
361
+ attribution,
362
+ ...(role === undefined ? {} : { role }),
363
+ ...(value.avatarAssetId === undefined
364
+ ? {}
365
+ : {
366
+ avatarAssetId: identity(value.avatarAssetId, `${path}.avatarAssetId`),
367
+ }),
368
+ };
154
369
  }
155
- case 'chart': {
156
- if (!['bar', 'line'].includes(String(value.chart)))
157
- fail('CONTENT_INVALID_CHART', path);
158
- const proof = base.evidence.map(e => sources.get(e.sourceId).text).join('\n');
159
- const unit = field('unit', 60, true);
370
+ case "chart": {
371
+ if (!["bar", "line"].includes(String(value.chart)))
372
+ fail("CONTENT_INVALID_CHART", path);
373
+ const proof = base.evidence
374
+ .map((e) => sources.get(e.sourceId).text)
375
+ .join("\n");
376
+ const unit = field("unit", 60, true);
160
377
  if (unit && !proof.includes(unit))
161
- fail('CONTENT_UNSUPPORTED_UNIT', path);
378
+ fail("CONTENT_UNSUPPORTED_UNIT", path);
162
379
  const numbers = new Set(proof.match(/-?\d+(?:\.\d+)?/g)?.map(Number) || []);
163
- return { ...base, kind: 'chart', chart: value.chart, unit, values: list(value.values, path, 2, 12).map(raw => { const entry = object(raw, path); if (typeof entry.value !== 'number' || !Number.isFinite(entry.value) || !numbers.has(entry.value))
164
- fail('CONTENT_UNSUPPORTED_VALUE', path); const label = text(entry.label, path, 100); if (!proof.includes(label))
165
- fail('CONTENT_UNSUPPORTED_LABEL', path); return { label, value: entry.value }; }) };
380
+ return {
381
+ ...base,
382
+ kind: "chart",
383
+ chart: value.chart,
384
+ unit,
385
+ values: list(value.values, path, 2, 12).map((raw) => {
386
+ const entry = object(raw, path);
387
+ if (typeof entry.value !== "number" ||
388
+ !Number.isFinite(entry.value) ||
389
+ !numbers.has(entry.value))
390
+ fail("CONTENT_UNSUPPORTED_VALUE", path);
391
+ const label = text(entry.label, path, 100);
392
+ if (!proof.includes(label))
393
+ fail("CONTENT_UNSUPPORTED_LABEL", path);
394
+ return { label, value: entry.value };
395
+ }),
396
+ };
166
397
  }
167
- case 'image': {
168
- const sourceId = field('sourceId', 80), segment = sources.get(sourceId);
169
- if (segment?.kind !== 'image-reference' || !base.evidence.some(e => e.sourceId === sourceId))
170
- return fail('CONTENT_INVALID_IMAGE_SOURCE', path);
171
- if (!['screenshot', 'photograph', 'diagram'].includes(String(value.treatment)))
172
- fail('CONTENT_INVALID_IMAGE_TREATMENT', path);
398
+ case "image": {
399
+ const sourceId = field("sourceId", 80), segment = sources.get(sourceId);
400
+ if (segment?.kind !== "image-reference" ||
401
+ !base.evidence.some((e) => e.sourceId === sourceId))
402
+ return fail("CONTENT_INVALID_IMAGE_SOURCE", path);
403
+ if (!["screenshot", "photograph", "diagram"].includes(String(value.treatment)))
404
+ fail("CONTENT_INVALID_IMAGE_TREATMENT", path);
173
405
  // Availability is resolved from the authenticated asset store later, never asserted by the model.
174
- return { ...base, kind: 'image', sourceId, alt: field('alt', 300, true), treatment: value.treatment, status: 'missing' };
406
+ return {
407
+ ...base,
408
+ kind: "image",
409
+ sourceId,
410
+ alt: field("alt", 300, true),
411
+ treatment: value.treatment,
412
+ status: "missing",
413
+ };
175
414
  }
176
- default: return fail('CONTENT_UNKNOWN_BLOCK', path);
415
+ default:
416
+ return fail("CONTENT_UNKNOWN_BLOCK", path);
177
417
  }
178
418
  };
179
- const pages = list(root.pages, 'pages', 1, root.kind === 'social' ? 8 : 24).map((raw, index) => {
419
+ const pages = list(root.pages, "pages", 1, root.kind === "social" ? 8 : 24).map((raw, index) => {
180
420
  const path = `pages.${index}`, page = object(raw, path);
181
- if (!['cover', 'explain', 'instruct', 'compare', 'evidence', 'conclude'].includes(String(page.purpose)))
182
- fail('CONTENT_INVALID_PURPOSE', path);
183
- return { ...sceneField(page.scene, `${path}.scene`), id: unique(page.id, `${path}.id`), title: text(page.title, `${path}.title`, 160), purpose: page.purpose, evidence: evidence(page.evidence, `${path}.evidence`), blocks: list(page.blocks, `${path}.blocks`, 1, 8).map((value, i) => block(value, `${path}.blocks.${i}`)), ...(page.caption !== undefined ? { caption: text(page.caption, `${path}.caption`, 2000, true) } : {}), ...(page.notes !== undefined ? { notes: text(page.notes, `${path}.notes`, 2000, true) } : {}) };
421
+ if (![
422
+ "cover",
423
+ "explain",
424
+ "instruct",
425
+ "compare",
426
+ "evidence",
427
+ "conclude",
428
+ ].includes(String(page.purpose)))
429
+ fail("CONTENT_INVALID_PURPOSE", path);
430
+ return {
431
+ ...(page.textPresentation ? { textPresentation: (0, textPresentation_js_1.validateTextPresentations)(page.textPresentation, page) } : {}),
432
+ ...titleEmphasis(page.titleEmphasis, page.title, `${path}.titleEmphasis`),
433
+ ...emojiField(page.emoji, `${path}.emoji`),
434
+ ...publication(page.publication, `${path}.publication`, source.text),
435
+ ...bookend(page.bookend, `${path}.bookend`, source.text),
436
+ ...sceneField(page.scene, `${path}.scene`),
437
+ id: unique(page.id, `${path}.id`),
438
+ title: text(page.title, `${path}.title`, 160),
439
+ purpose: page.purpose,
440
+ evidence: evidence(page.evidence, `${path}.evidence`),
441
+ blocks: list(page.blocks, `${path}.blocks`, 1, 8).map((value, i) => block(value, `${path}.blocks.${i}`)),
442
+ ...(page.caption !== undefined
443
+ ? { caption: text(page.caption, `${path}.caption`, 2000, true) }
444
+ : {}),
445
+ ...(page.notes !== undefined
446
+ ? { notes: text(page.notes, `${path}.notes`, 2000, true) }
447
+ : {}),
448
+ };
184
449
  });
185
- if (root.kind === 'presentation')
186
- for (const segment of source.segments.filter(s => s.kind === 'code'))
187
- if (!pages.some(p => p.blocks.some(b => b.kind === 'code' && b.sourceId === segment.id)))
188
- fail('CONTENT_CODE_OMITTED', segment.id);
189
- if (root.kind === 'social' && pages.some(page => !page.caption?.trim()))
190
- fail('CONTENT_CAPTION_REQUIRED', 'pages');
191
- return { version: 1, kind: root.kind, title: text(root.title, 'title', 160), pages };
450
+ if (root.kind === "presentation")
451
+ for (const segment of source.segments.filter((s) => s.kind === "code"))
452
+ if (!pages.some((p) => p.blocks.some((b) => b.kind === "code" && b.sourceId === segment.id)))
453
+ fail("CONTENT_CODE_OMITTED", segment.id);
454
+ if (root.kind === "social" && pages.some((page) => !page.caption?.trim()))
455
+ fail("CONTENT_CAPTION_REQUIRED", "pages");
456
+ return {
457
+ version: 1,
458
+ kind: root.kind,
459
+ title: text(root.title, "title", 160),
460
+ pages,
461
+ };
192
462
  }
193
463
  function contentBlockText(block) {
194
464
  switch (block.kind) {
195
- case 'paragraph': return [...(block.heading ? [block.heading] : []), block.text];
196
- case 'steps':
197
- case 'features': return block.entries.flatMap(e => [e.title, e.description]);
198
- case 'comparison': return block.columns.flatMap(c => [c.title, ...c.points]);
199
- case 'code': return [block.code];
200
- case 'parameters': return block.rows.flatMap(r => [r.name, r.value, r.description]);
201
- case 'quote': return [block.text, block.attribution];
202
- case 'chart': return [block.unit, ...block.values.flatMap(v => [v.label, String(v.value)])];
203
- case 'image': return [];
465
+ case "paragraph":
466
+ return [...(block.heading ? [block.heading] : []), block.text];
467
+ case "steps":
468
+ case "features":
469
+ return block.entries.flatMap((e) => [e.title, e.description]);
470
+ case "comparison":
471
+ return block.columns.flatMap((c) => [
472
+ c.title,
473
+ ...c.points,
474
+ ...(c.price ? [c.price] : []),
475
+ ...(c.values || []),
476
+ ]);
477
+ case "code":
478
+ return [block.code];
479
+ case "parameters":
480
+ return block.rows.flatMap((r) => [r.name, r.value, r.description]);
481
+ case "quote":
482
+ return [block.text, block.attribution];
483
+ case "chart":
484
+ return [
485
+ block.unit,
486
+ ...block.values.flatMap((v) => [v.label, String(v.value)]),
487
+ ];
488
+ case "image":
489
+ return [];
204
490
  }
205
491
  }
206
492
  /** Validate persisted/editor-authored content without reasserting that user edits came from AI evidence. */
207
493
  function validateEditableContentPage(raw, { allowIncomplete = false } = {}) {
208
494
  const draftText = (value, path, max = 1000, empty = false) => text(value, path, allowIncomplete ? Math.max(max, 20000) : max, empty || allowIncomplete);
209
- const page = object(raw, 'page'), ids = new Set();
210
- const id = (value, path) => { const result = identity(value, path); if (ids.has(result))
211
- fail('CONTENT_DUPLICATE_ID', path); ids.add(result); return result; };
212
- const proof = (value, path) => list(value, path, 1, 32).map((raw, index) => { const item = object(raw, `${path}.${index}`); return { sourceId: text(item.sourceId, path, 80), ...(item.quote === undefined ? {} : { quote: text(item.quote, path, 20000) }) }; });
213
- if (!['cover', 'explain', 'instruct', 'compare', 'evidence', 'conclude'].includes(String(page.purpose)))
214
- fail('CONTENT_INVALID_PURPOSE', 'page.purpose');
215
- const pageId = id(page.id, 'page.id');
216
- const blocks = list(page.blocks, 'page.blocks', 1, 8).map((raw, index) => {
217
- const path = `page.blocks.${index}`, value = object(raw, path), base = { id: id(value.id, `${path}.id`), evidence: proof(value.evidence, `${path}.evidence`) };
495
+ const page = object(raw, "page"), ids = new Set();
496
+ const id = (value, path) => {
497
+ const result = identity(value, path);
498
+ if (ids.has(result))
499
+ fail("CONTENT_DUPLICATE_ID", path);
500
+ ids.add(result);
501
+ return result;
502
+ };
503
+ const proof = (value, path) => list(value, path, 1, 32).map((raw, index) => {
504
+ const item = object(raw, `${path}.${index}`);
505
+ return {
506
+ sourceId: text(item.sourceId, path, 80),
507
+ ...(item.quote === undefined
508
+ ? {}
509
+ : { quote: text(item.quote, path, 20000) }),
510
+ };
511
+ });
512
+ if (![
513
+ "cover",
514
+ "explain",
515
+ "instruct",
516
+ "compare",
517
+ "evidence",
518
+ "conclude",
519
+ ].includes(String(page.purpose)))
520
+ fail("CONTENT_INVALID_PURPOSE", "page.purpose");
521
+ const pageId = id(page.id, "page.id");
522
+ const blocks = list(page.blocks, "page.blocks", 1, 8).map((raw, index) => {
523
+ const path = `page.blocks.${index}`, value = object(raw, path), base = {
524
+ id: id(value.id, `${path}.id`),
525
+ evidence: proof(value.evidence, `${path}.evidence`),
526
+ };
218
527
  const field = (name, max = 1000, empty = false) => draftText(value[name], `${path}.${name}`, max, empty);
219
528
  switch (value.kind) {
220
- case 'paragraph': return { ...base, kind: 'paragraph', text: field('text', 1600), ...(value.heading === undefined ? {} : { heading: field('heading', 160), level: value.level === 3 ? 3 : 2 }) };
221
- case 'steps':
222
- case 'features': return { ...base, kind: value.kind, entries: list(value.entries, path, 1, 12).map(raw => { const item = object(raw, path); return { title: draftText(item.title, path, 160), description: draftText(item.description, path, 600, true) }; }) };
223
- case 'comparison': return { ...base, kind: 'comparison', columns: list(value.columns, path, 2, 4).map(raw => { const item = object(raw, path); return { title: draftText(item.title, path, 160), points: list(item.points, path, 1, 8).map(raw => draftText(raw, path, 500)) }; }) };
224
- case 'code': return { ...base, kind: 'code', sourceId: field('sourceId', 80), language: field('language', 40), code: field('code', 20000) };
225
- case 'parameters': return { ...base, kind: 'parameters', rows: list(value.rows, path, 1, 20).map(raw => { const item = object(raw, path); return { name: draftText(item.name, path, 160), value: draftText(item.value, path, 1000, true), description: draftText(item.description, path, 500, true) }; }) };
226
- case 'quote': return { ...base, kind: 'quote', text: field('text', 1600), attribution: field('attribution', 160, true) };
227
- case 'chart': {
228
- if (!['bar', 'line'].includes(String(value.chart)))
229
- fail('CONTENT_INVALID_CHART', path);
230
- return { ...base, kind: 'chart', chart: value.chart, unit: field('unit', 60, true), values: list(value.values, path, 2, 12).map(raw => { const item = object(raw, path); if (typeof item.value !== 'number' || (!(allowIncomplete && Number.isNaN(item.value)) && (!Number.isFinite(item.value) || Math.abs(item.value) > 1e15)))
231
- fail('CONTENT_INVALID_VALUE', path); return { label: draftText(item.label, path, 100), value: item.value }; }) };
529
+ case "paragraph":
530
+ return {
531
+ ...base,
532
+ kind: "paragraph",
533
+ text: field("text", 1600),
534
+ ...(value.heading === undefined
535
+ ? {}
536
+ : {
537
+ heading: field("heading", 160),
538
+ level: value.level === 3 ? 3 : 2,
539
+ }),
540
+ };
541
+ case "steps":
542
+ case "features":
543
+ return {
544
+ ...base,
545
+ kind: value.kind,
546
+ entries: list(value.entries, path, 1, 12).map((raw) => {
547
+ const item = object(raw, path);
548
+ return {
549
+ title: draftText(item.title, path, 160),
550
+ description: draftText(item.description, path, 600, true),
551
+ ...visualDescriptor(item),
552
+ };
553
+ }),
554
+ };
555
+ case "comparison":
556
+ return {
557
+ ...base,
558
+ kind: "comparison",
559
+ columns: list(value.columns, path, 2, 4).map((raw, i) => comparisonColumn(raw, `${path}.columns.${i}`, undefined, allowIncomplete)),
560
+ };
561
+ case "code":
562
+ return {
563
+ ...base,
564
+ kind: "code",
565
+ sourceId: field("sourceId", 80),
566
+ language: field("language", 40),
567
+ code: field("code", 20000),
568
+ };
569
+ case "parameters":
570
+ return {
571
+ ...base,
572
+ kind: "parameters",
573
+ rows: list(value.rows, path, 1, 20).map((raw) => {
574
+ const item = object(raw, path);
575
+ return {
576
+ name: draftText(item.name, path, 160),
577
+ value: draftText(item.value, path, 1000, true),
578
+ description: draftText(item.description, path, 500, true),
579
+ };
580
+ }),
581
+ };
582
+ case "quote":
583
+ return {
584
+ ...base,
585
+ kind: "quote",
586
+ text: field("text", 1600),
587
+ attribution: field("attribution", 160, true),
588
+ ...(value.role === undefined
589
+ ? {}
590
+ : { role: field("role", 160, true) }),
591
+ ...(value.avatarAssetId === undefined
592
+ ? {}
593
+ : {
594
+ avatarAssetId: identity(value.avatarAssetId, `${path}.avatarAssetId`),
595
+ }),
596
+ };
597
+ case "chart": {
598
+ if (!["bar", "line"].includes(String(value.chart)))
599
+ fail("CONTENT_INVALID_CHART", path);
600
+ return {
601
+ ...base,
602
+ kind: "chart",
603
+ chart: value.chart,
604
+ unit: field("unit", 60, true),
605
+ values: list(value.values, path, 2, 12).map((raw) => {
606
+ const item = object(raw, path);
607
+ if (typeof item.value !== "number" ||
608
+ (!(allowIncomplete && Number.isNaN(item.value)) &&
609
+ (!Number.isFinite(item.value) || Math.abs(item.value) > 1e15)))
610
+ fail("CONTENT_INVALID_VALUE", path);
611
+ return {
612
+ label: draftText(item.label, path, 100),
613
+ value: item.value,
614
+ };
615
+ }),
616
+ };
232
617
  }
233
- case 'image': {
234
- if (!['screenshot', 'photograph', 'diagram'].includes(String(value.treatment)) || !['missing', 'available'].includes(String(value.status)))
235
- fail('CONTENT_INVALID_IMAGE', 'page');
236
- return { ...base, kind: 'image', sourceId: field('sourceId', 80), alt: field('alt', 300, true), treatment: value.treatment, status: value.status, ...(value.status === 'available' ? { assetId: field('assetId', 200) } : {}) };
618
+ case "image": {
619
+ if (!["screenshot", "photograph", "diagram"].includes(String(value.treatment)) ||
620
+ !["missing", "available"].includes(String(value.status)))
621
+ fail("CONTENT_INVALID_IMAGE", "page");
622
+ return {
623
+ ...base,
624
+ kind: "image",
625
+ sourceId: field("sourceId", 80),
626
+ alt: field("alt", 300, true),
627
+ treatment: value.treatment,
628
+ status: value.status,
629
+ ...(value.status === "available"
630
+ ? { assetId: field("assetId", 200) }
631
+ : {}),
632
+ };
237
633
  }
238
- default: return fail('CONTENT_UNKNOWN_BLOCK', path);
634
+ default:
635
+ return fail("CONTENT_UNKNOWN_BLOCK", path);
239
636
  }
240
637
  });
241
- return { ...sceneField(page.scene, 'page.scene'), id: pageId, title: draftText(page.title, 'page.title', 160), purpose: page.purpose, evidence: proof(page.evidence, 'page.evidence'), blocks, ...(page.caption === undefined ? {} : { caption: draftText(page.caption, 'page.caption', 2000, true) }), ...(page.notes === undefined ? {} : { notes: draftText(page.notes, 'page.notes', 2000, true) }) };
638
+ return {
639
+ ...(page.textPresentation ? { textPresentation: (0, textPresentation_js_1.validateTextPresentations)(page.textPresentation, page) } : {}),
640
+ ...titleEmphasis(page.titleEmphasis, page.title, "page.titleEmphasis"),
641
+ ...emojiField(page.emoji, "page.emoji"),
642
+ ...publication(page.publication, "page.publication"),
643
+ ...bookend(page.bookend, "page.bookend"),
644
+ ...sceneField(page.scene, "page.scene"),
645
+ id: pageId,
646
+ title: draftText(page.title, "page.title", 160),
647
+ purpose: page.purpose,
648
+ evidence: proof(page.evidence, "page.evidence"),
649
+ blocks,
650
+ ...(page.caption === undefined
651
+ ? {}
652
+ : { caption: draftText(page.caption, "page.caption", 2000, true) }),
653
+ ...(page.notes === undefined
654
+ ? {}
655
+ : { notes: draftText(page.notes, "page.notes", 2000, true) }),
656
+ };
657
+ }
658
+ function visualDescriptor(item) {
659
+ const icons = new Set([
660
+ "BsJournalText",
661
+ "BsPersonCheck",
662
+ "BsShare",
663
+ "BsPeople",
664
+ "BsSearch",
665
+ "BsShieldCheck",
666
+ "BsBarChart",
667
+ "BsCodeSlash",
668
+ "BsSignpostSplit",
669
+ "BsCheck2Circle",
670
+ "BsFolder2Open",
671
+ "BsGrid1X2",
672
+ ]);
673
+ const illustrations = new Set([
674
+ "document",
675
+ "collaboration",
676
+ "review",
677
+ "feedback",
678
+ "symbol",
679
+ "sequence",
680
+ "comparison",
681
+ "structure",
682
+ ]);
683
+ if (item.icon != null &&
684
+ (typeof item.icon !== "string" || !icons.has(item.icon)))
685
+ throw new Error("Invalid semantic icon");
686
+ if (item.illustration != null &&
687
+ (typeof item.illustration !== "string" ||
688
+ !illustrations.has(item.illustration)))
689
+ throw new Error("Invalid semantic illustration");
690
+ return {
691
+ ...(typeof item.icon === "string" ? { icon: item.icon } : {}),
692
+ ...(typeof item.illustration === "string"
693
+ ? { illustration: item.illustration }
694
+ : {}),
695
+ };
242
696
  }