@neatico/visual-core 0.14.26 → 0.14.65

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 (46) 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/compositionNumbers.js +111 -0
  5. package/dist/cjs/compositionUseCases.js +145 -0
  6. package/dist/cjs/content.js +46 -10
  7. package/dist/cjs/contentBrief.js +82 -0
  8. package/dist/cjs/designSystem.js +86 -9
  9. package/dist/cjs/exploreCollection.js +180 -0
  10. package/dist/cjs/fontCatalog.js +7 -1
  11. package/dist/cjs/formalCompositions.js +121 -0
  12. package/dist/cjs/index.js +14 -0
  13. package/dist/cjs/nativeSocialParameters.js +145 -0
  14. package/dist/cjs/pagination.js +14 -5
  15. package/dist/cjs/parameterDefaults.js +20 -0
  16. package/dist/cjs/recipeParameters.js +375 -0
  17. package/dist/cjs/styleManifest.js +107 -0
  18. package/dist/cjs/textPresentation.js +68 -0
  19. package/dist/compositionNumbers.d.ts +4 -0
  20. package/dist/compositionNumbers.js +108 -0
  21. package/dist/compositionUseCases.d.ts +1 -0
  22. package/dist/compositionUseCases.js +142 -0
  23. package/dist/content.d.ts +10 -0
  24. package/dist/content.js +46 -10
  25. package/dist/contentBrief.d.ts +16 -0
  26. package/dist/contentBrief.js +78 -0
  27. package/dist/designSystem.js +86 -9
  28. package/dist/exploreCollection.d.ts +7 -0
  29. package/dist/exploreCollection.js +176 -0
  30. package/dist/fontCatalog.js +7 -1
  31. package/dist/formalCompositions.d.ts +26 -0
  32. package/dist/formalCompositions.js +117 -0
  33. package/dist/index.d.ts +11 -0
  34. package/dist/index.js +11 -0
  35. package/dist/nativeSocialParameters.d.ts +38 -0
  36. package/dist/nativeSocialParameters.js +139 -0
  37. package/dist/pagination.js +14 -5
  38. package/dist/parameterDefaults.d.ts +2 -0
  39. package/dist/parameterDefaults.js +17 -0
  40. package/dist/recipeParameters.d.ts +34 -0
  41. package/dist/recipeParameters.js +371 -0
  42. package/dist/styleManifest.d.ts +20 -0
  43. package/dist/styleManifest.js +103 -0
  44. package/dist/textPresentation.d.ts +29 -0
  45. package/dist/textPresentation.js +64 -0
  46. package/package.json +1 -1
