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