@elytracms/studio 0.0.81 → 0.0.83
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/redirect-revalidate.test.ts +130 -0
- package/convex/revalidate.ts +9 -7
- package/convex/triggers.ts +33 -3
- package/dist/components/ai/ai-chat-panel.js +26 -4
- package/dist/components/ai/ai-chat-panel.js.map +1 -1
- package/dist/components/ai/proposal-card.d.ts +27 -0
- package/dist/components/ai/proposal-card.js +31 -3
- package/dist/components/ai/proposal-card.js.map +1 -1
- package/dist/components/ai/proposal-issues.d.ts +55 -0
- package/dist/components/ai/proposal-issues.js +69 -0
- package/dist/components/ai/proposal-issues.js.map +1 -0
- package/dist/components/ai/translate-document-dialog.js +38 -3
- package/dist/components/ai/translate-document-dialog.js.map +1 -1
- package/dist/components/app-views/studio-config.d.ts +1 -1
- package/dist/components/app-views/studio-config.js +1 -1
- package/dist/components/app-views/studio-config.js.map +1 -1
- package/dist/components/builder/canvas-render-context.js +3 -1
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +5 -3
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/cms/bulk-actions.js +8 -5
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cell-edit.d.ts +18 -3
- package/dist/components/cms/cell-edit.js +27 -9
- package/dist/components/cms/cell-edit.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +14 -1
- package/dist/components/cms/cms-store.js +93 -24
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +1 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +7 -12
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/document-editor-context.d.ts +12 -1
- package/dist/components/cms/document-editor-context.js.map +1 -1
- package/dist/components/cms/field-control.js +20 -7
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/gate-validation.d.ts +36 -0
- package/dist/components/cms/gate-validation.js +17 -0
- package/dist/components/cms/gate-validation.js.map +1 -0
- package/dist/components/cms/inline-field-errors.d.ts +8 -0
- package/dist/components/cms/inline-field-errors.js +52 -0
- package/dist/components/cms/inline-field-errors.js.map +1 -0
- package/dist/components/cms/issues-pane.js +8 -4
- package/dist/components/cms/issues-pane.js.map +1 -1
- package/dist/components/cms/slug-collision-report.d.ts +41 -0
- package/dist/components/cms/slug-collision-report.js +52 -0
- package/dist/components/cms/slug-collision-report.js.map +1 -0
- package/dist/components/deployment-setup-gate.d.ts +24 -0
- package/dist/components/deployment-setup-gate.js +29 -4
- package/dist/components/deployment-setup-gate.js.map +1 -1
- package/dist/components/issue-severity.d.ts +73 -0
- package/dist/components/issue-severity.js +56 -0
- package/dist/components/issue-severity.js.map +1 -0
- package/dist/components/nav-issues.d.ts +68 -0
- package/dist/components/nav-issues.js +78 -0
- package/dist/components/nav-issues.js.map +1 -0
- package/dist/components/operation-error.js +3 -3
- package/dist/components/operation-error.js.map +1 -1
- package/dist/components/section-nav.js +5 -38
- package/dist/components/section-nav.js.map +1 -1
- package/dist/lib/ai/apply-content.js +6 -7
- package/dist/lib/ai/apply-content.js.map +1 -1
- package/dist/lib/ai/chat-context.d.ts +32 -26
- package/dist/lib/ai/chat-context.js +81 -9
- package/dist/lib/ai/chat-context.js.map +1 -1
- package/dist/lib/ai/constraints.d.ts +42 -0
- package/dist/lib/ai/constraints.js +259 -0
- package/dist/lib/ai/constraints.js.map +1 -0
- package/dist/lib/ai/index.d.ts +2 -0
- package/dist/lib/ai/index.js +2 -0
- package/dist/lib/ai/index.js.map +1 -1
- package/dist/lib/ai/proposal-validation.d.ts +26 -0
- package/dist/lib/ai/proposal-validation.js +17 -0
- package/dist/lib/ai/proposal-validation.js.map +1 -0
- package/dist/lib/ai/shape.js +60 -13
- package/dist/lib/ai/shape.js.map +1 -1
- package/dist/lib/ai/tools.d.ts +8 -4
- package/dist/lib/ai/tools.js +43 -14
- package/dist/lib/ai/tools.js.map +1 -1
- package/dist/lib/ai/translate-document.d.ts +35 -1
- package/dist/lib/ai/translate-document.js +16 -1
- package/dist/lib/ai/translate-document.js.map +1 -1
- package/dist/lib/ai/types.d.ts +11 -6
- package/dist/lib/ai/types.js.map +1 -1
- package/dist/lib/ai/update-fields.d.ts +8 -6
- package/dist/lib/ai/update-fields.js +9 -22
- package/dist/lib/ai/update-fields.js.map +1 -1
- package/dist/lib/clipboard/paste-issues.d.ts +5 -0
- package/dist/lib/clipboard/paste-issues.js +16 -6
- package/dist/lib/clipboard/paste-issues.js.map +1 -1
- package/dist/lib/cms/document-gate.d.ts +37 -0
- package/dist/lib/cms/document-gate.js +40 -0
- package/dist/lib/cms/document-gate.js.map +1 -0
- package/dist/lib/cms-views/view-model.d.ts +5 -0
- package/dist/lib/cms-views/view-model.js +10 -4
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/data/collection-content.js +1 -3
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/i18n/i18n.d.ts +51 -16
- package/dist/lib/i18n/i18n.js +119 -43
- package/dist/lib/i18n/i18n.js.map +1 -1
- package/dist/lib/i18n/index.d.ts +8 -1
- package/dist/lib/i18n/index.js +1 -1
- package/dist/lib/i18n/index.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +68 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +59 -0
- package/dist/lib/i18n/messages.en.js +80 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/i18n/types.d.ts +15 -9
- package/dist/lib/i18n/types.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +19 -2
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.js +10 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +44 -9
- package/dist/lib/persona/labels.js +143 -60
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/webhooks/dispatcher.d.ts +5 -2
- package/dist/lib/webhooks/dispatcher.js.map +1 -1
- package/dist/lib/webhooks/tags.js +150 -17
- package/dist/lib/webhooks/tags.js.map +1 -1
- package/dist/lib/workspace/config-issues.d.ts +3 -1
- package/dist/lib/workspace/config-issues.js +3 -1
- package/dist/lib/workspace/config-issues.js.map +1 -1
- package/dist/lib/workspace/index.d.ts +15 -8
- package/dist/lib/workspace/index.js +17 -18
- package/dist/lib/workspace/index.js.map +1 -1
- package/dist/lib/workspace/workspace-source.js +27 -0
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/dist/previews/manifest-format.d.ts +13 -6
- package/dist/previews/manifest-format.js +9 -2
- package/dist/previews/manifest-format.js.map +1 -1
- package/dist/router/root.d.ts +36 -1
- package/dist/router/root.js +54 -7
- package/dist/router/root.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing constraint rendering (EC-473, tuned by EC-505).
|
|
3
|
+
*
|
|
4
|
+
* The engine has ONE constraint projection: `fieldJsonSchema` merges a field's
|
|
5
|
+
* declarative rules (`validation.min`/`max`/`pattern`) with the fragment derived
|
|
6
|
+
* from a schema-form `validation.validate` (`validation.schema`). This module turns
|
|
7
|
+
* that projection into what the MODEL should see — which is not the same artifact a
|
|
8
|
+
* JSON-Schema consumer wants:
|
|
9
|
+
*
|
|
10
|
+
* - **a regex is a poor instruction for a model.** `z.string().email()` emits a
|
|
11
|
+
* ~200-character pattern and `z.iso.date()` a leap-year monster (measured,
|
|
12
|
+
* EC-474 §3); `format` + bounds says the same thing in four tokens. So on the AI
|
|
13
|
+
* path a `pattern` is dropped wherever a `format` is present. The JSON Schema
|
|
14
|
+
* OUTPUT artifact keeps it — different consumer, different needs.
|
|
15
|
+
* - **a union `type` is a poor instruction too** (EC-505). A `date` field accepts
|
|
16
|
+
* an ISO string OR an epoch number *at rest*, so the general projection honestly
|
|
17
|
+
* says `type: ['string','number']`. The model does not need that latitude: it is
|
|
18
|
+
* asked to AUTHOR a date, EC-472 ratified that a date is an ISO instant, and the
|
|
19
|
+
* declared bounds this same projection carries (`formatMinimum`/`formatMaximum`)
|
|
20
|
+
* are ISO strings that mean nothing against an epoch. Some structured-output
|
|
21
|
+
* providers also reject a union `type` outright. So the model-facing shape says
|
|
22
|
+
* `type: 'string'`; accepting a number stays an input-parsing detail of the
|
|
23
|
+
* validator, not a shape the model is taught.
|
|
24
|
+
* - **incompleteness must be stated.** When `validation.schemaComplete` is `false`
|
|
25
|
+
* the projection is a partial view of the rule, and the model is told so rather
|
|
26
|
+
* than being handed a half-truth as the whole rule. (An `ai.hint` is exactly the
|
|
27
|
+
* right place to say what the dropped refinement wants — the note nudges toward
|
|
28
|
+
* writing one.)
|
|
29
|
+
*/
|
|
30
|
+
import { fieldJsonSchema } from '@elytracms/core/cms-core';
|
|
31
|
+
/** The note appended when a field's constraint projection is knowingly partial. */
|
|
32
|
+
export const INCOMPLETE_CONSTRAINTS_NOTE = 'further validation rules apply that are not expressible here — keep the value conventional and plausible';
|
|
33
|
+
/**
|
|
34
|
+
* The prose form of `format: 'date-time'` (EC-505). The JSON-Schema keyword rides
|
|
35
|
+
* on the target shape for the provider's validator; the CATALOG has no JSON Schema
|
|
36
|
+
* at all — it is prose only, and the chat update path authors date values through
|
|
37
|
+
* it. So the format is said in words too, or the only channel that path has says
|
|
38
|
+
* nothing about the one format the engine accepts.
|
|
39
|
+
*
|
|
40
|
+
* Kept to three words deliberately: measured at 6 tokens, against 20 for the same
|
|
41
|
+
* note with a worked example — and it rides every date field of every turn.
|
|
42
|
+
*/
|
|
43
|
+
export const ISO_DATE_NOTE = 'ISO 8601 date';
|
|
44
|
+
/**
|
|
45
|
+
* Drop `pattern` wherever `format` is present (EC-473/EC-474 req 6). Returns the
|
|
46
|
+
* same object when there is nothing to strip, so callers can rely on identity.
|
|
47
|
+
*/
|
|
48
|
+
export function stripRedundantPattern(schema) {
|
|
49
|
+
if (schema.format === undefined || schema.pattern === undefined)
|
|
50
|
+
return schema;
|
|
51
|
+
const { pattern: _pattern, ...rest } = schema;
|
|
52
|
+
return rest;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Narrow a date field's `['string','number']` union to the ISO string the model
|
|
56
|
+
* should author (EC-505 — see the module note). Keyed on the exact combination the
|
|
57
|
+
* date branch of the projection emits (`format: 'date-time'` + an array `type`
|
|
58
|
+
* containing `string`), so nothing else is touched. Identity-preserving.
|
|
59
|
+
*/
|
|
60
|
+
export function narrowDateUnion(schema) {
|
|
61
|
+
if (schema.format !== 'date-time')
|
|
62
|
+
return schema;
|
|
63
|
+
const type = schema.type;
|
|
64
|
+
if (!Array.isArray(type) || !type.includes('string'))
|
|
65
|
+
return schema;
|
|
66
|
+
return { ...schema, type: 'string' };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Apply a node transform through a schema tree (`items` + `properties`), preserving
|
|
70
|
+
* object identity wherever nothing changed — `buildTargetShape` relies on that to
|
|
71
|
+
* avoid copying a property it did not touch.
|
|
72
|
+
*/
|
|
73
|
+
function mapSchemaDeep(schema, transform) {
|
|
74
|
+
let out = transform(schema);
|
|
75
|
+
const items = out.items;
|
|
76
|
+
if (typeof items === 'object' && items !== null && !Array.isArray(items)) {
|
|
77
|
+
const next = mapSchemaDeep(items, transform);
|
|
78
|
+
if (next !== items)
|
|
79
|
+
out = { ...out, items: next };
|
|
80
|
+
}
|
|
81
|
+
const properties = out.properties;
|
|
82
|
+
if (typeof properties === 'object' && properties !== null && !Array.isArray(properties)) {
|
|
83
|
+
let changed = false;
|
|
84
|
+
const nextProps = {};
|
|
85
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
86
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
87
|
+
const next = mapSchemaDeep(value, transform);
|
|
88
|
+
nextProps[key] = next;
|
|
89
|
+
if (next !== value)
|
|
90
|
+
changed = true;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
nextProps[key] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (changed)
|
|
97
|
+
out = { ...out, properties: nextProps };
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/** Recursively strip redundant patterns through `items` / `properties`. */
|
|
102
|
+
export function stripRedundantPatternsDeep(schema) {
|
|
103
|
+
return mapSchemaDeep(schema, stripRedundantPattern);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The whole model-facing rewrite of a projected schema (EC-505): redundant patterns
|
|
107
|
+
* stripped and date unions narrowed, everywhere in the tree — an `object` field's
|
|
108
|
+
* date SUBfield gets the same treatment as a top-level one. Identity-preserving.
|
|
109
|
+
*/
|
|
110
|
+
export function toModelFacingSchema(schema) {
|
|
111
|
+
return mapSchemaDeep(schema, (node) => narrowDateUnion(stripRedundantPattern(node)));
|
|
112
|
+
}
|
|
113
|
+
/** The constraint keywords this module renders as prose, for the "is anything left?" scan. */
|
|
114
|
+
const RENDERED_KEYWORDS = [
|
|
115
|
+
'minLength',
|
|
116
|
+
'maxLength',
|
|
117
|
+
'minimum',
|
|
118
|
+
'maximum',
|
|
119
|
+
'minItems',
|
|
120
|
+
'maxItems',
|
|
121
|
+
'formatMinimum',
|
|
122
|
+
'formatMaximum',
|
|
123
|
+
'format',
|
|
124
|
+
'pattern',
|
|
125
|
+
'enum',
|
|
126
|
+
'const',
|
|
127
|
+
'multipleOf',
|
|
128
|
+
'exclusiveMinimum',
|
|
129
|
+
'exclusiveMaximum',
|
|
130
|
+
'uniqueItems',
|
|
131
|
+
];
|
|
132
|
+
/**
|
|
133
|
+
* Human-readable phrases for the constraint keywords of ONE schema node, in a stable
|
|
134
|
+
* order — the form a model reads best. Node-level only: nested `items`/`properties`
|
|
135
|
+
* are the caller's business (see {@link describeFieldConstraints}).
|
|
136
|
+
*/
|
|
137
|
+
function describeSchemaConstraints(schema) {
|
|
138
|
+
const out = [];
|
|
139
|
+
const num = (value) => typeof value === 'number' ? value : undefined;
|
|
140
|
+
const text = (value) => typeof value === 'string' ? value : undefined;
|
|
141
|
+
const minLength = num(schema.minLength);
|
|
142
|
+
const maxLength = num(schema.maxLength);
|
|
143
|
+
if (minLength !== undefined && maxLength !== undefined) {
|
|
144
|
+
out.push(`${minLength}–${maxLength} characters`);
|
|
145
|
+
}
|
|
146
|
+
else if (minLength !== undefined)
|
|
147
|
+
out.push(`at least ${minLength} characters`);
|
|
148
|
+
else if (maxLength !== undefined)
|
|
149
|
+
out.push(`at most ${maxLength} characters`);
|
|
150
|
+
const minimum = num(schema.minimum);
|
|
151
|
+
const maximum = num(schema.maximum);
|
|
152
|
+
if (minimum !== undefined && maximum !== undefined)
|
|
153
|
+
out.push(`between ${minimum} and ${maximum}`);
|
|
154
|
+
else if (minimum !== undefined)
|
|
155
|
+
out.push(`at least ${minimum}`);
|
|
156
|
+
else if (maximum !== undefined)
|
|
157
|
+
out.push(`at most ${maximum}`);
|
|
158
|
+
const minItems = num(schema.minItems);
|
|
159
|
+
const maxItems = num(schema.maxItems);
|
|
160
|
+
if (minItems !== undefined)
|
|
161
|
+
out.push(`at least ${minItems} items`);
|
|
162
|
+
if (maxItems !== undefined)
|
|
163
|
+
out.push(`at most ${maxItems} items`);
|
|
164
|
+
// EC-473: date bounds finally reach a consumer. `formatMinimum`/`formatMaximum`
|
|
165
|
+
// is the projection's machine form; this is the form a model acts on.
|
|
166
|
+
const from = text(schema.formatMinimum);
|
|
167
|
+
const to = text(schema.formatMaximum);
|
|
168
|
+
if (from !== undefined)
|
|
169
|
+
out.push(`not earlier than ${from}`);
|
|
170
|
+
if (to !== undefined)
|
|
171
|
+
out.push(`not later than ${to}`);
|
|
172
|
+
const format = text(schema.format);
|
|
173
|
+
// EC-505: `date-time` is said in words rather than skipped — the catalog carries
|
|
174
|
+
// no JSON Schema, so this is the only place the update path learns the format.
|
|
175
|
+
if (format === 'date-time')
|
|
176
|
+
out.push(ISO_DATE_NOTE);
|
|
177
|
+
else if (format !== undefined)
|
|
178
|
+
out.push(`format: ${format}`);
|
|
179
|
+
// Only a pattern that is NOT explained by a format is worth the tokens.
|
|
180
|
+
const pattern = text(schema.pattern);
|
|
181
|
+
if (pattern !== undefined && format === undefined)
|
|
182
|
+
out.push(`matching /${pattern}/`);
|
|
183
|
+
return out;
|
|
184
|
+
}
|
|
185
|
+
/** Does this fragment carry a constraint keyword BELOW its own top level? */
|
|
186
|
+
function hasNestedConstraint(schema) {
|
|
187
|
+
const scan = (node) => {
|
|
188
|
+
if (typeof node !== 'object' || node === null)
|
|
189
|
+
return false;
|
|
190
|
+
if (Array.isArray(node))
|
|
191
|
+
return node.some(scan);
|
|
192
|
+
const record = node;
|
|
193
|
+
if (RENDERED_KEYWORDS.some((keyword) => record[keyword] !== undefined))
|
|
194
|
+
return true;
|
|
195
|
+
return Object.values(record).some(scan);
|
|
196
|
+
};
|
|
197
|
+
return scan(schema.items) || scan(schema.properties);
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The PER-ITEM constraint fragment of a `multiple` field, if the author declared one
|
|
201
|
+
* (EC-505).
|
|
202
|
+
*
|
|
203
|
+
* A schema in the `validate` slot addresses the field's WHOLE value — that is not a
|
|
204
|
+
* choice this module makes, it is what the executor does: `validateDocument` calls
|
|
205
|
+
* the validator once with the resolved field value, which for a `multiple` field is
|
|
206
|
+
* the ARRAY (`validate-document.ts:1099`). The projection agrees with it, so a
|
|
207
|
+
* per-ITEM schema written directly (`z.string().min(3)` on a multi-valued field) is
|
|
208
|
+
* rejected loudly on the first save rather than silently mis-bounding anything.
|
|
209
|
+
*
|
|
210
|
+
* The per-item form is therefore `z.array(z.string().min(3))` — and THAT is what
|
|
211
|
+
* needed fixing: the merged projection contributes CONSTRAINT keywords only, so the
|
|
212
|
+
* derived fragment's nested `items` never reached the model even though the executor
|
|
213
|
+
* enforces it. This reads that fragment directly, which is the one place this module
|
|
214
|
+
* looks past `fieldJsonSchema` — see the EC-505 task file for the merge-side proposal.
|
|
215
|
+
*/
|
|
216
|
+
function perItemFragment(field) {
|
|
217
|
+
if (!('multiple' in field) || field.multiple !== true)
|
|
218
|
+
return undefined;
|
|
219
|
+
const derived = field.validation?.schema;
|
|
220
|
+
if (typeof derived !== 'object' || derived === null || Array.isArray(derived))
|
|
221
|
+
return undefined;
|
|
222
|
+
const items = derived.items;
|
|
223
|
+
if (typeof items !== 'object' || items === null || Array.isArray(items))
|
|
224
|
+
return undefined;
|
|
225
|
+
return items;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Human-readable constraint phrases for one field, in a stable order — the form a
|
|
229
|
+
* model reads best (and the form the collections catalog carries, where there is no
|
|
230
|
+
* JSON Schema to hand the model at all). Empty when the field constrains nothing.
|
|
231
|
+
*/
|
|
232
|
+
export function describeFieldConstraints(field) {
|
|
233
|
+
const out = [];
|
|
234
|
+
if (field.validation?.required === true)
|
|
235
|
+
out.push('required');
|
|
236
|
+
out.push(...describeSchemaConstraints(fieldJsonSchema(field)));
|
|
237
|
+
let incomplete = field.validation?.schemaComplete === false;
|
|
238
|
+
const items = perItemFragment(field);
|
|
239
|
+
if (items !== undefined) {
|
|
240
|
+
const itemParts = describeSchemaConstraints(items);
|
|
241
|
+
if (itemParts.length > 0)
|
|
242
|
+
out.push(`each item: ${itemParts.join(', ')}`);
|
|
243
|
+
// A nested rule this vocabulary cannot phrase (a constraint inside an OBJECT
|
|
244
|
+
// item, say) is declared partial rather than dropped in silence.
|
|
245
|
+
if (hasNestedConstraint(items))
|
|
246
|
+
incomplete = true;
|
|
247
|
+
}
|
|
248
|
+
if (field.validation?.unique === true)
|
|
249
|
+
out.push('unique across the collection');
|
|
250
|
+
if (incomplete)
|
|
251
|
+
out.push(INCOMPLETE_CONSTRAINTS_NOTE);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
/** The constraint phrases as one clause (`min 2 characters; required`), or `undefined`. */
|
|
255
|
+
export function constraintSentence(field) {
|
|
256
|
+
const parts = describeFieldConstraints(field);
|
|
257
|
+
return parts.length > 0 ? parts.join('; ') : undefined;
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=constraints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constraints.js","sourceRoot":"","sources":["../../../src/lib/ai/constraints.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Model-facing constraint rendering (EC-473, tuned by EC-505).\n *\n * The engine has ONE constraint projection: `fieldJsonSchema` merges a field's\n * declarative rules (`validation.min`/`max`/`pattern`) with the fragment derived\n * from a schema-form `validation.validate` (`validation.schema`). This module turns\n * that projection into what the MODEL should see — which is not the same artifact a\n * JSON-Schema consumer wants:\n *\n * - **a regex is a poor instruction for a model.** `z.string().email()` emits a\n * ~200-character pattern and `z.iso.date()` a leap-year monster (measured,\n * EC-474 §3); `format` + bounds says the same thing in four tokens. So on the AI\n * path a `pattern` is dropped wherever a `format` is present. The JSON Schema\n * OUTPUT artifact keeps it — different consumer, different needs.\n * - **a union `type` is a poor instruction too** (EC-505). A `date` field accepts\n * an ISO string OR an epoch number *at rest*, so the general projection honestly\n * says `type: ['string','number']`. The model does not need that latitude: it is\n * asked to AUTHOR a date, EC-472 ratified that a date is an ISO instant, and the\n * declared bounds this same projection carries (`formatMinimum`/`formatMaximum`)\n * are ISO strings that mean nothing against an epoch. Some structured-output\n * providers also reject a union `type` outright. So the model-facing shape says\n * `type: 'string'`; accepting a number stays an input-parsing detail of the\n * validator, not a shape the model is taught.\n * - **incompleteness must be stated.** When `validation.schemaComplete` is `false`\n * the projection is a partial view of the rule, and the model is told so rather\n * than being handed a half-truth as the whole rule. (An `ai.hint` is exactly the\n * right place to say what the dropped refinement wants — the note nudges toward\n * writing one.)\n */\nimport { fieldJsonSchema } from '@elytracms/core/cms-core'\nimport type { FieldDef, JsonSchema } from '@elytracms/core/cms-core'\n\n/** The note appended when a field's constraint projection is knowingly partial. */\nexport const INCOMPLETE_CONSTRAINTS_NOTE =\n 'further validation rules apply that are not expressible here — keep the value conventional and plausible'\n\n/**\n * The prose form of `format: 'date-time'` (EC-505). The JSON-Schema keyword rides\n * on the target shape for the provider's validator; the CATALOG has no JSON Schema\n * at all — it is prose only, and the chat update path authors date values through\n * it. So the format is said in words too, or the only channel that path has says\n * nothing about the one format the engine accepts.\n *\n * Kept to three words deliberately: measured at 6 tokens, against 20 for the same\n * note with a worked example — and it rides every date field of every turn.\n */\nexport const ISO_DATE_NOTE = 'ISO 8601 date'\n\n/**\n * Drop `pattern` wherever `format` is present (EC-473/EC-474 req 6). Returns the\n * same object when there is nothing to strip, so callers can rely on identity.\n */\nexport function stripRedundantPattern(schema: JsonSchema): JsonSchema {\n if (schema.format === undefined || schema.pattern === undefined) return schema\n const { pattern: _pattern, ...rest } = schema\n return rest\n}\n\n/**\n * Narrow a date field's `['string','number']` union to the ISO string the model\n * should author (EC-505 — see the module note). Keyed on the exact combination the\n * date branch of the projection emits (`format: 'date-time'` + an array `type`\n * containing `string`), so nothing else is touched. Identity-preserving.\n */\nexport function narrowDateUnion(schema: JsonSchema): JsonSchema {\n if (schema.format !== 'date-time') return schema\n const type = schema.type\n if (!Array.isArray(type) || !type.includes('string')) return schema\n return { ...schema, type: 'string' }\n}\n\n/**\n * Apply a node transform through a schema tree (`items` + `properties`), preserving\n * object identity wherever nothing changed — `buildTargetShape` relies on that to\n * avoid copying a property it did not touch.\n */\nfunction mapSchemaDeep(schema: JsonSchema, transform: (node: JsonSchema) => JsonSchema): JsonSchema {\n let out = transform(schema)\n const items = out.items\n if (typeof items === 'object' && items !== null && !Array.isArray(items)) {\n const next = mapSchemaDeep(items as JsonSchema, transform)\n if (next !== items) out = { ...out, items: next }\n }\n const properties = out.properties\n if (typeof properties === 'object' && properties !== null && !Array.isArray(properties)) {\n let changed = false\n const nextProps: Record<string, JsonSchema> = {}\n for (const [key, value] of Object.entries(properties as Record<string, unknown>)) {\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const next = mapSchemaDeep(value as JsonSchema, transform)\n nextProps[key] = next\n if (next !== value) changed = true\n } else {\n nextProps[key] = value as JsonSchema\n }\n }\n if (changed) out = { ...out, properties: nextProps }\n }\n return out\n}\n\n/** Recursively strip redundant patterns through `items` / `properties`. */\nexport function stripRedundantPatternsDeep(schema: JsonSchema): JsonSchema {\n return mapSchemaDeep(schema, stripRedundantPattern)\n}\n\n/**\n * The whole model-facing rewrite of a projected schema (EC-505): redundant patterns\n * stripped and date unions narrowed, everywhere in the tree — an `object` field's\n * date SUBfield gets the same treatment as a top-level one. Identity-preserving.\n */\nexport function toModelFacingSchema(schema: JsonSchema): JsonSchema {\n return mapSchemaDeep(schema, (node) => narrowDateUnion(stripRedundantPattern(node)))\n}\n\n/** The constraint keywords this module renders as prose, for the \"is anything left?\" scan. */\nconst RENDERED_KEYWORDS = [\n 'minLength',\n 'maxLength',\n 'minimum',\n 'maximum',\n 'minItems',\n 'maxItems',\n 'formatMinimum',\n 'formatMaximum',\n 'format',\n 'pattern',\n 'enum',\n 'const',\n 'multipleOf',\n 'exclusiveMinimum',\n 'exclusiveMaximum',\n 'uniqueItems',\n] as const\n\n/**\n * Human-readable phrases for the constraint keywords of ONE schema node, in a stable\n * order — the form a model reads best. Node-level only: nested `items`/`properties`\n * are the caller's business (see {@link describeFieldConstraints}).\n */\nfunction describeSchemaConstraints(schema: JsonSchema): string[] {\n const out: string[] = []\n const num = (value: unknown): number | undefined =>\n typeof value === 'number' ? value : undefined\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' ? value : undefined\n\n const minLength = num(schema.minLength)\n const maxLength = num(schema.maxLength)\n if (minLength !== undefined && maxLength !== undefined) {\n out.push(`${minLength}–${maxLength} characters`)\n } else if (minLength !== undefined) out.push(`at least ${minLength} characters`)\n else if (maxLength !== undefined) out.push(`at most ${maxLength} characters`)\n\n const minimum = num(schema.minimum)\n const maximum = num(schema.maximum)\n if (minimum !== undefined && maximum !== undefined) out.push(`between ${minimum} and ${maximum}`)\n else if (minimum !== undefined) out.push(`at least ${minimum}`)\n else if (maximum !== undefined) out.push(`at most ${maximum}`)\n\n const minItems = num(schema.minItems)\n const maxItems = num(schema.maxItems)\n if (minItems !== undefined) out.push(`at least ${minItems} items`)\n if (maxItems !== undefined) out.push(`at most ${maxItems} items`)\n\n // EC-473: date bounds finally reach a consumer. `formatMinimum`/`formatMaximum`\n // is the projection's machine form; this is the form a model acts on.\n const from = text(schema.formatMinimum)\n const to = text(schema.formatMaximum)\n if (from !== undefined) out.push(`not earlier than ${from}`)\n if (to !== undefined) out.push(`not later than ${to}`)\n\n const format = text(schema.format)\n // EC-505: `date-time` is said in words rather than skipped — the catalog carries\n // no JSON Schema, so this is the only place the update path learns the format.\n if (format === 'date-time') out.push(ISO_DATE_NOTE)\n else if (format !== undefined) out.push(`format: ${format}`)\n // Only a pattern that is NOT explained by a format is worth the tokens.\n const pattern = text(schema.pattern)\n if (pattern !== undefined && format === undefined) out.push(`matching /${pattern}/`)\n\n return out\n}\n\n/** Does this fragment carry a constraint keyword BELOW its own top level? */\nfunction hasNestedConstraint(schema: JsonSchema): boolean {\n const scan = (node: unknown): boolean => {\n if (typeof node !== 'object' || node === null) return false\n if (Array.isArray(node)) return node.some(scan)\n const record = node as Record<string, unknown>\n if (RENDERED_KEYWORDS.some((keyword) => record[keyword] !== undefined)) return true\n return Object.values(record).some(scan)\n }\n return scan(schema.items) || scan(schema.properties)\n}\n\n/**\n * The PER-ITEM constraint fragment of a `multiple` field, if the author declared one\n * (EC-505).\n *\n * A schema in the `validate` slot addresses the field's WHOLE value — that is not a\n * choice this module makes, it is what the executor does: `validateDocument` calls\n * the validator once with the resolved field value, which for a `multiple` field is\n * the ARRAY (`validate-document.ts:1099`). The projection agrees with it, so a\n * per-ITEM schema written directly (`z.string().min(3)` on a multi-valued field) is\n * rejected loudly on the first save rather than silently mis-bounding anything.\n *\n * The per-item form is therefore `z.array(z.string().min(3))` — and THAT is what\n * needed fixing: the merged projection contributes CONSTRAINT keywords only, so the\n * derived fragment's nested `items` never reached the model even though the executor\n * enforces it. This reads that fragment directly, which is the one place this module\n * looks past `fieldJsonSchema` — see the EC-505 task file for the merge-side proposal.\n */\nfunction perItemFragment(field: FieldDef): JsonSchema | undefined {\n if (!('multiple' in field) || field.multiple !== true) return undefined\n const derived = field.validation?.schema\n if (typeof derived !== 'object' || derived === null || Array.isArray(derived)) return undefined\n const items = (derived as JsonSchema).items\n if (typeof items !== 'object' || items === null || Array.isArray(items)) return undefined\n return items as JsonSchema\n}\n\n/**\n * Human-readable constraint phrases for one field, in a stable order — the form a\n * model reads best (and the form the collections catalog carries, where there is no\n * JSON Schema to hand the model at all). Empty when the field constrains nothing.\n */\nexport function describeFieldConstraints(field: FieldDef): string[] {\n const out: string[] = []\n if (field.validation?.required === true) out.push('required')\n out.push(...describeSchemaConstraints(fieldJsonSchema(field)))\n\n let incomplete = field.validation?.schemaComplete === false\n const items = perItemFragment(field)\n if (items !== undefined) {\n const itemParts = describeSchemaConstraints(items)\n if (itemParts.length > 0) out.push(`each item: ${itemParts.join(', ')}`)\n // A nested rule this vocabulary cannot phrase (a constraint inside an OBJECT\n // item, say) is declared partial rather than dropped in silence.\n if (hasNestedConstraint(items)) incomplete = true\n }\n\n if (field.validation?.unique === true) out.push('unique across the collection')\n if (incomplete) out.push(INCOMPLETE_CONSTRAINTS_NOTE)\n return out\n}\n\n/** The constraint phrases as one clause (`min 2 characters; required`), or `undefined`. */\nexport function constraintSentence(field: FieldDef): string | undefined {\n const parts = describeFieldConstraints(field)\n return parts.length > 0 ? parts.join('; ') : undefined\n}\n"]}
|
package/dist/lib/ai/index.d.ts
CHANGED
|
@@ -24,6 +24,8 @@ export * from './config.js';
|
|
|
24
24
|
export * from './provenance.js';
|
|
25
25
|
export * from './knowledge.js';
|
|
26
26
|
export * from './rich-text-input.js';
|
|
27
|
+
export * from './constraints.js';
|
|
28
|
+
export * from './proposal-validation.js';
|
|
27
29
|
export * from './shape.js';
|
|
28
30
|
export * from './apply-content.js';
|
|
29
31
|
export * from './update-fields.js';
|
package/dist/lib/ai/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export * from './config.js';
|
|
|
24
24
|
export * from './provenance.js';
|
|
25
25
|
export * from './knowledge.js';
|
|
26
26
|
export * from './rich-text-input.js';
|
|
27
|
+
export * from './constraints.js';
|
|
28
|
+
export * from './proposal-validation.js';
|
|
27
29
|
export * from './shape.js';
|
|
28
30
|
export * from './apply-content.js';
|
|
29
31
|
export * from './update-fields.js';
|
package/dist/lib/ai/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './constraints'\nexport * from './proposal-validation'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proposal validation gate (EC-473) — one rule for every AI write path.
|
|
3
|
+
*
|
|
4
|
+
* Every proposal the assistant produces is validated with the SAME engine the
|
|
5
|
+
* editor runs (`validateDocument`, with the studio's injected leashes and its
|
|
6
|
+
* store-dependent oracles), and the gate is always the same question: **did this
|
|
7
|
+
* proposal INTRODUCE an error?** Not "is the document valid" — a document may
|
|
8
|
+
* already be broken for reasons that are not the assistant's doing, and forcing a
|
|
9
|
+
* whole-document repair on top of an unrelated edit is how an assistant becomes
|
|
10
|
+
* unusable on real content (errors gate publish, not drafts).
|
|
11
|
+
*
|
|
12
|
+
* `buildUpdateProposal` established this discipline; EC-473 extends it to the two
|
|
13
|
+
* paths that had no validation at all — `translateDocument`'s output, and the
|
|
14
|
+
* APPLY step, which writes a proposal that may have been built against a document
|
|
15
|
+
* the editor has since changed.
|
|
16
|
+
*/
|
|
17
|
+
import type { CmsValidationIssue } from '@elytracms/core/cms-core';
|
|
18
|
+
/** Identity for "is this error pre-existing?" — code + path + message. */
|
|
19
|
+
export declare function issueKey(issue: CmsValidationIssue): string;
|
|
20
|
+
/**
|
|
21
|
+
* The error-severity issues present in `after` that were not already present in
|
|
22
|
+
* `before` — the ones the change is responsible for. Order follows `after`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function introducedErrors(before: readonly CmsValidationIssue[], after: readonly CmsValidationIssue[]): CmsValidationIssue[];
|
|
25
|
+
/** The non-error issues (warnings) of a validated result — carried onto the proposal. */
|
|
26
|
+
export declare function warningsOf(issues: readonly CmsValidationIssue[]): CmsValidationIssue[];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Identity for "is this error pre-existing?" — code + path + message. */
|
|
2
|
+
export function issueKey(issue) {
|
|
3
|
+
return `${issue.code}|${issue.path.join('.')}|${issue.message}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The error-severity issues present in `after` that were not already present in
|
|
7
|
+
* `before` — the ones the change is responsible for. Order follows `after`.
|
|
8
|
+
*/
|
|
9
|
+
export function introducedErrors(before, after) {
|
|
10
|
+
const preExisting = new Set(before.filter((issue) => issue.severity === 'error').map(issueKey));
|
|
11
|
+
return after.filter((issue) => issue.severity === 'error' && !preExisting.has(issueKey(issue)));
|
|
12
|
+
}
|
|
13
|
+
/** The non-error issues (warnings) of a validated result — carried onto the proposal. */
|
|
14
|
+
export function warningsOf(issues) {
|
|
15
|
+
return issues.filter((issue) => issue.severity !== 'error');
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=proposal-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proposal-validation.js","sourceRoot":"","sources":["../../../src/lib/ai/proposal-validation.ts"],"names":[],"mappings":"AAkBA,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,KAAyB;IAChD,OAAO,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,CAAA;AACjE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAqC,EACrC,KAAoC;IAEpC,MAAM,WAAW,GAAG,IAAI,GAAG,CACzB,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CACnE,CAAA;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACjG,CAAC;AAED,yFAAyF;AACzF,MAAM,UAAU,UAAU,CAAC,MAAqC;IAC9D,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;AAC7D,CAAC","sourcesContent":["/**\n * The proposal validation gate (EC-473) — one rule for every AI write path.\n *\n * Every proposal the assistant produces is validated with the SAME engine the\n * editor runs (`validateDocument`, with the studio's injected leashes and its\n * store-dependent oracles), and the gate is always the same question: **did this\n * proposal INTRODUCE an error?** Not \"is the document valid\" — a document may\n * already be broken for reasons that are not the assistant's doing, and forcing a\n * whole-document repair on top of an unrelated edit is how an assistant becomes\n * unusable on real content (errors gate publish, not drafts).\n *\n * `buildUpdateProposal` established this discipline; EC-473 extends it to the two\n * paths that had no validation at all — `translateDocument`'s output, and the\n * APPLY step, which writes a proposal that may have been built against a document\n * the editor has since changed.\n */\nimport type { CmsValidationIssue } from '@elytracms/core/cms-core'\n\n/** Identity for \"is this error pre-existing?\" — code + path + message. */\nexport function issueKey(issue: CmsValidationIssue): string {\n return `${issue.code}|${issue.path.join('.')}|${issue.message}`\n}\n\n/**\n * The error-severity issues present in `after` that were not already present in\n * `before` — the ones the change is responsible for. Order follows `after`.\n */\nexport function introducedErrors(\n before: readonly CmsValidationIssue[],\n after: readonly CmsValidationIssue[],\n): CmsValidationIssue[] {\n const preExisting = new Set(\n before.filter((issue) => issue.severity === 'error').map(issueKey),\n )\n return after.filter((issue) => issue.severity === 'error' && !preExisting.has(issueKey(issue)))\n}\n\n/** The non-error issues (warnings) of a validated result — carried onto the proposal. */\nexport function warningsOf(issues: readonly CmsValidationIssue[]): CmsValidationIssue[] {\n return issues.filter((issue) => issue.severity !== 'error')\n}\n"]}
|
package/dist/lib/ai/shape.js
CHANGED
|
@@ -23,27 +23,57 @@
|
|
|
23
23
|
*
|
|
24
24
|
* Localized fields stay flat in the shape (the model authors one locale at a time);
|
|
25
25
|
* the engine routes their values under `localized[locale]`.
|
|
26
|
+
*
|
|
27
|
+
* EC-473 — the shape now consumes the ONE constraint projection, so a rule declared
|
|
28
|
+
* in config actually reaches the model:
|
|
29
|
+
*
|
|
30
|
+
* - `collectionJsonSchema` merges declarative rules with the fragment derived from
|
|
31
|
+
* a schema-form `validation.validate`, so `{ min: 2 }` and `z.string().min(2)`
|
|
32
|
+
* arrive identically (and a declared DATE RANGE arrives at all — it previously
|
|
33
|
+
* reached no consumer whatsoever);
|
|
34
|
+
* - a rich-text field no longer OVERWRITES its constraints: the markdown schema is
|
|
35
|
+
* merged over them instead of replacing them (measured drift, EC-474 §2);
|
|
36
|
+
* - a `pattern` is dropped wherever a `format` explains the same thing (a
|
|
37
|
+
* 200-character email regex teaches a model nothing and costs real tokens);
|
|
38
|
+
* - a field whose projection is knowingly partial (`schemaComplete: false`) SAYS SO
|
|
39
|
+
* in its description rather than presenting the partial rule as the whole rule.
|
|
40
|
+
*
|
|
41
|
+
* EC-505 — one further narrowing of what the model is TOLD (never of what the engine
|
|
42
|
+
* accepts): a `date` property is `type: 'string'` + `format: 'date-time'` rather than
|
|
43
|
+
* the `['string','number']` union the general projection carries, because the model is
|
|
44
|
+
* asked to author an ISO instant (EC-472) and some providers reject a union `type`.
|
|
26
45
|
*/
|
|
27
|
-
import { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core';
|
|
46
|
+
import { collectionJsonSchema, fieldJsonSchema, resolveLabel } from '@elytracms/core/cms-core';
|
|
28
47
|
import { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text';
|
|
48
|
+
import { constraintSentence, stripRedundantPattern, toModelFacingSchema } from './constraints.js';
|
|
29
49
|
/**
|
|
30
|
-
* A rich-text field's model-facing schema (EC-379): a MARKDOWN string
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* the
|
|
34
|
-
*
|
|
50
|
+
* A rich-text field's model-facing schema (EC-379): a MARKDOWN string, MERGED over
|
|
51
|
+
* the field's own constraint projection (EC-473 — it used to replace it, silently
|
|
52
|
+
* dropping a declared `min`/`max`/`pattern`). The description carries the allowed
|
|
53
|
+
* subset (from the engine converter — one source of truth), so the model has
|
|
54
|
+
* something concrete to aim at; the engine converts the markdown into the
|
|
55
|
+
* `{ version, doc }` storage envelope at proposal-build time and rejects
|
|
56
|
+
* out-of-subset constructs with explicit issues.
|
|
35
57
|
*/
|
|
36
|
-
function richTextMarkdownSchema() {
|
|
58
|
+
function richTextMarkdownSchema(field) {
|
|
59
|
+
const constraints = stripRedundantPattern(fieldJsonSchema(field));
|
|
37
60
|
return {
|
|
61
|
+
...constraints,
|
|
38
62
|
type: 'string',
|
|
39
63
|
description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,
|
|
40
64
|
};
|
|
41
65
|
}
|
|
42
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* A field's model-facing description: resolved label + optional `ai.hint` + the
|
|
68
|
+
* constraint clause (EC-473). The constraints are stated in PROSE because that is
|
|
69
|
+
* what a model acts on — the same facts also ride as JSON-Schema keywords for the
|
|
70
|
+
* structured-output validator, which is a different reader.
|
|
71
|
+
*/
|
|
43
72
|
function describe(field, locale) {
|
|
44
73
|
const label = resolveLabel(field.form?.label, locale) ?? field.name;
|
|
45
74
|
const hint = field.ai?.hint;
|
|
46
|
-
const
|
|
75
|
+
const constraints = constraintSentence(field);
|
|
76
|
+
const parts = [label, hint, constraints].filter((p) => typeof p === 'string' && p.length > 0);
|
|
47
77
|
return parts.length > 0 ? parts.join(' — ') : undefined;
|
|
48
78
|
}
|
|
49
79
|
/**
|
|
@@ -116,14 +146,31 @@ export function buildTargetShape(input) {
|
|
|
116
146
|
assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids });
|
|
117
147
|
}
|
|
118
148
|
else if (field.type === 'richText') {
|
|
119
|
-
properties[field.name] = richTextMarkdownSchema();
|
|
149
|
+
properties[field.name] = richTextMarkdownSchema(field);
|
|
120
150
|
}
|
|
121
151
|
const prop = properties[field.name];
|
|
122
152
|
if (prop) {
|
|
153
|
+
// EC-473: a `pattern` next to a `format` is 200+ characters that teach the
|
|
154
|
+
// model nothing — the format (and the prose constraints below) say it better.
|
|
155
|
+
// EC-505: and a `date`'s `['string','number']` union becomes the ISO string
|
|
156
|
+
// the model is actually asked to author.
|
|
157
|
+
const stripped = toModelFacingSchema(prop);
|
|
123
158
|
const desc = describe(field, input.locale);
|
|
124
|
-
// Preserve an existing description (e.g. relation enum / rich-text note)
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
// Preserve an existing description (e.g. relation enum / rich-text note), but
|
|
160
|
+
// append the constraint clause to it — the enum leash and the value rules are
|
|
161
|
+
// different facts and the model needs both.
|
|
162
|
+
const next = stripped === prop ? prop : { ...stripped };
|
|
163
|
+
if (desc !== undefined) {
|
|
164
|
+
const existing = typeof next.description === 'string' ? next.description : undefined;
|
|
165
|
+
const constraints = constraintSentence(field);
|
|
166
|
+
next.description =
|
|
167
|
+
existing === undefined
|
|
168
|
+
? desc
|
|
169
|
+
: constraints !== undefined
|
|
170
|
+
? `${existing} — ${constraints}`
|
|
171
|
+
: existing;
|
|
172
|
+
}
|
|
173
|
+
properties[field.name] = next;
|
|
127
174
|
}
|
|
128
175
|
}
|
|
129
176
|
const requiredIn = Array.isArray(base.required) ? base.required : [];
|
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 three leashes the AI\n * engine adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation fields become an ENUM over the provided candidate ids (or are omitted\n * when no candidates exist) — so a hallucinated relation id is not representable\n * as valid structured output.\n * - asset/image fields become an ENUM over the searched library asset ids (or are\n * omitted when none were searched) — EC-381, the SAME leash as relations: a\n * proposed asset id must be a real, `listAssets`-returned library asset, and a\n * create shape with no searched assets simply has no asset property (empty beats\n * wrong; the assistant never generates or uploads).\n * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the\n * allowed subset, and the engine converts md → the `{ version, doc }` storage\n * envelope at proposal-build time. The field's `allow` config leashes component\n * EMBEDS only — markdown cannot express an embed, so the leash is satisfied by\n * construction (and still enforced downstream by `validateRichTextEmbeds`).\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 */\nimport { collectionJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text'\nimport type { AssetCandidates, RelationCandidates } from './types'\n\n/** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */\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/** An asset field's resolved candidate set — how the engine maps ids → asset envelopes (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 whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n /** Asset fields with their candidate enums (EC-381). */\n assetFields: TargetAssetField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** Relation candidate ids by field — a relation field with none is omitted from the shape. */\n candidates?: RelationCandidates\n /** Asset candidate ids by field — an asset field with none is omitted from the shape (EC-381). */\n assetCandidates?: AssetCandidates\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/**\n * A rich-text field's model-facing schema (EC-379): a MARKDOWN string. The\n * description carries the allowed subset (from the engine converter — one source\n * of truth), so the model has something concrete to aim at; the engine converts\n * the markdown into the `{ version, doc }` storage envelope at proposal-build\n * time and rejects out-of-subset constructs with explicit issues.\n */\nfunction richTextMarkdownSchema(): JsonSchema {\n return {\n type: 'string',\n description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,\n }\n}\n\n/** A field's model-facing description: resolved label + optional `ai.hint`. */\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 parts = [label, hint].filter((p): p is string => typeof p === 'string' && p.length > 0)\n return parts.length > 0 ? parts.join(' — ') : undefined\n}\n\n/**\n * Build the leash-aware target shape from a collection + candidates. Deterministic;\n * never throws. Reuses `collectionJsonSchema` as the base, then rewrites relations /\n * rich text and strips excluded fields.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\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 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 if (field.type === 'relation') {\n const candidates = input.candidates?.[field.name] ?? []\n if (candidates.length === 0) {\n // No candidate ids ⇒ omit the field: the model cannot emit a relation value\n // at all, so a hallucinated id is structurally impossible.\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose one of: ${label}`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n relationFields.push({\n fieldName: field.name,\n target: field.target,\n multiple: field.multiple,\n candidateIds: ids,\n })\n } else if (field.type === 'asset') {\n // EC-381 — same leash as relations: an asset field is an enum over the ids\n // the model actually searched in the media library (`listAssets`). No\n // searched assets ⇒ omit the field, so the proposal simply leaves it empty\n // (an invented / to-be-uploaded id is structurally unrepresentable).\n const candidates = input.assetCandidates?.[field.name] ?? []\n if (candidates.length === 0) {\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose an EXISTING library asset id, one of: ${label}. Leave empty if none fits.`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids })\n } else if (field.type === 'richText') {\n properties[field.name] = richTextMarkdownSchema()\n }\n\n const prop = properties[field.name]\n if (prop) {\n const desc = describe(field, input.locale)\n // Preserve an existing description (e.g. relation enum / rich-text note).\n if (desc && typeof prop.description !== 'string') prop.description = desc\n }\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 }\n return { schema, excluded, 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 three leashes the AI\n * engine adds on top:\n *\n * - `ai.exclude` fields are removed entirely (the model never sees them → cannot\n * write them).\n * - relation fields become an ENUM over the provided candidate ids (or are omitted\n * when no candidates exist) — so a hallucinated relation id is not representable\n * as valid structured output.\n * - asset/image fields become an ENUM over the searched library asset ids (or are\n * omitted when none were searched) — EC-381, the SAME leash as relations: a\n * proposed asset id must be a real, `listAssets`-returned library asset, and a\n * create shape with no searched assets simply has no asset property (empty beats\n * wrong; the assistant never generates or uploads).\n * - rich-text fields are MARKDOWN STRINGS (EC-379): the description names the\n * allowed subset, and the engine converts md → the `{ version, doc }` storage\n * envelope at proposal-build time. The field's `allow` config leashes component\n * EMBEDS only — markdown cannot express an embed, so the leash is satisfied by\n * construction (and still enforced downstream by `validateRichTextEmbeds`).\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 */\nimport { collectionJsonSchema, fieldJsonSchema, resolveLabel } from '@elytracms/core/cms-core'\nimport type { CollectionDef, FieldDef, JsonSchema } from '@elytracms/core/cms-core'\nimport { MARKDOWN_PROSE_DESCRIPTION } from '@elytracms/core/rich-text'\nimport { constraintSentence, stripRedundantPattern, toModelFacingSchema } from './constraints'\nimport type { AssetCandidates, RelationCandidates } from './types'\n\n/** A relation field's resolved candidate set — how the engine maps ids → reference envelopes. */\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/** An asset field's resolved candidate set — how the engine maps ids → asset envelopes (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 whose values route under `localized[locale]`. */\n localizedFields: string[]\n /** Relation fields with their candidate enums. */\n relationFields: TargetRelationField[]\n /** Asset fields with their candidate enums (EC-381). */\n assetFields: TargetAssetField[]\n}\n\nexport interface BuildTargetShapeInput {\n collection: CollectionDef\n /** Relation candidate ids by field — a relation field with none is omitted from the shape. */\n candidates?: RelationCandidates\n /** Asset candidate ids by field — an asset field with none is omitted from the shape (EC-381). */\n assetCandidates?: AssetCandidates\n /** UI locale for resolving field labels into descriptions. */\n locale?: string\n}\n\n/**\n * A rich-text field's model-facing schema (EC-379): a MARKDOWN string, MERGED over\n * the field's own constraint projection (EC-473 — it used to replace it, silently\n * dropping a declared `min`/`max`/`pattern`). The description carries the allowed\n * subset (from the engine converter — one source of truth), so the model has\n * something concrete to aim at; the engine converts the markdown into the\n * `{ version, doc }` storage envelope at proposal-build time and rejects\n * out-of-subset constructs with explicit issues.\n */\nfunction richTextMarkdownSchema(field: FieldDef): JsonSchema {\n const constraints = stripRedundantPattern(fieldJsonSchema(field))\n return {\n ...constraints,\n type: 'string',\n description: `Rich text, written as ${MARKDOWN_PROSE_DESCRIPTION} Write genuine, publishable prose.`,\n }\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 + candidates. Deterministic;\n * never throws. Reuses `collectionJsonSchema` as the base, then rewrites relations /\n * rich text and strips excluded fields.\n */\nexport function buildTargetShape(input: BuildTargetShapeInput): TargetShape {\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 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 if (field.type === 'relation') {\n const candidates = input.candidates?.[field.name] ?? []\n if (candidates.length === 0) {\n // No candidate ids ⇒ omit the field: the model cannot emit a relation value\n // at all, so a hallucinated id is structurally impossible.\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose one of: ${label}`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n relationFields.push({\n fieldName: field.name,\n target: field.target,\n multiple: field.multiple,\n candidateIds: ids,\n })\n } else if (field.type === 'asset') {\n // EC-381 — same leash as relations: an asset field is an enum over the ids\n // the model actually searched in the media library (`listAssets`). No\n // searched assets ⇒ omit the field, so the proposal simply leaves it empty\n // (an invented / to-be-uploaded id is structurally unrepresentable).\n const candidates = input.assetCandidates?.[field.name] ?? []\n if (candidates.length === 0) {\n delete properties[field.name]\n continue\n }\n const ids = candidates.map((c) => c.id)\n const label = candidates\n .map((c) => (c.label ? `${c.id} (${c.label})` : c.id))\n .join(', ')\n const idSchema: JsonSchema = {\n type: 'string',\n enum: ids,\n description: `Choose an EXISTING library asset id, one of: ${label}. Leave empty if none fits.`,\n }\n properties[field.name] = field.multiple ? { type: 'array', items: idSchema } : idSchema\n assetFields.push({ fieldName: field.name, multiple: field.multiple, candidateIds: ids })\n } else if (field.type === 'richText') {\n properties[field.name] = richTextMarkdownSchema(field)\n }\n\n const prop = properties[field.name]\n if (prop) {\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-505: and a `date`'s `['string','number']` union becomes the ISO string\n // the model is actually asked to author.\n const stripped = toModelFacingSchema(prop)\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 === prop ? prop : { ...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\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 }\n return { schema, excluded, localizedFields, relationFields, assetFields }\n}\n"]}
|
package/dist/lib/ai/tools.d.ts
CHANGED
|
@@ -249,10 +249,14 @@ export interface ChatToolExecutorOptions {
|
|
|
249
249
|
locale?: string;
|
|
250
250
|
/** Locale config — required for `translateDocument`. */
|
|
251
251
|
localeConfig?: LocaleConfig;
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
252
|
+
/**
|
|
253
|
+
* THE validation option bag (EC-497) — the CMS session's
|
|
254
|
+
* `gateValidateOptions(...)`, forwarded whole to every engine below. EC-473
|
|
255
|
+
* made the assistant's gate the editor's gate; passing one bag instead of five
|
|
256
|
+
* keys makes it stay that way, because a key can no longer be dropped by being
|
|
257
|
+
* forgotten at one of the three call sites this replaced.
|
|
258
|
+
*/
|
|
259
|
+
validateOptions?: ValidateDocumentOptions;
|
|
256
260
|
/** Max repair attempts inside `applyContentWithShape`. */
|
|
257
261
|
maxRepairAttempts?: number;
|
|
258
262
|
/** Per-search result cap (default {@link DEFAULT_SEARCH_LIMIT}). */
|