@@ -0,0 +1,38 @@
1
+ import { type TextPresentation } from './textPresentation.js';
2
+ import type { ParameterSchema } from './recipeParameters.js';
3
+ /** Native slots keep renderer-specific content separate from semantic page projection. */
4
+ export interface NativeSocialParameters {
5
+ textPresentation?: TextPresentation[];
6
+ decorationEmoji?: string;
7
+ title: string;
8
+ body: string;
9
+ eyebrow?: string;
10
+ emphasis?: string[];
11
+ titleLines?: string[];
12
+ cta?: string;
13
+ value?: string;
14
+ attribution?: string;
15
+ imageAssetId?: string;
16
+ comparisonLabels?: [string, string];
17
+ items?: {
18
+ title: string;
19
+ description: string;
20
+ assetId?: string;
21
+ illustration?: 'document' | 'collaboration' | 'review' | 'feedback' | 'sequence' | 'comparison' | 'structure';
22
+ primaryValue?: number;
23
+ secondaryValue?: number;
24
+ }[];
25
+ }
26
+ export interface NativeSocialContract {
27
+ parameterModel: 'native-social';
28
+ parameterSchema: ParameterSchema;
29
+ importance: Readonly<Record<string, number>>;
30
+ generationPolicy: Readonly<Record<string, readonly string[]>>;
31
+ }
32
+ export declare const COMPOSITION_ILLUSTRATIONS: readonly ["document", "collaboration", "review", "feedback", "sequence", "comparison", "structure"];
33
+ /** Every field corresponds to an authored visible slot; no scene-based eligibility filter. */
34
+ export declare function nativeSocialParameterContract(id: string): NativeSocialContract;
35
+ /** Apply the small closed schema vocabulary used by native slots on every local render. */
36
+ export declare function validateNativeSocialParameters(id: string, raw: unknown): NativeSocialParameters;
37
+ /** Numeric and image identities remain available when a native design becomes editable content. */
38
+ export declare function nativeSocialContentPage(values: NativeSocialParameters, original: import('./content.js').ContentPage): import('./content.js').ContentPage;
@@ -0,0 +1,139 @@
1
+ import { compositionParameterDefaults } from './parameterDefaults.js';
2
+ import { textPresentationSchema, validateTextPresentations } from './textPresentation.js';
3
+ import { SOCIAL_LAYOUT_CATALOG } from './socialLayouts.js';
4
+ const slots = {
5
+ 'note-question': {}, 'note-marker': {}, 'note-editorial': {}, 'note-checklist': { items: 3 }, 'note-photo': { image: true }, 'note-news': { value: true },
6
+ 'chat-messenger': { items: 3 }, 'chat-imessage': { items: 3 }, 'chat-telegram': { items: 3 }, 'chat-workplace': { items: 3 },
7
+ 'time-notice': { value: true }, 'news-focus': {}, manifesto: {}, launch: { image: true }, bento: { items: 3 }, spotlight: { items: 3, value: true, image: true },
8
+ ticket: { items: 2, value: true }, webinar: { value: true, image: true }, quote: { attribution: true }, testimonial: { attribution: true }, metric: { value: true },
9
+ report: { items: 3 }, checklist: { items: 3 }, steps: { items: 3 }, comparison: { items: 2 }, myth: { items: 2 }, pricing: { items: 3, value: true }, offer: { value: true, image: true },
10
+ editorial: { image: true }, story: { image: true }, hiring: { items: 3 }, team: { items: 3, itemImages: true }, course: { items: 3 }, 'case-study': { items: 3 }, notice: {}, community: { value: true },
11
+ menu: { items: 3 }, specification: { items: 3 }, itinerary: { items: 3 }, schedule: { items: 3 }, safety: { items: 3 }, faq: { items: 3 },
12
+ 'split-bar-compare': { items: 3, quantities: 'comparison' }, 'benchmark-bars': { items: 3, quantities: 'benchmark' }, 'usage-breakdown': { items: 3, quantities: 'share' },
13
+ organization: { items: 3 }, 'editorial-quote': { attribution: true }, 'research-highlight': { items: 2 }, 'product-showcase': { items: 3, image: true },
14
+ };
15
+ const copy = (maxLength, description, minLength = 1) => ({ type: 'string', minLength, maxLength, description });
16
+ const object = (properties) => ({ type: 'object', additionalProperties: false, properties, required: Object.keys(properties) });
17
+ const asset = () => copy(160, 'An existing supplied asset ID. Use neatico-editorial-placeholder when no matching source image exists.');
18
+ export const COMPOSITION_ILLUSTRATIONS = ['document', 'collaboration', 'review', 'feedback', 'sequence', 'comparison', 'structure'];
19
+ const illustratedSlots = new Set(['bento', 'course', 'case-study', 'report', 'product-showcase', 'comparison', 'research-highlight']);
20
+ const provenance = ['user-fact', 'derived', 'creative', 'simulated'];
21
+ // These authored slots have been measured with complete provider sentences at
22
+ // square, landscape, and portrait ratios. Compact labels keep their own limits.
23
+ const sentenceBodySlots = new Set(['note-question', 'note-marker', 'note-editorial', 'chat-imessage', 'chat-telegram', 'news-focus', 'manifesto', 'launch', 'spotlight', 'comparison', 'myth', 'editorial', 'story', 'notice']);
24
+ const supportingItemSlots = new Set(['team', 'course', 'specification', 'schedule', 'research-highlight']);
25
+ /** Every field corresponds to an authored visible slot; no scene-based eligibility filter. */
26
+ export function nativeSocialParameterContract(id) {
27
+ const spec = slots[id];
28
+ if (!spec || !SOCIAL_LAYOUT_CATALOG.some(item => item.id === id))
29
+ throw new Error('COMPOSITION_UNKNOWN_NATIVE_SOCIAL');
30
+ const properties = { title: copy(id === 'note-question' ? 44 : 40, 'Concise audience-facing headline.'), body: copy(id === 'organization' ? 32 : id === 'case-study' ? 64 : ['quote', 'testimonial', 'editorial-quote'].includes(id) ? 140 : sentenceBodySlots.has(id) ? 96 : 80, id === 'organization' ? 'A short label for the parent or coordinating role; never invent a real person.' : 'One supporting sentence, or the quoted passage for a quote composition.') };
31
+ if (!id.startsWith('chat-') && !['note-photo', 'note-checklist'].includes(id))
32
+ properties.eyebrow = { ...copy(20, 'Optional concise section label. Use an empty string when no label improves the message.', 0), default: '' };
33
+ if (!id.startsWith('chat-') && !['note-photo', 'note-checklist'].includes(id))
34
+ properties.cta = { ...copy(24, 'Optional short call to action appropriate to the input. Use an empty string when this is purely informational.', 0), default: '' };
35
+ if (['note-marker', 'note-editorial', 'note-question'].includes(id))
36
+ properties.titleLines = { type: 'array', minItems: 0, maxItems: 4, default: [], items: copy(40, 'One complete phrase from the title, preserving its original language.'), description: 'Preferred title lines: one item for a single line, two to four for a multiline heading, or empty for automatic fitting. Concatenating the phrases must preserve the complete title and word boundaries. The renderer may reflow only when the requested lines cannot fit legibly.' };
37
+ if (id === 'note-marker')
38
+ properties.emphasis = { type: 'array', minItems: 0, maxItems: 2, default: [], items: copy(40, 'An exact contiguous phrase from title to highlight.'), description: 'Choose one or two important exact title phrases. Empty means the renderer highlights the final title line.' };
39
+ if (spec.value)
40
+ properties.value = copy(24, 'The focal quantity, price, time, or short value statement. Label fictional quantities as simulated.');
41
+ if (spec.attribution)
42
+ properties.attribution = copy(48, 'Exact supplied attribution, or an empty string. Never invent an endorsement.', 0);
43
+ if (spec.image)
44
+ properties.imageAssetId = asset();
45
+ if (spec.items) {
46
+ const item = { title: copy(id === 'faq' ? 40 : id === 'case-study' ? 18 : 20, id === 'faq' ? 'A concise audience question ending in a question mark.' : 'A short item label.'), description: copy(id === 'faq' ? 64 : spec.quantities || id === 'case-study' ? 32 : ['comparison', 'myth'].includes(id) ? 80 : supportingItemSlots.has(id) ? 48 : 40, 'Concise supporting copy.', spec.quantities ? 0 : 1) };
47
+ if (id === 'faq')
48
+ item.title.pattern = '^.+[??]$';
49
+ if (illustratedSlots.has(id))
50
+ item.illustration = { type: 'string', enum: COMPOSITION_ILLUSTRATIONS, default: 'structure', description: 'An editable native illustration that explains this item. Choose document for material, review for checking, collaboration for people, feedback for revision, sequence for ordered actions, comparison for alternatives, or structure for organization. Never omit the visual slot.' };
51
+ if (spec.itemImages)
52
+ item.assetId = asset();
53
+ if (spec.quantities) {
54
+ item.primaryValue = { type: 'number', minimum: 0, maximum: 100, description: spec.quantities === 'comparison' ? 'A percentage from zero to one hundred. primaryValue plus secondaryValue must equal exactly 100 in this item.' : 'A percentage from zero to one hundred. Never encode numbers inside descriptive copy.' };
55
+ if (spec.quantities !== 'share')
56
+ item.secondaryValue = { type: 'number', minimum: 0, maximum: 100, description: 'The comparison or benchmark percentage.' };
57
+ if (spec.quantities === 'share')
58
+ item.description = { const: '' };
59
+ }
60
+ properties.items = { type: 'array', minItems: spec.items, maxItems: spec.items, items: object(item), ...(spec.quantities === 'share' ? { description: 'Exactly three nonnegative percentage shares totaling at most one hundred.' } : {}) };
61
+ }
62
+ if (spec.quantities === 'comparison' || spec.quantities === 'benchmark')
63
+ properties.comparisonLabels = { type: 'array', minItems: 2, maxItems: 2, items: copy(20, 'A supplied or explicitly illustrative series label.') };
64
+ if (['note-question', 'note-marker', 'note-editorial'].includes(id))
65
+ properties.decorationEmoji = { type: 'string', enum: ['', '✨', '💡', '🌱', '💬', '🎯', '📚'], default: '', description: 'Optional built-in decorative emoji for informal social notes. Choose one relevant symbol, or empty for the authored illustration. Never use it as factual evidence.' };
66
+ properties.textPresentation = textPresentationSchema();
67
+ const contentKeys = Object.keys(properties).filter(key => !['textPresentation', 'decorationEmoji'].includes(key));
68
+ const importance = Object.fromEntries(contentKeys.map(key => [key, key === 'title' ? 3 : key === 'items' ? 7 : ['eyebrow', 'cta'].includes(key) ? .5 : 2]));
69
+ return { parameterModel: 'native-social', parameterSchema: object(properties), importance, generationPolicy: Object.fromEntries(contentKeys.map(key => [key, provenance])) };
70
+ }
71
+ /** Apply the small closed schema vocabulary used by native slots on every local render. */
72
+ export function validateNativeSocialParameters(id, raw) {
73
+ const check = (value, schema) => {
74
+ if (Object.hasOwn(schema, 'const'))
75
+ return value === schema.const;
76
+ if (Array.isArray(schema.enum))
77
+ return schema.enum.includes(value);
78
+ if (schema.type === 'string')
79
+ return typeof value === 'string' && Array.from(value).length >= Number(schema.minLength || 0) && Array.from(value).length <= Number(schema.maxLength) && (!schema.pattern || new RegExp(String(schema.pattern), 'u').test(value));
80
+ if (schema.type === 'boolean')
81
+ return typeof value === 'boolean';
82
+ if (schema.type === 'number')
83
+ return typeof value === 'number' && Number.isFinite(value) && value >= Number(schema.minimum) && value <= Number(schema.maximum);
84
+ if (schema.type === 'array')
85
+ return Array.isArray(value) && value.length >= Number(schema.minItems) && value.length <= Number(schema.maxItems) && value.every(item => check(item, schema.items));
86
+ if (schema.type === 'object') {
87
+ if (!value || typeof value !== 'object' || Array.isArray(value))
88
+ return false;
89
+ const properties = schema.properties, object = value;
90
+ return Object.keys(object).every(key => Object.hasOwn(properties, key) && check(object[key], properties[key])) && schema.required.every(key => Object.hasOwn(object, key) && check(object[key], properties[key]));
91
+ }
92
+ return false;
93
+ };
94
+ const schema = nativeSocialParameterContract(id).parameterSchema;
95
+ raw = compositionParameterDefaults(schema, raw);
96
+ if (!check(raw, schema))
97
+ throw new Error('CANDIDATE_INVALID_PARAMETERS');
98
+ const values = raw;
99
+ validateTextPresentations(values.textPresentation, values);
100
+ if (values.titleLines?.length) {
101
+ const normalize = (copy) => copy.normalize('NFC').replace(/\s+/gu, ' ').trim();
102
+ const title = normalize(values.title);
103
+ let cursor = 0;
104
+ for (const phrase of values.titleLines) {
105
+ const line = normalize(phrase), hadSpace = title[cursor] === ' ';
106
+ if (hadSpace)
107
+ cursor++;
108
+ // CJK phrases may meet without spaces, but Latin words cannot be split.
109
+ const brokenWord = cursor > 0 && !hadSpace && /[\p{Script=Latin}\p{Mark}\d’'-]/u.test(title[cursor - 1]) && /^[\p{Script=Latin}\p{Mark}\d’'-]/u.test(line);
110
+ if (!line || brokenWord || !title.startsWith(line, cursor))
111
+ throw new Error('CANDIDATE_INVALID_TITLE_LINES');
112
+ cursor += line.length;
113
+ }
114
+ if (cursor !== title.length)
115
+ throw new Error('CANDIDATE_INVALID_TITLE_LINES');
116
+ }
117
+ if (values.emphasis?.some(phrase => !values.title.includes(phrase)))
118
+ throw new Error('CANDIDATE_INVALID_EMPHASIS');
119
+ if (id === 'usage-breakdown' && values.items.reduce((sum, item) => sum + item.primaryValue, 0) > 100.0001)
120
+ throw new Error('CANDIDATE_INVALID_PERCENTAGES');
121
+ if (id === 'split-bar-compare' && values.items.some(item => Math.abs(item.primaryValue + item.secondaryValue - 100) > .001))
122
+ throw new Error('CANDIDATE_INVALID_PERCENTAGES');
123
+ return JSON.parse(JSON.stringify(values));
124
+ }
125
+ /** Numeric and image identities remain available when a native design becomes editable content. */
126
+ export function nativeSocialContentPage(values, original) {
127
+ const proof = original.evidence, blocks = [{ id: 'native-body', kind: 'paragraph', text: values.body, evidence: proof }];
128
+ if (values.items?.length)
129
+ blocks.push({ id: 'native-items', kind: 'features', evidence: proof, entries: values.items.map(item => ({ title: item.title, ...(item.illustration ? { illustration: item.illustration } : {}), description: [item.description, ...[item.primaryValue, item.secondaryValue].filter(value => value !== undefined).map(value => `${value}%`)].filter(Boolean).join(' · ') })) });
130
+ for (const field of ['value', 'attribution', 'eyebrow', 'cta'])
131
+ if (values[field])
132
+ blocks.push({ id: `native-${field}`, kind: 'paragraph', text: values[field], evidence: proof });
133
+ if (values.comparisonLabels)
134
+ blocks.push({ id: 'native-series', kind: 'paragraph', evidence: proof, text: values.comparisonLabels.join(' / ') });
135
+ const images = [values.imageAssetId, ...(values.items || []).map(item => item.assetId)].filter((id) => !!id);
136
+ for (const [index, assetId] of images.entries())
137
+ blocks.push({ id: `native-image-${index}`, kind: 'image', evidence: [{ sourceId: assetId }], sourceId: assetId, assetId, alt: values.title, treatment: assetId === 'neatico-editorial-placeholder' ? 'diagram' : 'photograph', status: 'available' });
138
+ return { ...original, title: values.title, blocks };
139
+ }
@@ -1,12 +1,21 @@
1
1
  /** Split only at explicit word/sentence boundaries. Never cut identifiers,
2
2
  * decimal values, or unbroken URLs to force them onto a smaller canvas. */
