@elytracms/studio 0.0.100 → 0.0.101
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/convex/analytics-cost.test.ts +1 -1
- package/convex/redirect-migration.test.ts +1 -1
- package/convex/redirects.ts +2 -2
- package/convex/shared-capture.convex.test.ts +165 -0
- package/dist/components/ai/ai-chat-panel-lazy.js +7 -2
- package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
- package/dist/components/ai/proposal-card.js +2 -1
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-unfilled.d.ts +14 -0
- package/dist/components/ai/proposal-unfilled.js +33 -0
- package/dist/components/ai/proposal-unfilled.js.map +1 -0
- package/dist/components/ai/translate-document-action.js +8 -6
- package/dist/components/ai/translate-document-action.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +12 -12
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/builder-store.d.ts +37 -1
- package/dist/components/builder/builder-store.js +21 -6
- package/dist/components/builder/builder-store.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.d.ts +1 -1
- package/dist/components/builder/canvas-chrome-css.js +1 -1
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-chrome.d.ts +1 -1
- package/dist/components/builder/canvas-chrome.js +1 -1
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-rebase.d.ts +4 -4
- package/dist/components/builder/canvas-rebase.js +3 -2
- package/dist/components/builder/canvas-rebase.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +1 -1
- package/dist/components/builder/canvas-render-context.js +1 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/composition-field-editor-lazy.js +2 -1
- package/dist/components/builder/composition-field-editor-lazy.js.map +1 -1
- package/dist/components/builder/reuse-context.d.ts +8 -2
- package/dist/components/builder/reuse-context.js +62 -7
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +106 -70
- package/dist/components/builder/reuse.js +125 -108
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +9 -1
- package/dist/components/cms/cms-store.js +1 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +13 -13
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +1 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/lazy.js +2 -1
- package/dist/components/cms/rich-text/lazy.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js +1 -1
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/use-field-focus.js +3 -3
- package/dist/components/cms/use-field-focus.js.map +1 -1
- package/dist/components/cms/version-compare-lazy.js +2 -1
- package/dist/components/cms/version-compare-lazy.js.map +1 -1
- package/dist/components/components/components-index.js +1 -1
- package/dist/components/components/components-index.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +1 -1
- package/dist/components/deployment-setup-gate.js +1 -1
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/load-failure.d.ts +62 -0
- package/dist/components/load-failure.js +110 -0
- package/dist/components/load-failure.js.map +1 -0
- package/dist/components/notifications/shell-findings.js +3 -3
- package/dist/components/notifications/shell-findings.js.map +1 -1
- package/dist/customization.d.ts +3 -3
- package/dist/customization.js.map +1 -1
- package/dist/lib/ai/apply-content.js +99 -28
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +73 -7
- package/dist/lib/ai/chat-context.js +133 -14
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/chat-state.d.ts +7 -1
- package/dist/lib/ai/chat-state.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +5 -0
- package/dist/lib/ai/constraints.js +21 -6
- package/dist/lib/ai/constraints.js.map +1 -1
- package/dist/lib/ai/fake-provider.d.ts +76 -26
- package/dist/lib/ai/fake-provider.js +210 -66
- package/dist/lib/ai/fake-provider.js.map +1 -1
- package/dist/lib/ai/index.d.ts +1 -0
- package/dist/lib/ai/index.js +1 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/keyless-assistant.d.ts +37 -0
- package/dist/lib/ai/keyless-assistant.js +193 -0
- package/dist/lib/ai/keyless-assistant.js.map +1 -0
- package/dist/lib/ai/shape.d.ts +25 -2
- package/dist/lib/ai/shape.js +58 -7
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +11 -1
- package/dist/lib/ai/tools.js +51 -6
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.js +6 -5
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +22 -7
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.js +9 -6
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/ai/writer-form.d.ts +11 -3
- package/dist/lib/ai/writer-form.js +105 -18
- package/dist/lib/ai/writer-form.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +1 -1
- package/dist/lib/auth-errors.js +1 -1
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/engine-version.d.ts +1 -1
- package/dist/lib/engine-version.js +1 -1
- package/dist/lib/engine-version.js.map +1 -1
- package/dist/lib/i18n/messages.de.d.ts +1 -1
- package/dist/lib/i18n/messages.de.js +23 -7
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +18 -3
- package/dist/lib/i18n/messages.en.js +29 -9
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/cms-persistence.d.ts +18 -0
- package/dist/lib/operations/cms-persistence.js +47 -0
- package/dist/lib/operations/cms-persistence.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +3 -3
- package/dist/lib/workspace/index.js +3 -3
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/router/navigate-once.d.ts +1 -1
- package/dist/router/navigate-once.js.map +1 -1
- package/dist/router/root.d.ts +3 -2
- package/dist/router/root.js +25 -4
- package/dist/router/root.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **The keyless assistant** (EC-715): the ONE deterministic, offline provider a project runs
|
|
3
|
+
* when no model key is set — so a fresh scaffold's assistant works on WHATEVER schema it has,
|
|
4
|
+
* not on the collections one starter kit happens to declare.
|
|
5
|
+
*
|
|
6
|
+
* - the conversation: {@link createFakeCreateFlowChatModel}, which reads the project's catalog
|
|
7
|
+
* and the open document from the system prompt (the route has no data access; that prose is
|
|
8
|
+
* the only channel) and derives every answer from the tool results;
|
|
9
|
+
* - the draft: {@link keylessDraft}, the structured responder `applyContentWithShape` calls
|
|
10
|
+
* with the create's target shape — it fills what the shape OFFERS, at every depth (nested
|
|
11
|
+
* objects included), and nothing else, so it can never name a hallucinated id, a read-only
|
|
12
|
+
* field (EC-716) or the slug the engine derives; a slug role the shape offers (marked with
|
|
13
|
+
* THE slug format) is the topic as a URL segment, never prose;
|
|
14
|
+
* - the translation: {@link keylessTranslate}, a visible pseudo-translation (`[de] …`) that
|
|
15
|
+
* keeps every placeholder token, so the engine rebuilds rich text exactly.
|
|
16
|
+
*
|
|
17
|
+
* A scaffold's AI handler and the demo's are one call: `createKeylessProvider()`. Swap in a
|
|
18
|
+
* real key for real output.
|
|
19
|
+
*/
|
|
20
|
+
import { slugifySegment } from '@elytracms/core/cms-core';
|
|
21
|
+
import { createFakeCreateFlowChatModel, createFakeProvider } from './fake-provider.js';
|
|
22
|
+
import { mapEncodedSegments } from './rich-text-translate.js';
|
|
23
|
+
import { SLUG_SEGMENT_DESCRIPTION } from './shape.js';
|
|
24
|
+
import { TRANSLATE_SCHEMA_NAME } from './translate-document.js';
|
|
25
|
+
/** A leading imperative the topic leaves out ("Create a new …", "Please write …"). */
|
|
26
|
+
const IMPERATIVE = /^(?:please\s+)?(?:create|write|draft|add|make|compose)\s+(?:me\s+)?(?:(?:a|an|the|some|one|another)\s+)?(?:new\s+)?/i;
|
|
27
|
+
/**
|
|
28
|
+
* What a keyless draft writes about: the instruction's `about …` clause, else the
|
|
29
|
+
* instruction without its leading imperative — read off the authoring prompt
|
|
30
|
+
* (`Instruction:\n…`, `applyContentWithShape`'s first part).
|
|
31
|
+
*/
|
|
32
|
+
export function keylessTopic(prompt) {
|
|
33
|
+
const instruction = /Instruction:\n([\s\S]*?)(?:\n\n|$)/.exec(prompt)?.[1]?.trim() ?? prompt.trim();
|
|
34
|
+
const about = /\babout\s+(.+)/i.exec(instruction)?.[1] ?? instruction.replace(IMPERATIVE, '');
|
|
35
|
+
return about.replace(/["'.?!\s]+$/g, '').trim() || 'your new document';
|
|
36
|
+
}
|
|
37
|
+
function titleCase(text) {
|
|
38
|
+
return text.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
39
|
+
}
|
|
40
|
+
/** The topic as a URL segment — THE default slug rule (core's `slugifySegment`), never a second one. */
|
|
41
|
+
function slugOf(topic) {
|
|
42
|
+
return slugifySegment(topic) || 'draft';
|
|
43
|
+
}
|
|
44
|
+
/** An identifier-like name (`layoutId`, `key`): no prose fits it — left empty unless a pattern says how. */
|
|
45
|
+
const IDENTIFIER = /(?:^(?:id|key)$|[a-z0-9](?:Id|Key)$)/;
|
|
46
|
+
function num(value) {
|
|
47
|
+
return typeof value === 'number' ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
function propertiesOf(schema) {
|
|
50
|
+
const properties = schema.properties;
|
|
51
|
+
return typeof properties === 'object' && properties !== null && !Array.isArray(properties)
|
|
52
|
+
? properties
|
|
53
|
+
: {};
|
|
54
|
+
}
|
|
55
|
+
/** A string within the schema's length bounds. */
|
|
56
|
+
function bounded(text, schema) {
|
|
57
|
+
const max = num(schema.maxLength);
|
|
58
|
+
const min = num(schema.minLength) ?? 0;
|
|
59
|
+
let out = text;
|
|
60
|
+
while (out.length < min)
|
|
61
|
+
out = `${out} More details follow.`;
|
|
62
|
+
return max !== undefined ? out.slice(0, max).trimEnd() : out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A plain string position: by its role, its format, its pattern, then its name. `undefined` —
|
|
66
|
+
* left empty (and so listed on the proposal).
|
|
67
|
+
*/
|
|
68
|
+
function draftString(name, schema, topic) {
|
|
69
|
+
// EC-715: the collection's AUTHORED slug role (the shape marks it) — a URL segment, never prose.
|
|
70
|
+
if (typeof schema.description === 'string' && schema.description.startsWith(SLUG_SEGMENT_DESCRIPTION)) {
|
|
71
|
+
return slugOf(topic);
|
|
72
|
+
}
|
|
73
|
+
const format = typeof schema.format === 'string' ? schema.format : undefined;
|
|
74
|
+
if (format === 'date-time' || format === 'date' || format === 'time')
|
|
75
|
+
return undefined;
|
|
76
|
+
if (format === 'email' || /e-?mail/i.test(name))
|
|
77
|
+
return 'hello@example.com';
|
|
78
|
+
if (format === 'uri' || format === 'url' || /^(url|href|link|website)$/i.test(name))
|
|
79
|
+
return 'https://example.com/';
|
|
80
|
+
if (typeof schema.pattern === 'string') {
|
|
81
|
+
const candidate = slugOf(topic);
|
|
82
|
+
try {
|
|
83
|
+
return new RegExp(schema.pattern).test(candidate) ? candidate : undefined;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (IDENTIFIER.test(name))
|
|
90
|
+
return undefined;
|
|
91
|
+
if (/title|name|heading|headline|label/i.test(name))
|
|
92
|
+
return bounded(titleCase(topic), schema);
|
|
93
|
+
return bounded(`An overview of ${topic}.`, schema);
|
|
94
|
+
}
|
|
95
|
+
/** The value one offered position gets — recursing into objects; `undefined` leaves it empty. */
|
|
96
|
+
function draftValue(name, schema, topic) {
|
|
97
|
+
if (schema.const !== undefined)
|
|
98
|
+
return schema.const;
|
|
99
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0)
|
|
100
|
+
return schema.enum[0];
|
|
101
|
+
if (typeof schema.description === 'string' && schema.description.startsWith('Rich text')) {
|
|
102
|
+
// Authored as MARKDOWN (EC-379); the engine converts it to the stored value.
|
|
103
|
+
return `## ${titleCase(topic)}\n\nA draft about **${topic}**, written by the keyless assistant for you to review and edit.`;
|
|
104
|
+
}
|
|
105
|
+
const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
|
|
106
|
+
switch (type) {
|
|
107
|
+
case 'object':
|
|
108
|
+
return draftObject(schema, topic);
|
|
109
|
+
case 'array': {
|
|
110
|
+
const items = schema.items;
|
|
111
|
+
if (typeof items !== 'object' || items === null || Array.isArray(items))
|
|
112
|
+
return undefined;
|
|
113
|
+
const item = items;
|
|
114
|
+
// A relation list offers its searched ids as an enum: take the first.
|
|
115
|
+
if (Array.isArray(item.enum) && item.enum.length > 0)
|
|
116
|
+
return [item.enum[0]];
|
|
117
|
+
if ((num(schema.minItems) ?? 0) > 0) {
|
|
118
|
+
const one = draftValue(name, item, topic);
|
|
119
|
+
return one === undefined ? undefined : [one];
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
case 'number':
|
|
124
|
+
case 'integer': {
|
|
125
|
+
const minimum = num(schema.minimum) ?? (num(schema.exclusiveMinimum) !== undefined ? num(schema.exclusiveMinimum) + 1 : 0);
|
|
126
|
+
return minimum;
|
|
127
|
+
}
|
|
128
|
+
case 'boolean':
|
|
129
|
+
return false;
|
|
130
|
+
case 'string':
|
|
131
|
+
return draftString(name, schema, topic);
|
|
132
|
+
default:
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function draftObject(schema, topic) {
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [key, sub] of Object.entries(propertiesOf(schema))) {
|
|
139
|
+
const value = draftValue(key, sub, topic);
|
|
140
|
+
if (value !== undefined)
|
|
141
|
+
out[key] = value;
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The keyless DRAFT (EC-715): a deterministic document for a create's target shape. It fills
|
|
147
|
+
* every position the shape offers — text from the instruction's topic, the first offered id
|
|
148
|
+
* of a relation or asset enum, nested objects recursively — and leaves a date, an unbounded
|
|
149
|
+
* list and anything unrecognised empty (the engine lists those on the proposal).
|
|
150
|
+
*/
|
|
151
|
+
export function keylessDraft(request) {
|
|
152
|
+
return draftObject(request.schema, keylessTopic(request.prompt));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The keyless TRANSLATION: every human-readable segment prefixed with the target locale
|
|
156
|
+
* (`[de] …`), placeholder tokens kept verbatim — visible, so a live check can see which locale
|
|
157
|
+
* each fill came from.
|
|
158
|
+
*/
|
|
159
|
+
export function keylessTranslate(request) {
|
|
160
|
+
const locale = /locale code "([^"]+)"/.exec(request.prompt)?.[1] ?? 'xx';
|
|
161
|
+
const json = /Units:\n(\[[\s\S]*\])\s*$/.exec(request.prompt)?.[1] ?? '[]';
|
|
162
|
+
let units;
|
|
163
|
+
try {
|
|
164
|
+
units = JSON.parse(json);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
units = [];
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
units: units.map((unit) => ({
|
|
171
|
+
id: unit.id,
|
|
172
|
+
text: mapEncodedSegments(unit.text, (segment) => (segment.trim().length > 0 ? `[${locale}] ${segment}` : segment)),
|
|
173
|
+
})),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/** The keyless structured responder: a translation batch, else a create's draft. */
|
|
177
|
+
export function keylessRespond(request) {
|
|
178
|
+
return request.schemaName === TRANSLATE_SCHEMA_NAME ? keylessTranslate(request) : keylessDraft(request);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* THE keyless provider (EC-715): the project-reading scripted conversation plus the draft and
|
|
182
|
+
* translation responders, as one `AiProvider`. A host's AI handler falls back to it when no
|
|
183
|
+
* key is set.
|
|
184
|
+
*/
|
|
185
|
+
export function createKeylessProvider(options = {}) {
|
|
186
|
+
return createFakeProvider({
|
|
187
|
+
...(options.id !== undefined ? { id: options.id } : {}),
|
|
188
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
189
|
+
respond: keylessRespond,
|
|
190
|
+
chatModel: createFakeCreateFlowChatModel(options.searchAssets !== undefined ? { searchAssets: options.searchAssets } : {}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=keyless-assistant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyless-assistant.js","sourceRoot":"","sources":["../../../src/lib/ai/keyless-assistant.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * **The keyless assistant** (EC-715): the ONE deterministic, offline provider a project runs\n * when no model key is set — so a fresh scaffold's assistant works on WHATEVER schema it has,\n * not on the collections one starter kit happens to declare.\n *\n * - the conversation: {@link createFakeCreateFlowChatModel}, which reads the project's catalog\n * and the open document from the system prompt (the route has no data access; that prose is\n * the only channel) and derives every answer from the tool results;\n * - the draft: {@link keylessDraft}, the structured responder `applyContentWithShape` calls\n * with the create's target shape — it fills what the shape OFFERS, at every depth (nested\n * objects included), and nothing else, so it can never name a hallucinated id, a read-only\n * field (EC-716) or the slug the engine derives; a slug role the shape offers (marked with\n * THE slug format) is the topic as a URL segment, never prose;\n * - the translation: {@link keylessTranslate}, a visible pseudo-translation (`[de] …`) that\n * keeps every placeholder token, so the engine rebuilds rich text exactly.\n *\n * A scaffold's AI handler and the demo's are one call: `createKeylessProvider()`. Swap in a\n * real key for real output.\n */\nimport { slugifySegment } from '@elytracms/core/cms-core'\nimport type { JsonSchema } from '@elytracms/core/cms-core'\nimport { createFakeCreateFlowChatModel, createFakeProvider } from './fake-provider'\nimport type { FakeProvider } from './fake-provider'\nimport { mapEncodedSegments } from './rich-text-translate'\nimport { SLUG_SEGMENT_DESCRIPTION } from './shape'\nimport { TRANSLATE_SCHEMA_NAME } from './translate-document'\nimport type { GenerateStructuredRequest } from './types'\n\n/** A leading imperative the topic leaves out (\"Create a new …\", \"Please write …\"). */\nconst IMPERATIVE = /^(?:please\\s+)?(?:create|write|draft|add|make|compose)\\s+(?:me\\s+)?(?:(?:a|an|the|some|one|another)\\s+)?(?:new\\s+)?/i\n\n/**\n * What a keyless draft writes about: the instruction's `about …` clause, else the\n * instruction without its leading imperative — read off the authoring prompt\n * (`Instruction:\\n…`, `applyContentWithShape`'s first part).\n */\nexport function keylessTopic(prompt: string): string {\n const instruction = /Instruction:\\n([\\s\\S]*?)(?:\\n\\n|$)/.exec(prompt)?.[1]?.trim() ?? prompt.trim()\n const about = /\\babout\\s+(.+)/i.exec(instruction)?.[1] ?? instruction.replace(IMPERATIVE, '')\n return about.replace(/[\"'.?!\\s]+$/g, '').trim() || 'your new document'\n}\n\nfunction titleCase(text: string): string {\n return text.replace(/\\b\\w/g, (c) => c.toUpperCase())\n}\n\n/** The topic as a URL segment — THE default slug rule (core's `slugifySegment`), never a second one. */\nfunction slugOf(topic: string): string {\n return slugifySegment(topic) || 'draft'\n}\n\n/** An identifier-like name (`layoutId`, `key`): no prose fits it — left empty unless a pattern says how. */\nconst IDENTIFIER = /(?:^(?:id|key)$|[a-z0-9](?:Id|Key)$)/\n\nfunction num(value: unknown): number | undefined {\n return typeof value === 'number' ? value : undefined\n}\n\nfunction propertiesOf(schema: JsonSchema): Record<string, JsonSchema> {\n const properties = schema.properties\n return typeof properties === 'object' && properties !== null && !Array.isArray(properties)\n ? (properties as Record<string, JsonSchema>)\n : {}\n}\n\n/** A string within the schema's length bounds. */\nfunction bounded(text: string, schema: JsonSchema): string {\n const max = num(schema.maxLength)\n const min = num(schema.minLength) ?? 0\n let out = text\n while (out.length < min) out = `${out} More details follow.`\n return max !== undefined ? out.slice(0, max).trimEnd() : out\n}\n\n/**\n * A plain string position: by its role, its format, its pattern, then its name. `undefined` —\n * left empty (and so listed on the proposal).\n */\nfunction draftString(name: string, schema: JsonSchema, topic: string): string | undefined {\n // EC-715: the collection's AUTHORED slug role (the shape marks it) — a URL segment, never prose.\n if (typeof schema.description === 'string' && schema.description.startsWith(SLUG_SEGMENT_DESCRIPTION)) {\n return slugOf(topic)\n }\n const format = typeof schema.format === 'string' ? schema.format : undefined\n if (format === 'date-time' || format === 'date' || format === 'time') return undefined\n if (format === 'email' || /e-?mail/i.test(name)) return 'hello@example.com'\n if (format === 'uri' || format === 'url' || /^(url|href|link|website)$/i.test(name)) return 'https://example.com/'\n if (typeof schema.pattern === 'string') {\n const candidate = slugOf(topic)\n try {\n return new RegExp(schema.pattern).test(candidate) ? candidate : undefined\n } catch {\n return undefined\n }\n }\n if (IDENTIFIER.test(name)) return undefined\n if (/title|name|heading|headline|label/i.test(name)) return bounded(titleCase(topic), schema)\n return bounded(`An overview of ${topic}.`, schema)\n}\n\n/** The value one offered position gets — recursing into objects; `undefined` leaves it empty. */\nfunction draftValue(name: string, schema: JsonSchema, topic: string): unknown {\n if (schema.const !== undefined) return schema.const\n if (Array.isArray(schema.enum) && schema.enum.length > 0) return schema.enum[0]\n if (typeof schema.description === 'string' && schema.description.startsWith('Rich text')) {\n // Authored as MARKDOWN (EC-379); the engine converts it to the stored value.\n return `## ${titleCase(topic)}\\n\\nA draft about **${topic}**, written by the keyless assistant for you to review and edit.`\n }\n const type = Array.isArray(schema.type) ? schema.type[0] : schema.type\n switch (type) {\n case 'object':\n return draftObject(schema, topic)\n case 'array': {\n const items = schema.items\n if (typeof items !== 'object' || items === null || Array.isArray(items)) return undefined\n const item = items as JsonSchema\n // A relation list offers its searched ids as an enum: take the first.\n if (Array.isArray(item.enum) && item.enum.length > 0) return [item.enum[0]]\n if ((num(schema.minItems) ?? 0) > 0) {\n const one = draftValue(name, item, topic)\n return one === undefined ? undefined : [one]\n }\n return undefined\n }\n case 'number':\n case 'integer': {\n const minimum = num(schema.minimum) ?? (num(schema.exclusiveMinimum) !== undefined ? num(schema.exclusiveMinimum)! + 1 : 0)\n return minimum\n }\n case 'boolean':\n return false\n case 'string':\n return draftString(name, schema, topic)\n default:\n return undefined\n }\n}\n\nfunction draftObject(schema: JsonSchema, topic: string): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, sub] of Object.entries(propertiesOf(schema))) {\n const value = draftValue(key, sub, topic)\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\n/**\n * The keyless DRAFT (EC-715): a deterministic document for a create's target shape. It fills\n * every position the shape offers — text from the instruction's topic, the first offered id\n * of a relation or asset enum, nested objects recursively — and leaves a date, an unbounded\n * list and anything unrecognised empty (the engine lists those on the proposal).\n */\nexport function keylessDraft(request: GenerateStructuredRequest): Record<string, unknown> {\n return draftObject(request.schema, keylessTopic(request.prompt))\n}\n\n/**\n * The keyless TRANSLATION: every human-readable segment prefixed with the target locale\n * (`[de] …`), placeholder tokens kept verbatim — visible, so a live check can see which locale\n * each fill came from.\n */\nexport function keylessTranslate(request: GenerateStructuredRequest): unknown {\n const locale = /locale code \"([^\"]+)\"/.exec(request.prompt)?.[1] ?? 'xx'\n const json = /Units:\\n(\\[[\\s\\S]*\\])\\s*$/.exec(request.prompt)?.[1] ?? '[]'\n let units: { id: string; text: string }[]\n try {\n units = JSON.parse(json) as { id: string; text: string }[]\n } catch {\n units = []\n }\n return {\n units: units.map((unit) => ({\n id: unit.id,\n text: mapEncodedSegments(unit.text, (segment) => (segment.trim().length > 0 ? `[${locale}] ${segment}` : segment)),\n })),\n }\n}\n\n/** The keyless structured responder: a translation batch, else a create's draft. */\nexport function keylessRespond(request: GenerateStructuredRequest): unknown {\n return request.schemaName === TRANSLATE_SCHEMA_NAME ? keylessTranslate(request) : keylessDraft(request)\n}\n\nexport interface KeylessProviderOptions {\n /** Provider id recorded in provenance (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Whether a create lists the media library first (default: when the collection has an asset field). */\n searchAssets?: boolean\n}\n\n/**\n * THE keyless provider (EC-715): the project-reading scripted conversation plus the draft and\n * translation responders, as one `AiProvider`. A host's AI handler falls back to it when no\n * key is set.\n */\nexport function createKeylessProvider(options: KeylessProviderOptions = {}): FakeProvider {\n return createFakeProvider({\n ...(options.id !== undefined ? { id: options.id } : {}),\n ...(options.model !== undefined ? { model: options.model } : {}),\n respond: keylessRespond,\n chatModel: createFakeCreateFlowChatModel(\n options.searchAssets !== undefined ? { searchAssets: options.searchAssets } : {},\n ),\n })\n}\n"]}
|
package/dist/lib/ai/shape.d.ts
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
-
import type { CollectionDef, JsonSchema } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { WriterLeash } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The collection's SLUG ROLE when a writer never authors it (EC-715): a text `slugField`
|
|
5
|
+
* (EC-590) that has a source — its `slug.source`, else the title role — and is not locked.
|
|
6
|
+
* The create path derives its value with core's `autoDeriveSlug` (THE derivation the slug
|
|
7
|
+
* control and translate read) instead of asking the model, which used to fill it with prose
|
|
8
|
+
* wherever no pattern stopped it. `slug.source: false` (EC-691) opts out: the model writes it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function derivedSlugField(collection: CollectionDef): FieldDef | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* How the writer form of an AUTHORED slug role begins (EC-715): the slug role a writer does
|
|
13
|
+
* write — `slug.source: false`, or no source to derive from. A reader of the shape (the keyless
|
|
14
|
+
* draft, like a real model) recognises the role by it; {@link slugSegmentDescription} says the rest.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SLUG_SEGMENT_DESCRIPTION = "URL segment";
|
|
17
|
+
/**
|
|
18
|
+
* The format an authored slug role takes, stated by THE slug rule itself (core's `applySlugify`:
|
|
19
|
+
* the field's own `slug.slugify`, else the default `slugifySegment`) — an example it computes,
|
|
20
|
+
* never a second regex restating it, and never prose: a value is a slug when the rule leaves it
|
|
21
|
+
* as it is.
|
|
22
|
+
*/
|
|
23
|
+
export declare function slugSegmentDescription(field: FieldDef): string;
|
|
3
24
|
/** A top-level relation field's resolved candidate set (the enum the model chose from). */
|
|
4
25
|
export interface TargetRelationField {
|
|
5
26
|
fieldName: string;
|
|
@@ -20,10 +41,12 @@ export interface TargetAssetField {
|
|
|
20
41
|
export interface TargetShape {
|
|
21
42
|
/** The JSON Schema the structured output must satisfy. */
|
|
22
43
|
schema: JsonSchema;
|
|
23
|
-
/** Field names removed
|
|
44
|
+
/** Field names removed as writer-locked — `ai.exclude` or `form.readOnly` (EC-716). */
|
|
24
45
|
excluded: string[];
|
|
25
46
|
/** Field names omitted because a writer never writes them — a composition (4c). */
|
|
26
47
|
compositions: string[];
|
|
48
|
+
/** Field names omitted because the engine derives them — the slug role (EC-715). */
|
|
49
|
+
derived: string[];
|
|
27
50
|
/** Field names whose values route under `localized[locale]`. */
|
|
28
51
|
localizedFields: string[];
|
|
29
52
|
/** Top-level relation fields with their candidate enums. */
|
package/dist/lib/ai/shape.js
CHANGED
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* cms-core) rather than writing a new one, then applies the leashes the AI engine
|
|
6
6
|
* adds on top:
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* - writer-locked fields are removed entirely (EC-716, owner ruling "Yes, honour
|
|
9
|
+
* readOnly"): `ai.exclude` and `form.readOnly`, at EVERY depth — core's one predicate
|
|
10
|
+
* (`isWriterLocked`), so a read-only sub-field of an object is as absent as a
|
|
11
|
+
* read-only top-level field. The model cannot name a position the editor's own form
|
|
12
|
+
* cannot change.
|
|
10
13
|
* - relation ids become an ENUM over the searched candidates of the relation's
|
|
11
14
|
* target (or the position is omitted when none were searched) — so a hallucinated
|
|
12
15
|
* relation id is not representable as valid structured output.
|
|
@@ -21,6 +24,10 @@
|
|
|
21
24
|
* value at proposal-build time (`writer-form.ts`). The field's `allow` config leashes
|
|
22
25
|
* component EMBEDS only — markdown cannot express an embed, so the leash is
|
|
23
26
|
* satisfied by construction (and still enforced downstream by `validateRichTextEmbeds`).
|
|
27
|
+
* - the slug role is DERIVED, never authored (EC-715, {@link derivedSlugField}): it is
|
|
28
|
+
* omitted, and the create path fills it from the title with core's `autoDeriveSlug`; a
|
|
29
|
+
* slug role a writer DOES author (`slug.source: false`, or nothing to derive from) is
|
|
30
|
+
* offered in THE slug format ({@link slugSegmentDescription}), never as a free string;
|
|
24
31
|
* - a composition (`blocks`) has NO writer form (ratified 4c, EC-593): it is omitted,
|
|
25
32
|
* and the schema's own description says so — the omission is visible, never a field
|
|
26
33
|
* the model simply never hears of.
|
|
@@ -53,9 +60,43 @@
|
|
|
53
60
|
* id, a relation id, a markdown string — so an asset, relation or rich-text SUBFIELD of
|
|
54
61
|
* an `object` is told the same form as a top-level one.
|
|
55
62
|
*/
|
|
56
|
-
import { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core';
|
|
63
|
+
import { applySlugify, collectionJsonSchema, isWriterLocked, resolveLabel, slugFieldOf, titleFieldOf, } from '@elytracms/core/cms-core';
|
|
57
64
|
import { constraintSentence, toModelFacingSchema, writerFieldSchema } from './constraints.js';
|
|
58
65
|
import { NO_SEARCHED_IDS } from './leash.js';
|
|
66
|
+
/**
|
|
67
|
+
* The collection's SLUG ROLE when a writer never authors it (EC-715): a text `slugField`
|
|
68
|
+
* (EC-590) that has a source — its `slug.source`, else the title role — and is not locked.
|
|
69
|
+
* The create path derives its value with core's `autoDeriveSlug` (THE derivation the slug
|
|
70
|
+
* control and translate read) instead of asking the model, which used to fill it with prose
|
|
71
|
+
* wherever no pattern stopped it. `slug.source: false` (EC-691) opts out: the model writes it.
|
|
72
|
+
*/
|
|
73
|
+
export function derivedSlugField(collection) {
|
|
74
|
+
const field = slugFieldOf(collection);
|
|
75
|
+
if (!field || field.type !== 'text' || isWriterLocked(field))
|
|
76
|
+
return undefined;
|
|
77
|
+
const source = field.slug?.source;
|
|
78
|
+
if (source === false)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (source !== undefined)
|
|
81
|
+
return field;
|
|
82
|
+
return typeof collection.titleField === 'function' || titleFieldOf(collection) !== undefined ? field : undefined;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* How the writer form of an AUTHORED slug role begins (EC-715): the slug role a writer does
|
|
86
|
+
* write — `slug.source: false`, or no source to derive from. A reader of the shape (the keyless
|
|
87
|
+
* draft, like a real model) recognises the role by it; {@link slugSegmentDescription} says the rest.
|
|
88
|
+
*/
|
|
89
|
+
export const SLUG_SEGMENT_DESCRIPTION = 'URL segment';
|
|
90
|
+
/**
|
|
91
|
+
* The format an authored slug role takes, stated by THE slug rule itself (core's `applySlugify`:
|
|
92
|
+
* the field's own `slug.slugify`, else the default `slugifySegment`) — an example it computes,
|
|
93
|
+
* never a second regex restating it, and never prose: a value is a slug when the rule leaves it
|
|
94
|
+
* as it is.
|
|
95
|
+
*/
|
|
96
|
+
export function slugSegmentDescription(field) {
|
|
97
|
+
const example = applySlugify(field.slug, 'Our team');
|
|
98
|
+
return `${SLUG_SEGMENT_DESCRIPTION}, written exactly as the slug rule writes one (e.g. "${example}" for "Our team") — never a sentence.`;
|
|
99
|
+
}
|
|
59
100
|
/**
|
|
60
101
|
* A field's model-facing description: resolved label + optional `ai.hint` + the
|
|
61
102
|
* constraint clause (EC-473). The constraints are stated in PROSE because that is
|
|
@@ -83,17 +124,26 @@ export function buildTargetShape(input) {
|
|
|
83
124
|
};
|
|
84
125
|
const excluded = [];
|
|
85
126
|
const compositions = [];
|
|
127
|
+
const derived = [];
|
|
128
|
+
const slugField = derivedSlugField(input.collection);
|
|
129
|
+
// EC-715: the slug role a writer DOES author (not derived) — offered in THE slug format.
|
|
130
|
+
const authoredSlug = slugField === undefined ? slugFieldOf(input.collection) : undefined;
|
|
86
131
|
const localizedFields = [];
|
|
87
132
|
const relationFields = [];
|
|
88
133
|
const assetFields = [];
|
|
89
134
|
for (const field of input.collection.fields) {
|
|
90
135
|
if (field.localized)
|
|
91
136
|
localizedFields.push(field.name);
|
|
92
|
-
if (field
|
|
137
|
+
if (isWriterLocked(field)) {
|
|
93
138
|
excluded.push(field.name);
|
|
94
139
|
delete properties[field.name];
|
|
95
140
|
continue;
|
|
96
141
|
}
|
|
142
|
+
if (field === slugField) {
|
|
143
|
+
derived.push(field.name);
|
|
144
|
+
delete properties[field.name];
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
97
147
|
// EC-556: the writer form at every depth (rich text → markdown, EC-379 / D2;
|
|
98
148
|
// nested asset / relation → the bare forms the write-time lens wraps); EC-593:
|
|
99
149
|
// every id an enum over what was searched for that position.
|
|
@@ -117,7 +167,8 @@ export function buildTargetShape(input) {
|
|
|
117
167
|
}
|
|
118
168
|
// EC-473: a `pattern` next to a `format` is 200+ characters that teach the
|
|
119
169
|
// model nothing — the format (and the prose constraints below) say it better.
|
|
120
|
-
|
|
170
|
+
// EC-715: an authored slug role carries its format first, as rich text carries its own.
|
|
171
|
+
const stripped = toModelFacingSchema(field === authoredSlug && field.type === 'text' ? { ...written, description: slugSegmentDescription(field) } : written);
|
|
121
172
|
const desc = describe(field, input.locale);
|
|
122
173
|
// Preserve an existing description (e.g. relation enum / rich-text note), but
|
|
123
174
|
// append the constraint clause to it — the enum leash and the value rules are
|
|
@@ -136,7 +187,7 @@ export function buildTargetShape(input) {
|
|
|
136
187
|
properties[field.name] = next;
|
|
137
188
|
}
|
|
138
189
|
const requiredIn = Array.isArray(base.required) ? base.required : [];
|
|
139
|
-
const required = requiredIn.filter((n) =>
|
|
190
|
+
const required = requiredIn.filter((n) => properties[n] !== undefined);
|
|
140
191
|
const schema = {
|
|
141
192
|
type: 'object',
|
|
142
193
|
properties,
|
|
@@ -149,6 +200,6 @@ export function buildTargetShape(input) {
|
|
|
149
200
|
}
|
|
150
201
|
: {}),
|
|
151
202
|
};
|
|
152
|
-
return { schema, excluded, compositions, localizedFields, relationFields, assetFields };
|
|
203
|
+
return { schema, excluded, compositions, derived, localizedFields, relationFields, assetFields };
|
|
153
204
|
}
|
|
154
205
|
//# sourceMappingURL=shape.js.map
|
package/dist/lib/ai/shape.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../src/lib/ai/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target-shape derivation for `applyContentWithShape` (EC-331).\n *\n * Reuses the existing collection→JSON-Schema derivation (`collectionJsonSchema`,\n * cms-core) rather than writing a new one, then applies the leashes the AI engine\n * adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation ids become an ENUM over the searched candidates of the relation's\n * target (or the position is omitted when none were searched) — so a hallucinated\n * relation id is not representable as valid structured output.\n * - asset ids become an ENUM over the searched library assets that fit the\n * position's `accept` (or the position is omitted when none fit) — EC-381, the SAME\n * leash as relations: a proposed asset id must be a real, `listAssets`-returned\n * library asset (empty beats wrong; the assistant never generates or uploads).\n * EC-593: both leashes hold at EVERY depth — a relation in an object, an asset in a\n * repeater item — not only on a top-level field (`writerFieldSchema(field, leash)`).\n * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the\n * allowed subset, and the engine converts md → the `{ version: 2, content }` storage\n * value at proposal-build time (`writer-form.ts`). The field's `allow` config leashes\n * component EMBEDS only — markdown cannot express an embed, so the leash is\n * satisfied by construction (and still enforced downstream by `validateRichTextEmbeds`).\n * - a composition (`blocks`) has NO writer form (ratified 4c, EC-593): it is omitted,\n * and the schema's own description says so — the omission is visible, never a field\n * the model simply never hears of.\n *\n * Localized fields stay flat in the shape (the model authors one locale at a time);\n * the engine routes their values under `localized[locale]`.\n *\n * EC-473 — the shape now consumes the ONE constraint projection, so a rule declared\n * in config actually reaches the model:\n *\n * - `collectionJsonSchema` merges declarative rules with the fragment derived from\n * a schema-form `validation.validate`, so `{ min: 2 }` and `z.string().min(2)`\n * arrive identically (and a declared DATE RANGE arrives at all — it previously\n * reached no consumer whatsoever);\n * - a rich-text field no longer OVERWRITES its constraints: the markdown schema is\n * merged over them instead of replacing them (measured drift, EC-474 §2);\n * - a `pattern` is dropped wherever a `format` explains the same thing (a\n * 200-character email regex teaches a model nothing and costs real tokens);\n * - a field whose projection is knowingly partial (`schemaComplete: false`) SAYS SO\n * in its description rather than presenting the partial rule as the whole rule.\n *\n * EC-505 — one further narrowing of what the model is TOLD (never of what the engine\n * accepts): a `date` property is `type: 'string'` + `format: 'date-time'` rather than\n * the `['string','number']` union the general projection carries, because the model is\n * asked to author an ISO instant (EC-472) and some providers reject a union `type`.\n *\n * EC-556 — the projection is VALIDATOR-facing (the value at rest: asset/relation\n * envelopes, the v2 rich-text object). Every property here is its WRITER form\n * (`writerFieldSchema`), derived from that projection at every depth — a bare asset\n * id, a relation id, a markdown string — so an asset, relation or rich-text SUBFIELD of\n * an `object` is told the same form as a top-level one.\n */\nimport { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport { constraintSentence, toModelFacingSchema, writerFieldSchema } from './constraints'\nimport { NO_SEARCHED_IDS } from './leash'\nimport type { WriterLeash } from './types'\n\n/** A top-level relation field's resolved candidate set (the enum the model chose from). */\nexport interface TargetRelationField {\n fieldName: string\n /** The relation's target collection id. */\n target: string\n multiple: boolean\n /** The allowed candidate ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** A top-level asset field's resolved candidate set (EC-381). */\nexport interface TargetAssetField {\n fieldName: string\n multiple: boolean\n /** The allowed library asset ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** The derived target shape + the metadata the engine needs to post-process the output. */\nexport interface TargetShape {\n /** The JSON Schema the structured output must satisfy. */\n schema: JsonSchema\n /** Field names removed by `ai.exclude` (untouchable). */\n excluded: string[]\n /** Field names omitted because a writer never writes them — a composition (4c). */\n compositions: string[]\n /** Field names whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Top-level relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n /** Top-level asset fields with their candidate enums (EC-381). */\n assetFields: TargetAssetField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** What the model searched — the id leash at every position (absent → nothing searched). */\n leash?: WriterLeash\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/**\n * A field's model-facing description: resolved label + optional `ai.hint` + the\n * constraint clause (EC-473). The constraints are stated in PROSE because that is\n * what a model acts on — the same facts also ride as JSON-Schema keywords for the\n * structured-output validator, which is a different reader.\n */\nfunction describe(field: FieldDef, locale?: string): string | undefined {\n const label = resolveLabel(field.form?.label, locale) ?? field.name\n const hint = field.ai?.hint\n const constraints = constraintSentence(field)\n const parts = [label, hint, constraints].filter(\n (p): p is string => typeof p === 'string' && p.length > 0,\n )\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + what the model searched.\n * Deterministic; never throws. Reuses `collectionJsonSchema` as the base (its\n * `required` list), then gives every property its writer form (EC-556) — id enums at\n * every depth (EC-593) — and strips excluded fields and compositions.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\n const leash = input.leash ?? NO_SEARCHED_IDS\n const base = collectionJsonSchema(input.collection)\n const properties: Record<string, JsonSchema> = {\n ...((base.properties as Record<string, JsonSchema>) ?? {}),\n }\n const excluded: string[] = []\n const compositions: string[] = []\n const localizedFields: string[] = []\n const relationFields: TargetRelationField[] = []\n const assetFields: TargetAssetField[] = []\n\n for (const field of input.collection.fields) {\n if (field.localized) localizedFields.push(field.name)\n\n if (field.ai?.exclude) {\n excluded.push(field.name)\n delete properties[field.name]\n continue\n }\n\n // EC-556: the writer form at every depth (rich text → markdown, EC-379 / D2;\n // nested asset / relation → the bare forms the write-time lens wraps); EC-593:\n // every id an enum over what was searched for that position.\n const written = writerFieldSchema(field, leash)\n if (written === undefined) {\n // No writer form: a composition (4c), or a relation / asset with nothing\n // searched for it — the model cannot emit a value, so a hallucinated id is\n // structurally impossible (empty beats wrong).\n if (field.type === 'blocks') compositions.push(field.name)\n delete properties[field.name]\n continue\n }\n if (field.type === 'relation') {\n const candidateIds = leash.relationCandidates(field.target).map((c) => c.id)\n relationFields.push({ fieldName: field.name, target: field.target, multiple: field.multiple, candidateIds })\n } else if (field.type === 'asset') {\n const candidateIds = leash.assetCandidates(field).map((c) => c.id)\n assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds })\n }\n\n // EC-473: a `pattern` next to a `format` is 200+ characters that teach the\n // model nothing — the format (and the prose constraints below) say it better.\n const stripped = toModelFacingSchema(written)\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note), but\n // append the constraint clause to it — the enum leash and the value rules are\n // different facts and the model needs both.\n const next: JsonSchema = stripped === written ? { ...written } : { ...stripped }\n if (desc !== undefined) {\n const existing = typeof next.description === 'string' ? next.description : undefined\n const constraints = constraintSentence(field)\n next.description =\n existing === undefined\n ? desc\n : constraints !== undefined\n ? `${existing} — ${constraints}`\n : existing\n }\n properties[field.name] = next\n }\n\n const requiredIn = Array.isArray(base.required) ? (base.required as string[]) : []\n const required = requiredIn.filter((n) => !excluded.includes(n) && properties[n] !== undefined)\n\n const schema: JsonSchema = {\n type: 'object',\n properties,\n additionalProperties: false,\n ...(required.length > 0 ? { required } : {}),\n // 4c (EC-593): the omission is said, not silent.\n ...(compositions.length > 0\n ? {\n description: `Not part of this object: ${compositions.join(', ')} — page-builder composition${compositions.length > 1 ? 's' : ''} whose components are added, moved and removed on the canvas, never by the assistant.`,\n }\n : {}),\n }\n return { schema, excluded, compositions, localizedFields, relationFields, assetFields }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shape.js","sourceRoot":"","sources":["../../../src/lib/ai/shape.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Target-shape derivation for `applyContentWithShape` (EC-331).\n *\n * Reuses the existing collection→JSON-Schema derivation (`collectionJsonSchema`,\n * cms-core) rather than writing a new one, then applies the leashes the AI engine\n * adds on top:\n *\n * - writer-locked fields are removed entirely (EC-716, owner ruling \"Yes, honour\n * readOnly\"): `ai.exclude` and `form.readOnly`, at EVERY depth — core's one predicate\n * (`isWriterLocked`), so a read-only sub-field of an object is as absent as a\n * read-only top-level field. The model cannot name a position the editor's own form\n * cannot change.\n * - relation ids become an ENUM over the searched candidates of the relation's\n * target (or the position is omitted when none were searched) — so a hallucinated\n * relation id is not representable as valid structured output.\n * - asset ids become an ENUM over the searched library assets that fit the\n * position's `accept` (or the position is omitted when none fit) — EC-381, the SAME\n * leash as relations: a proposed asset id must be a real, `listAssets`-returned\n * library asset (empty beats wrong; the assistant never generates or uploads).\n * EC-593: both leashes hold at EVERY depth — a relation in an object, an asset in a\n * repeater item — not only on a top-level field (`writerFieldSchema(field, leash)`).\n * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the\n * allowed subset, and the engine converts md → the `{ version: 2, content }` storage\n * value at proposal-build time (`writer-form.ts`). The field's `allow` config leashes\n * component EMBEDS only — markdown cannot express an embed, so the leash is\n * satisfied by construction (and still enforced downstream by `validateRichTextEmbeds`).\n * - the slug role is DERIVED, never authored (EC-715, {@link derivedSlugField}): it is\n * omitted, and the create path fills it from the title with core's `autoDeriveSlug`; a\n * slug role a writer DOES author (`slug.source: false`, or nothing to derive from) is\n * offered in THE slug format ({@link slugSegmentDescription}), never as a free string;\n * - a composition (`blocks`) has NO writer form (ratified 4c, EC-593): it is omitted,\n * and the schema's own description says so — the omission is visible, never a field\n * the model simply never hears of.\n *\n * Localized fields stay flat in the shape (the model authors one locale at a time);\n * the engine routes their values under `localized[locale]`.\n *\n * EC-473 — the shape now consumes the ONE constraint projection, so a rule declared\n * in config actually reaches the model:\n *\n * - `collectionJsonSchema` merges declarative rules with the fragment derived from\n * a schema-form `validation.validate`, so `{ min: 2 }` and `z.string().min(2)`\n * arrive identically (and a declared DATE RANGE arrives at all — it previously\n * reached no consumer whatsoever);\n * - a rich-text field no longer OVERWRITES its constraints: the markdown schema is\n * merged over them instead of replacing them (measured drift, EC-474 §2);\n * - a `pattern` is dropped wherever a `format` explains the same thing (a\n * 200-character email regex teaches a model nothing and costs real tokens);\n * - a field whose projection is knowingly partial (`schemaComplete: false`) SAYS SO\n * in its description rather than presenting the partial rule as the whole rule.\n *\n * EC-505 — one further narrowing of what the model is TOLD (never of what the engine\n * accepts): a `date` property is `type: 'string'` + `format: 'date-time'` rather than\n * the `['string','number']` union the general projection carries, because the model is\n * asked to author an ISO instant (EC-472) and some providers reject a union `type`.\n *\n * EC-556 — the projection is VALIDATOR-facing (the value at rest: asset/relation\n * envelopes, the v2 rich-text object). Every property here is its WRITER form\n * (`writerFieldSchema`), derived from that projection at every depth — a bare asset\n * id, a relation id, a markdown string — so an asset, relation or rich-text SUBFIELD of\n * an `object` is told the same form as a top-level one.\n */\nimport {\n applySlugify,\n collectionJsonSchema,\n isWriterLocked,\n resolveLabel,\n slugFieldOf,\n titleFieldOf,\n} from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport { constraintSentence, toModelFacingSchema, writerFieldSchema } from './constraints'\nimport { NO_SEARCHED_IDS } from './leash'\nimport type { WriterLeash } from './types'\n\n/**\n * The collection's SLUG ROLE when a writer never authors it (EC-715): a text `slugField`\n * (EC-590) that has a source — its `slug.source`, else the title role — and is not locked.\n * The create path derives its value with core's `autoDeriveSlug` (THE derivation the slug\n * control and translate read) instead of asking the model, which used to fill it with prose\n * wherever no pattern stopped it. `slug.source: false` (EC-691) opts out: the model writes it.\n */\nexport function derivedSlugField(collection: CollectionDef): FieldDef | undefined {\n const field = slugFieldOf(collection)\n if (!field || field.type !== 'text' || isWriterLocked(field)) return undefined\n const source = field.slug?.source\n if (source === false) return undefined\n if (source !== undefined) return field\n return typeof collection.titleField === 'function' || titleFieldOf(collection) !== undefined ? field : undefined\n}\n\n/**\n * How the writer form of an AUTHORED slug role begins (EC-715): the slug role a writer does\n * write — `slug.source: false`, or no source to derive from. A reader of the shape (the keyless\n * draft, like a real model) recognises the role by it; {@link slugSegmentDescription} says the rest.\n */\nexport const SLUG_SEGMENT_DESCRIPTION = 'URL segment'\n\n/**\n * The format an authored slug role takes, stated by THE slug rule itself (core's `applySlugify`:\n * the field's own `slug.slugify`, else the default `slugifySegment`) — an example it computes,\n * never a second regex restating it, and never prose: a value is a slug when the rule leaves it\n * as it is.\n */\nexport function slugSegmentDescription(field: FieldDef): string {\n const example = applySlugify(field.slug, 'Our team')\n return `${SLUG_SEGMENT_DESCRIPTION}, written exactly as the slug rule writes one (e.g. \"${example}\" for \"Our team\") — never a sentence.`\n}\n\n/** A top-level relation field's resolved candidate set (the enum the model chose from). */\nexport interface TargetRelationField {\n fieldName: string\n /** The relation's target collection id. */\n target: string\n multiple: boolean\n /** The allowed candidate ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** A top-level asset field's resolved candidate set (EC-381). */\nexport interface TargetAssetField {\n fieldName: string\n multiple: boolean\n /** The allowed library asset ids (the enum the model chose from). */\n candidateIds: string[]\n}\n\n/** The derived target shape + the metadata the engine needs to post-process the output. */\nexport interface TargetShape {\n /** The JSON Schema the structured output must satisfy. */\n schema: JsonSchema\n /** Field names removed as writer-locked — `ai.exclude` or `form.readOnly` (EC-716). */\n excluded: string[]\n /** Field names omitted because a writer never writes them — a composition (4c). */\n compositions: string[]\n /** Field names omitted because the engine derives them — the slug role (EC-715). */\n derived: string[]\n /** Field names whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Top-level relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n /** Top-level asset fields with their candidate enums (EC-381). */\n assetFields: TargetAssetField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** What the model searched — the id leash at every position (absent → nothing searched). */\n leash?: WriterLeash\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/**\n * A field's model-facing description: resolved label + optional `ai.hint` + the\n * constraint clause (EC-473). The constraints are stated in PROSE because that is\n * what a model acts on — the same facts also ride as JSON-Schema keywords for the\n * structured-output validator, which is a different reader.\n */\nfunction describe(field: FieldDef, locale?: string): string | undefined {\n const label = resolveLabel(field.form?.label, locale) ?? field.name\n const hint = field.ai?.hint\n const constraints = constraintSentence(field)\n const parts = [label, hint, constraints].filter(\n (p): p is string => typeof p === 'string' && p.length > 0,\n )\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + what the model searched.\n * Deterministic; never throws. Reuses `collectionJsonSchema` as the base (its\n * `required` list), then gives every property its writer form (EC-556) — id enums at\n * every depth (EC-593) — and strips excluded fields and compositions.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\n const leash = input.leash ?? NO_SEARCHED_IDS\n const base = collectionJsonSchema(input.collection)\n const properties: Record<string, JsonSchema> = {\n ...((base.properties as Record<string, JsonSchema>) ?? {}),\n }\n const excluded: string[] = []\n const compositions: string[] = []\n const derived: string[] = []\n const slugField = derivedSlugField(input.collection)\n // EC-715: the slug role a writer DOES author (not derived) — offered in THE slug format.\n const authoredSlug = slugField === undefined ? slugFieldOf(input.collection) : undefined\n const localizedFields: string[] = []\n const relationFields: TargetRelationField[] = []\n const assetFields: TargetAssetField[] = []\n\n for (const field of input.collection.fields) {\n if (field.localized) localizedFields.push(field.name)\n\n if (isWriterLocked(field)) {\n excluded.push(field.name)\n delete properties[field.name]\n continue\n }\n if (field === slugField) {\n derived.push(field.name)\n delete properties[field.name]\n continue\n }\n\n // EC-556: the writer form at every depth (rich text → markdown, EC-379 / D2;\n // nested asset / relation → the bare forms the write-time lens wraps); EC-593:\n // every id an enum over what was searched for that position.\n const written = writerFieldSchema(field, leash)\n if (written === undefined) {\n // No writer form: a composition (4c), or a relation / asset with nothing\n // searched for it — the model cannot emit a value, so a hallucinated id is\n // structurally impossible (empty beats wrong).\n if (field.type === 'blocks') compositions.push(field.name)\n delete properties[field.name]\n continue\n }\n if (field.type === 'relation') {\n const candidateIds = leash.relationCandidates(field.target).map((c) => c.id)\n relationFields.push({ fieldName: field.name, target: field.target, multiple: field.multiple, candidateIds })\n } else if (field.type === 'asset') {\n const candidateIds = leash.assetCandidates(field).map((c) => c.id)\n assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds })\n }\n\n // EC-473: a `pattern` next to a `format` is 200+ characters that teach the\n // model nothing — the format (and the prose constraints below) say it better.\n // EC-715: an authored slug role carries its format first, as rich text carries its own.\n const stripped = toModelFacingSchema(\n field === authoredSlug && field.type === 'text' ? { ...written, description: slugSegmentDescription(field) } : written,\n )\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note), but\n // append the constraint clause to it — the enum leash and the value rules are\n // different facts and the model needs both.\n const next: JsonSchema = stripped === written ? { ...written } : { ...stripped }\n if (desc !== undefined) {\n const existing = typeof next.description === 'string' ? next.description : undefined\n const constraints = constraintSentence(field)\n next.description =\n existing === undefined\n ? desc\n : constraints !== undefined\n ? `${existing} — ${constraints}`\n : existing\n }\n properties[field.name] = next\n }\n\n const requiredIn = Array.isArray(base.required) ? (base.required as string[]) : []\n const required = requiredIn.filter((n) => properties[n] !== undefined)\n\n const schema: JsonSchema = {\n type: 'object',\n properties,\n additionalProperties: false,\n ...(required.length > 0 ? { required } : {}),\n // 4c (EC-593): the omission is said, not silent.\n ...(compositions.length > 0\n ? {\n description: `Not part of this object: ${compositions.join(', ')} — page-builder composition${compositions.length > 1 ? 's' : ''} whose components are added, moved and removed on the canvas, never by the assistant.`,\n }\n : {}),\n }\n return { schema, excluded, compositions, derived, localizedFields, relationFields, assetFields }\n}\n"]}
|
package/dist/lib/ai/tools.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ import type { CmsDocument, CollectionDef, CollectionRegistry, LocaleConfig, Vali
|
|
|
36
36
|
import type { SearchedAsset, SearchedDocument } from './leash.js';
|
|
37
37
|
import type { TranslateSkip } from './translate-document.js';
|
|
38
38
|
import type { ChatProposal } from './chat-state.js';
|
|
39
|
-
import type { AiProvider, ResolvedKnowledge, WriterLeash } from './types.js';
|
|
39
|
+
import type { AiProvider, ResolvedKnowledge, UnfilledPosition, WriterLeash } from './types.js';
|
|
40
40
|
/** A document the model may reference as a relation target (from a `searchDocuments` read). */
|
|
41
41
|
export type DocumentSearchResult = SearchedDocument;
|
|
42
42
|
/**
|
|
@@ -267,6 +267,11 @@ export interface ChatToolExecutor {
|
|
|
267
267
|
}
|
|
268
268
|
/** A translate skip as one model-facing phrase: `menu[1].label (de): the provider returned no text`. */
|
|
269
269
|
export declare function describeTranslateSkip(skip: TranslateSkip): string;
|
|
270
|
+
/**
|
|
271
|
+
* An unfilled position as one model-facing phrase (EC-715): `cover (no searched library
|
|
272
|
+
* asset fits it)`, `card.image (an id outside the search results was dropped)`.
|
|
273
|
+
*/
|
|
274
|
+
export declare function describeUnfilled(position: UnfilledPosition): string;
|
|
270
275
|
/**
|
|
271
276
|
* The `getDocument` output (EC-379 → EC-593): the document in the form the model reads
|
|
272
277
|
* — the ONE converter's read side (`toWriterDocument`), so rich text is a markdown
|
|
@@ -281,9 +286,14 @@ export declare function describeTranslateSkip(skip: TranslateSkip): string;
|
|
|
281
286
|
* document, the props `updateFields` may change and the rules each value must satisfy,
|
|
282
287
|
* read off the same declarations the validator judges (`lookupManifest`). Applies to
|
|
283
288
|
* the TOOL OUTPUT only; the proposal/apply paths always read the raw stored document.
|
|
289
|
+
*
|
|
290
|
+
* `readOnly` (EC-716) lists every writer-locked position the collection declares
|
|
291
|
+
* (`views`, `meta.locked`): the values stay in the output — the read context keeps them —
|
|
292
|
+
* and the list says they are read, never written.
|
|
284
293
|
*/
|
|
285
294
|
export declare function modelFacingDocument(document: CmsDocument, collection: CollectionDef, validateOptions?: ValidateDocumentOptions): CmsDocument & {
|
|
286
295
|
components?: Record<string, string[]>;
|
|
296
|
+
readOnly?: string[];
|
|
287
297
|
};
|
|
288
298
|
/**
|
|
289
299
|
* Build the client executor map. Every entry validates its input against
|
package/dist/lib/ai/tools.js
CHANGED
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
* with the hand-rolled orchestrator (EC-334 slice 2).
|
|
33
33
|
*/
|
|
34
34
|
import { z } from 'zod';
|
|
35
|
-
import { resolveDocumentTitle } from '@elytracms/core/cms-core';
|
|
35
|
+
import { isSingleton, resolveDocumentTitle } from '@elytracms/core/cms-core';
|
|
36
36
|
import { applyContentWithShape } from './apply-content.js';
|
|
37
|
-
import { describeComponentProps } from './chat-context.js';
|
|
37
|
+
import { describeComponentProps, lockedPositionsOf } from './chat-context.js';
|
|
38
38
|
import { assetCandidatesOf, relationCandidatesOf } from './leash.js';
|
|
39
39
|
import { translateDocument } from './translate-document.js';
|
|
40
40
|
import { buildUpdateProposal } from './update-fields.js';
|
|
@@ -82,7 +82,8 @@ export const chatToolDefinitions = {
|
|
|
82
82
|
'id. Omit both to read the document currently open in the editor. Rich-text values — ' +
|
|
83
83
|
'at any depth — are returned as markdown strings where faithfully expressible. ' +
|
|
84
84
|
'`components` lists, for every component on the document, the props you may change ' +
|
|
85
|
-
'with updateFields and the rules each value must satisfy.'
|
|
85
|
+
'with updateFields and the rules each value must satisfy. `readOnly` lists the ' +
|
|
86
|
+
'positions you can read but never change.',
|
|
86
87
|
inputSchema: z.object({
|
|
87
88
|
collection: z
|
|
88
89
|
.string()
|
|
@@ -233,6 +234,19 @@ export function describeTranslateSkip(skip) {
|
|
|
233
234
|
: 'the translation did not keep its formatting, links or embeds, so the original was kept';
|
|
234
235
|
return `${where}${component} (${skip.locale}): ${why}`;
|
|
235
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* An unfilled position as one model-facing phrase (EC-715): `cover (no searched library
|
|
239
|
+
* asset fits it)`, `card.image (an id outside the search results was dropped)`.
|
|
240
|
+
*/
|
|
241
|
+
export function describeUnfilled(position) {
|
|
242
|
+
const where = position.at.reduce((out, key) => (typeof key === 'number' ? `${out}[${key}]` : `${out}.${key}`), position.field);
|
|
243
|
+
const why = position.reason === 'dropped'
|
|
244
|
+
? 'an id outside the search results was dropped'
|
|
245
|
+
: position.reason === 'no-candidates'
|
|
246
|
+
? 'nothing searched fits it'
|
|
247
|
+
: 'left empty';
|
|
248
|
+
return `${where} (${why})`;
|
|
249
|
+
}
|
|
236
250
|
function issueSummary(issues) {
|
|
237
251
|
return issues
|
|
238
252
|
.slice(0, 5)
|
|
@@ -253,10 +267,18 @@ function issueSummary(issues) {
|
|
|
253
267
|
* document, the props `updateFields` may change and the rules each value must satisfy,
|
|
254
268
|
* read off the same declarations the validator judges (`lookupManifest`). Applies to
|
|
255
269
|
* the TOOL OUTPUT only; the proposal/apply paths always read the raw stored document.
|
|
270
|
+
*
|
|
271
|
+
* `readOnly` (EC-716) lists every writer-locked position the collection declares
|
|
272
|
+
* (`views`, `meta.locked`): the values stay in the output — the read context keeps them —
|
|
273
|
+
* and the list says they are read, never written.
|
|
256
274
|
*/
|
|
257
275
|
export function modelFacingDocument(document, collection, validateOptions = {}) {
|
|
258
276
|
const lookupManifest = validateOptions.lookupManifest;
|
|
259
|
-
const
|
|
277
|
+
const locked = lockedPositionsOf(collection.fields);
|
|
278
|
+
const read = {
|
|
279
|
+
...toWriterDocument(collection, document, lookupManifest ? { lookupManifest } : {}),
|
|
280
|
+
...(locked.length > 0 ? { readOnly: locked } : {}),
|
|
281
|
+
};
|
|
260
282
|
if (!lookupManifest)
|
|
261
283
|
return read;
|
|
262
284
|
const ids = componentIdsOf(collection, document, lookupManifest);
|
|
@@ -416,6 +438,20 @@ export function createChatToolExecutor(options) {
|
|
|
416
438
|
if (!collection) {
|
|
417
439
|
return { ok: false, errorText: `Unknown collection "${input.collection}" — use a collection id from the catalog.` };
|
|
418
440
|
}
|
|
441
|
+
// EC-715: a singleton has its one document; the media library holds uploads. Neither
|
|
442
|
+
// takes a create — refused here, not proposed for the editor to discover on apply.
|
|
443
|
+
if (isSingleton(collection)) {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
errorText: `"${collection.id}" is a singleton — it has exactly one document, which is changed with updateFields, never created.`,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
if (collection.kind === 'asset') {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
errorText: `"${collection.id}" is the media library — assets are uploaded by people, never created by the assistant.`,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
419
455
|
const documentId = options.mintDocumentId(collection.id);
|
|
420
456
|
const knowledge = options.knowledge?.();
|
|
421
457
|
const result = await applyContentWithShape({
|
|
@@ -448,16 +484,25 @@ export function createChatToolExecutor(options) {
|
|
|
448
484
|
provenance: result.provenance,
|
|
449
485
|
attempts: result.attempts,
|
|
450
486
|
issues: result.issues,
|
|
487
|
+
...(result.unfilled.length > 0 ? { unfilled: result.unfilled } : {}),
|
|
451
488
|
});
|
|
452
489
|
// Compact summary only — the full patch goes to the review UI, not the model.
|
|
490
|
+
const unfilled = result.unfilled.map(describeUnfilled);
|
|
491
|
+
const titleValues = {
|
|
492
|
+
...result.patch.values,
|
|
493
|
+
...(options.locale ? result.patch.localized?.[options.locale] : {}),
|
|
494
|
+
};
|
|
453
495
|
return {
|
|
454
496
|
ok: true,
|
|
455
497
|
output: {
|
|
456
498
|
status: 'proposed',
|
|
457
499
|
collection: collection.id,
|
|
458
500
|
documentId,
|
|
459
|
-
title: resolveDocumentTitle(collection,
|
|
460
|
-
|
|
501
|
+
title: resolveDocumentTitle(collection, titleValues, documentId),
|
|
502
|
+
...(unfilled.length > 0 ? { unfilled } : {}),
|
|
503
|
+
note: unfilled.length > 0
|
|
504
|
+
? 'Draft proposal created; the editor reviews and applies it in the studio. Summarize briefly — do not repeat the content — and tell the user which fields were left empty (unfilled).'
|
|
505
|
+
: 'Draft proposal created; the editor reviews and applies it in the studio. Summarize briefly — do not repeat the content.',
|
|
461
506
|
},
|
|
462
507
|
};
|
|
463
508
|
}
|