3
3
  export function splitCopy(copy) {
4
- const breaks = [...copy.matchAll(/\s+|[。!?;]|[.!?;](?=\s|$)/gu)]
5
- .map((match) => match.index + match[0].length)
6
- .filter((index) => index > 0 && index < copy.length);
7
- if (!breaks.length)
4
+ let at;
5
+ let distance = Infinity;
6
+ const midpoint = copy.length / 2;
7
+ for (const match of copy.matchAll(/\s+|[。!?;]|[.!?;](?=\s|$)/gu)) {
8
+ const index = match.index + match[0].length;
9
+ if (index <= 0 || index >= copy.length)
10
+ continue;
11
+ const candidateDistance = Math.abs(index - midpoint);
12
+ if (candidateDistance < distance) {
13
+ at = index;
14
+ distance = candidateDistance;
15
+ }
16
+ }
17
+ if (at === undefined)
8
18
  return;
9
- const at = breaks.sort((a, b) => Math.abs(a - copy.length / 2) - Math.abs(b - copy.length / 2))[0];
10
19
  return [copy.slice(0, at), copy.slice(at)];
11
20
  }
12
21
  /** No paraphrasing, invented continuation labels or removed constraints. The
@@ -0,0 +1,2 @@
1
+ /** Apply only renderer-authored defaults. Never rewrite supplied copy or values. */
2
+ export declare function compositionParameterDefaults(schema: Readonly<Record<string, unknown>>, value: unknown): unknown;
@@ -0,0 +1,17 @@
1
+ /** Apply only renderer-authored defaults. Never rewrite supplied copy or values. */
2
+ export function compositionParameterDefaults(schema, value) {
3
+ if (value === undefined && Object.hasOwn(schema, 'default'))
4
+ return JSON.parse(JSON.stringify(schema.default));
5
+ if (schema.type === 'object' && value && typeof value === 'object' && !Array.isArray(value)) {
6
+ const next = { ...value };
7
+ for (const [key, child] of Object.entries(schema.properties || {})) {
8
+ const resolved = compositionParameterDefaults(child, next[key]);
9
+ if (resolved !== undefined)
10
+ next[key] = resolved;
11
+ }
12
+ return next;
13
+ }
14
+ if (schema.type === 'array' && Array.isArray(value))
15
+ return value.map((item, index) => compositionParameterDefaults((Array.isArray(schema.items) ? schema.items[index] : schema.items) || {}, item));
16
+ return value;
17
+ }
@@ -0,0 +1,34 @@
1
+ import { type DesignRecipe } from "./designSystem.js";
2
+ export type ParameterSchema = Readonly<Record<string, unknown>>;
3
+ export type CompositionKind = "presentation" | "social" | "article-cover" | "article-illustration";
4
+ export interface RecipeParameterContract {
5
+ parameterModel?: "semantic" | "native-social";
6
+ id: string;
7
+ schemaVersion: 1;
8
+ supportedKinds: readonly CompositionKind[];
9
+ semanticDescription: string;
10
+ scenes: readonly string[];
11
+ rendererKey: string;
12
+ capacity: {
13
+ minEntries: number;
14
+ maxEntries: number;
15
+ requiresImage: boolean;
16
+ };
17
+ parameterSchema: ParameterSchema;
18
+ importance: Readonly<Record<string, number>>;
19
+ generationPolicy: Readonly<Record<string, readonly string[]>>;
20
+ themeBindings: readonly string[];
21
+ fallbackPolicy: {
22
+ missingContent: "derive-or-labeled-example";
23
+ missingAsset: "search-or-native-placeholder";
24
+ omitCandidate: false;
25
+ };
26
+ }
27
+ /** Fixed renderer parameters, not executable layout trees. The complete schema
28
+ * travels with each formal generation batch; summaries are scheduling aids only.
29
+ */
30
+ export declare function recipeParameterContract(recipe: DesignRecipe, supportedKinds?: RecipeParameterContract["supportedKinds"]): RecipeParameterContract;
31
+ /** Shared recipes are one part of the formal catalog; host-specific renderer
32
+ * definitions register through the same contract constructor during migration.
33
+ */
34
+ export declare const RECIPE_PARAMETER_CONTRACTS: readonly RecipeParameterContract[];
@@ -0,0 +1,371 @@
1
+ import { COMPOSITION_ILLUSTRATIONS } from "./nativeSocialParameters.js";
2
+ import { textPresentationSchema } from "./textPresentation.js";
3
+ import { DESIGN_RECIPES } from "./designSystem.js";
4
+ const text = (description, maxLength = 600) => ({
5
+ type: "string",
6
+ description,
7
+ maxLength,
8
+ });
9
+ const object = (properties, required) => ({
10
+ type: "object",
11
+ additionalProperties: false,
12
+ properties,
13
+ required,
14
+ });
15
+ const array = (items, minItems, maxItems) => ({ type: "array", items, minItems, maxItems });
16
+ const evidence = array(object({
17
+ sourceId: text("Existing source segment ID.", 160),
18
+ quote: text("Exact source quotation.", 2000),
19
+ }, ["sourceId"]), 1, 32);
20
+ const base = (kind, properties, required) => object({
21
+ id: text("Stable block ID, unique within this page.", 80),
22
+ kind: { const: kind },
23
+ evidence,
24
+ ...properties,
25
+ }, ["id", "kind", "evidence", ...required]);
26
+ const descriptions = {
27
+ paragraph: "Explain a narrative idea with readable prose and optional section headings.",
28
+ features: "Present parallel capabilities or a list of distinct ideas without implying a sequence.",
29
+ steps: "Explain ordered actions or a chronological sequence supported by the source.",
30
+ comparison: "Compare supplied alternatives with aligned criteria; never invent a competing alternative.",
31
+ parameters: "Present exact specifications, logistics, identifiers, dates, prices, or named facts.",
32
+ quote: "Emphasize a verbatim quotation with its supplied attribution.",
33
+ chart: "Explain a comparable numeric series with source-backed values and a shared unit.",
34
+ code: "Display exact source code without rewriting identifiers, literals, or line breaks.",
35
+ image: "Arrange a real, previously supplied image asset with its source reference and focal subject.",
36
+ };
37
+ function blockSchema(kind, maximum) {
38
+ const count = Math.max(1, Math.min(12, maximum || 12));
39
+ switch (kind) {
40
+ case "paragraph":
41
+ return base(kind, {
42
+ text: text("Source-faithful prose.", 2000),
43
+ heading: text("Optional source-supported heading.", 120),
44
+ level: { enum: [2, 3] },
45
+ }, ["text"]);
46
+ case "features":
47
+ case "steps":
48
+ return base(kind, {
49
+ entries: array(object({
50
+ title: text("Concise item label without an ordinal prefix.", 120),
51
+ description: text("Source-faithful explanation; omit filler.", 600),
52
+ }, ["title", "description"]), 1, count),
53
+ }, ["entries"]);
54
+ case "comparison":
55
+ return base(kind, {
56
+ columns: array(object({
57
+ title: text("A supplied alternative.", 120),
58
+ points: array(text("Source-backed comparison criterion.", 600), 1, 8),
59
+ price: text("Exact source price, only when supplied.", 120),
60
+ values: array(text("Exact value aligned with points.", 160), 1, 8),
61
+ statuses: array({ enum: ["included", "excluded", "partial", "unknown"] }, 1, 8),
62
+ }, ["title", "points"]), 2, Math.max(2, Math.min(4, count))),
63
+ }, ["columns"]);
64
+ case "parameters":
65
+ return base(kind, {
66
+ rows: array(object({
67
+ name: text("Fact name.", 120),
68
+ value: text("Exact source value.", 300),
69
+ description: text("Source-supported qualification, otherwise an empty string.", 600),
70
+ }, ["name", "value", "description"]), 1, count),
71
+ }, ["rows"]);
72
+ case "quote":
73
+ return base(kind, {
74
+ text: text("Verbatim source quotation.", 1200),
75
+ attribution: text("Exact source attribution.", 160),
76
+ role: text("Role only when explicitly supplied.", 160),
77
+ avatarAssetId: text("Existing supplied portrait asset ID; never invent a person or image.", 160),
78
+ }, ["text", "attribution"]);
79
+ case "chart":
80
+ return base(kind, {
81
+ chart: { enum: ["bar", "line"] },
82
+ unit: text("Exact source unit.", 80),
83
+ values: array(object({
84
+ label: text("Exact source label.", 120),
85
+ value: { type: "number" },
86
+ }, ["label", "value"]), 2, 12),
87
+ }, ["chart", "unit", "values"]);
88
+ case "code":
89
+ return base(kind, {
90
+ sourceId: text("Existing code source ID; code remains unchanged.", 160),
91
+ language: text("Source code language.", 80),
92
+ code: text("Exact source code.", 20000),
93
+ }, ["sourceId", "language", "code"]);
94
+ case "image":
95
+ return base(kind, {
96
+ sourceId: text("Existing image source ID.", 160),
97
+ assetId: text("Existing asset ID; no generated URLs.", 160),
98
+ alt: text("Subject and purpose of the supplied image.", 600),
99
+ treatment: { enum: ["screenshot", "photograph", "diagram"] },
100
+ status: { enum: ["available", "missing"] },
101
+ }, ["sourceId", "alt", "treatment", "status"]);
102
+ }
103
+ }
104
+ /** Each authored renderer receives a fixed block sequence and bounded copy. */
105
+ function fixedBlocks(recipe) {
106
+ const id = recipe.id;
107
+ let kinds;
108
+ if (["authored-explore-cover", "authored-explore-photo-story"].includes(id))
109
+ kinds = ["paragraph", "image"];
110
+ else if (id.endsWith("ledger-chart"))
111
+ kinds = ["parameters", "chart"];
112
+ else if (id.endsWith("detail-sidebar"))
113
+ kinds = ["paragraph", "parameters"];
114
+ else if (id.endsWith("technical-log"))
115
+ kinds = ["paragraph", "code"];
116
+ else if (id.endsWith("architecture-stack"))
117
+ kinds = ["steps", "paragraph"];
118
+ else if (["authored-mechanism-flow", "authored-reference-capability-map"].includes(id))
119
+ kinds = ["features", "paragraph"];
120
+ else if (id === "authored-founder-timeline")
121
+ kinds = ["steps", "paragraph"];
122
+ else if (id === "authored-founder-comparison")
123
+ kinds = ["comparison", "paragraph"];
124
+ else if (id === "semantic-hero-editorial")
125
+ kinds = ["paragraph", "paragraph", "paragraph", "image"];
126
+ else if (id.endsWith("speaker-lineup"))
127
+ kinds = ["parameters", "quote", "quote", "quote"];
128
+ else if (id.endsWith("speaker-spotlight"))
129
+ kinds = ["parameters", "quote"];
130
+ else if (id === "authored-reference-story")
131
+ kinds = ["image", "paragraph", "features"];
132
+ else if (id === "authored-reference-photo")
133
+ kinds = ["paragraph", "image"];
134
+ else if (id === "authored-image-triptych")
135
+ kinds = ["paragraph", "features", "paragraph", "image"];
136
+ else if (recipe.requiresImage && recipe.accepts.includes("image"))
137
+ kinds = ["image"];
138
+ else if (recipe.family === "mixed-stack")
139
+ kinds = ["features"];
140
+ else
141
+ kinds = [
142
+ recipe.accepts.find((kind) => kind !== "paragraph" && kind !== "image") ||
143
+ "paragraph",
144
+ ];
145
+ if (id.startsWith("bookend-") || recipe.family.startsWith("prose-"))
146
+ kinds = ["paragraph"];
147
+ const items = kinds.map((kind) => {
148
+ const schema = JSON.parse(JSON.stringify(blockSchema(kind, recipe.maxEntries)));
149
+ const fields = schema.properties;
150
+ const count = Math.max(recipe.minEntries || 1, Math.min(3, recipe.maxEntries || 3));
151
+ if (kind === "paragraph")
152
+ fields.text.maxLength = [
153
+ "authored-reference-detail-sidebar",
154
+ "authored-reference-diagonal",
155
+ "authored-prose-inset",
156
+ ].includes(id)
157
+ ? 160
158
+ : 120;
159
+ if (kind === "features" || kind === "steps") {
160
+ const entries = fields.entries;
161
+ entries.minItems = id === "authored-reference-story" ? 2 : count;
162
+ entries.maxItems = count;
163
+ entries.items.properties.illustration = {
164
+ type: "string",
165
+ enum: [...COMPOSITION_ILLUSTRATIONS, "symbol"],
166
+ default: "structure",
167
+ description: "Editable native illustration for the item. Choose the visual concept that explains its meaning. This field controls a graphic, not prose.",
168
+ };
169
+ entries.items.required.push("illustration");
170
+ entries.items.properties.icon = {
171
+ type: "string",
172
+ enum: [
173
+ "BsJournalText",
174
+ "BsPersonCheck",
175
+ "BsShare",
176
+ "BsPeople",
177
+ "BsSearch",
178
+ "BsShieldCheck",
179
+ "BsBarChart",
180
+ "BsCodeSlash",
181
+ "BsSignpostSplit",
182
+ "BsCheck2Circle",
183
+ "BsFolder2Open",
184
+ "BsGrid1X2",
185
+ ],
186
+ description: "Optional authored icon variant; illustration remains the primary visual concept.",
187
+ };
188
+ entries.items.properties.title.maxLength = 28;
189
+ entries.items.properties.description.maxLength =
190
+ id === "authored-feature-focus" ? 160 : 48;
191
+ if (/(?:reference-faq|editorial-faq)$/.test(id) || id === "faq")
192
+ entries.items.properties.title.pattern = "^.+[??]$";
193
+ }
194
+ if (kind === "parameters") {
195
+ const rows = id.endsWith("ledger-chart") ||
196
+ id.endsWith("detail-sidebar") ||
197
+ id.includes("speaker-")
198
+ ? 2
199
+ : count;
200
+ fields.rows.minItems = id.endsWith("speaker-lineup")
201
+ ? 1
202
+ : id === "pricing"
203
+ ? 2
204
+ : rows;
205
+ fields.rows.maxItems = rows;
206
+ fields.rows.items.properties.name.maxLength = 20;
207
+ fields.rows.items.properties.value.maxLength = 24;
208
+ fields.rows.items.properties.description.maxLength = 64;
209
+ if (id.includes("speaker-"))
210
+ fields.rows.items.properties.description = { const: "" };
211
+ }
212
+ if (kind === "comparison") {
213
+ fields.columns.minItems = 2;
214
+ fields.columns.maxItems = 2;
215
+ const column = fields.columns.items.properties;
216
+ column.title.maxLength = 20;
217
+ column.points.minItems = id === "authored-founder-comparison" ? 1 : 2;
218
+ column.points.maxItems = 3;
219
+ column.points.items.maxLength =
220
+ id === "authored-founder-comparison" ? 64 : 40;
221
+ column.values.maxItems = 3;
222
+ column.values.items.maxLength = 16;
223
+ column.values.minItems = 0;
224
+ column.statuses.minItems = 0;
225
+ column.statuses.maxItems = 3;
226
+ column.price.maxLength = 24;
227
+ }
228
+ if (kind === "quote") {
229
+ fields.text.maxLength = id.includes("speaker-") ? 48 : 160;
230
+ fields.attribution.maxLength = 32;
231
+ if (id.includes("speaker-")) {
232
+ fields.avatarAssetId.description =
233
+ "An existing portrait asset ID, or neatico-editorial-placeholder for an original source-attributed illustration.";
234
+ schema.required.push("avatarAssetId");
235
+ }
236
+ }
237
+ if (kind === "code") {
238
+ fields.code.maxLength = 400;
239
+ fields.code.description =
240
+ "A concise exact source excerpt, or a clearly labeled illustrative example when no source code exists. At most eight lines and 56 columns per line.";
241
+ fields.code.pattern = String.raw `^(?:[^\n]{0,56}\n){0,7}[^\n]{0,56}$`;
242
+ }
243
+ if (kind === "image")
244
+ fields.alt.maxLength = 80;
245
+ if (kind === "chart") {
246
+ const count = id.endsWith("ledger-chart") ? 3 : 4;
247
+ fields.values.minItems = Math.min(3, count);
248
+ fields.values.maxItems = count;
249
+ fields.values.items.properties.label.maxLength = 20;
250
+ fields.unit.maxLength = 24;
251
+ if (/trend/.test(id))
252
+ fields.chart = { const: "line" };
253
+ if (/bars|ledger-chart/.test(id))
254
+ fields.chart = { const: "bar" };
255
+ if (/donut|reference-share|reference-metrics/.test(id)) {
256
+ fields.chart = { const: "bar" };
257
+ fields.unit = {
258
+ type: "string",
259
+ maxLength: 24,
260
+ pattern: "%",
261
+ description: "Percentage unit, optionally qualified as illustrative or simulated.",
262
+ };
263
+ fields.values.description =
264
+ "Nonnegative shares totaling exactly 100. Use labeled illustrative shares when the input has no comparable numbers.";
265
+ fields.values.items.properties.value.minimum = 0;
266
+ fields.values.items.properties.value.maximum = 100;
267
+ }
268
+ }
269
+ if (id.startsWith("authored-explore-")) {
270
+ if (kind === "paragraph") {
271
+ fields.text.maxLength = 220;
272
+ if (id !== "authored-explore-focus") {
273
+ delete fields.heading;
274
+ delete fields.level;
275
+ }
276
+ }
277
+ if (kind === "features" || kind === "steps") {
278
+ fields.entries.minItems = Math.max(2, recipe.minEntries);
279
+ fields.entries.maxItems = recipe.maxEntries;
280
+ fields.entries.items.properties.title.maxLength = 44;
281
+ fields.entries.items.properties.description.maxLength = 120;
282
+ delete fields.entries.items.properties.illustration;
283
+ delete fields.entries.items.properties.icon;
284
+ fields.entries.items.required = fields.entries.items.required.filter((key) => !["illustration", "icon"].includes(key));
285
+ }
286
+ if (kind === "parameters") {
287
+ fields.rows.minItems = recipe.minEntries;
288
+ fields.rows.maxItems = recipe.maxEntries;
289
+ fields.rows.items.properties.name.maxLength = 36;
290
+ fields.rows.items.properties.value.maxLength = 120;
291
+ fields.rows.items.properties.description.maxLength = 60;
292
+ }
293
+ if (kind === "comparison") {
294
+ fields.columns.items.properties.points.maxItems = 3;
295
+ fields.columns.items.properties.points.items.maxLength = 100;
296
+ for (const key of ["price", "values", "statuses"])
297
+ delete fields.columns.items.properties[key];
298
+ }
299
+ if (kind === "chart") {
300
+ fields.chart = { const: "bar" };
301
+ fields.values.minItems = 2;
302
+ fields.values.maxItems = 6;
303
+ fields.values.items.properties.value.minimum = 0;
304
+ }
305
+ }
306
+ return schema;
307
+ });
308
+ return {
309
+ type: "array",
310
+ items,
311
+ minItems: id === "authored-explore-cover" ? 1 : items.length,
312
+ maxItems: items.length,
313
+ additionalItems: false,
314
+ description: "Fixed ordered parameters for this renderer. Return every block exactly once in this order; do not add explanatory blocks.",
315
+ };
316
+ }
317
+ /** Fixed renderer parameters, not executable layout trees. The complete schema
318
+ * travels with each formal generation batch; summaries are scheduling aids only.
319
+ */
320
+ export function recipeParameterContract(recipe, supportedKinds = [recipe.kind]) {
321
+ return {
322
+ id: recipe.id,
323
+ schemaVersion: 1,
324
+ supportedKinds,
325
+ semanticDescription: recipe.accepts
326
+ .map((kind) => descriptions[kind])
327
+ .join(" "),
328
+ scenes: recipe.scenes || [],
329
+ rendererKey: `${recipe.renderer}:${recipe.id}`,
330
+ capacity: {
331
+ minEntries: recipe.minEntries,
332
+ maxEntries: recipe.maxEntries,
333
+ requiresImage: recipe.requiresImage,
334
+ },
335
+ parameterSchema: object({
336
+ title: {
337
+ ...text("Audience-facing headline preserving the original topic.", 64),
338
+ minLength: 1,
339
+ },
340
+ blocks: fixedBlocks(recipe),
341
+ textPresentation: textPresentationSchema(),
342
+ }, ["title", "blocks"]),
343
+ importance: { title: 3, blocks: 7 },
344
+ generationPolicy: {
345
+ title: ["user-fact", "derived", "creative"],
346
+ blocks: ["user-fact", "derived", "creative", "simulated"],
347
+ assets: ["existing-reference-only"],
348
+ },
349
+ fallbackPolicy: {
350
+ missingContent: "derive-or-labeled-example",
351
+ missingAsset: "search-or-native-placeholder",
352
+ omitCandidate: false,
353
+ },
354
+ themeBindings: [
355
+ "palette.background",
356
+ "palette.surface",
357
+ "palette.ink",
358
+ "palette.muted",
359
+ "palette.accent",
360
+ "palette.line",
361
+ "typography.heading",
362
+ "typography.body",
363
+ "typography.code",
364
+ "geometry",
365
+ ],
366
+ };
367
+ }
368
+ /** Shared recipes are one part of the formal catalog; host-specific renderer
369
+ * definitions register through the same contract constructor during migration.
370
+ */
371
+ export const RECIPE_PARAMETER_CONTRACTS = DESIGN_RECIPES.filter((recipe, index, recipes) => recipes.findIndex((other) => other.id === recipe.id) === index).map((recipe) => recipeParameterContract(recipe, DESIGN_RECIPES.filter((other) => other.id === recipe.id).map((other) => other.kind)));