@caelo-cms/shared 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-tools.d.ts +30 -144
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +41 -141
- package/dist/ai-tools.js.map +1 -1
- package/dist/build-page.d.ts +5 -5
- package/dist/build-page.js +9 -9
- package/dist/build-page.js.map +1 -1
- package/dist/content.d.ts +13 -2
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +45 -12
- package/dist/content.js.map +1 -1
- package/dist/css-scope-scan.js +1 -1
- package/dist/escaped-line-breaks.d.ts +44 -0
- package/dist/escaped-line-breaks.d.ts.map +1 -0
- package/dist/escaped-line-breaks.js +203 -0
- package/dist/escaped-line-breaks.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interactions.js +5 -5
- package/dist/interactions.js.map +1 -1
- package/dist/lists/default-types.d.ts +12 -0
- package/dist/lists/default-types.d.ts.map +1 -1
- package/dist/lists/default-types.js +36 -0
- package/dist/lists/default-types.js.map +1 -1
- package/dist/lists/fields.d.ts.map +1 -1
- package/dist/lists/fields.js +1 -0
- package/dist/lists/fields.js.map +1 -1
- package/dist/lists/render-data.d.ts +9 -0
- package/dist/lists/render-data.d.ts.map +1 -1
- package/dist/lists/render-deps.d.ts +4 -3
- package/dist/lists/render-deps.d.ts.map +1 -1
- package/dist/lists/render-deps.js +5 -3
- package/dist/lists/render-deps.js.map +1 -1
- package/dist/lists/resolve.d.ts +13 -0
- package/dist/lists/resolve.d.ts.map +1 -1
- package/dist/lists/resolve.js +95 -3
- package/dist/lists/resolve.js.map +1 -1
- package/dist/nested-module-render.d.ts +13 -0
- package/dist/nested-module-render.d.ts.map +1 -1
- package/dist/nested-module-render.js +34 -0
- package/dist/nested-module-render.js.map +1 -1
- package/dist/redirect-plan.d.ts +13 -0
- package/dist/redirect-plan.d.ts.map +1 -1
- package/dist/redirect-plan.js +22 -0
- package/dist/redirect-plan.js.map +1 -1
- package/dist/seo.d.ts +21 -0
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +32 -0
- package/dist/seo.js.map +1 -1
- package/dist/source-text-transport.d.ts +28 -2
- package/dist/source-text-transport.d.ts.map +1 -1
- package/dist/source-text-transport.js +12 -4
- package/dist/source-text-transport.js.map +1 -1
- package/dist/template-sections.d.ts.map +1 -1
- package/dist/template-sections.js +17 -4
- package/dist/template-sections.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/__tests__/redos-hardening.test.ts +1 -0
- package/src/ai-tools-add-module-modes.test.ts +1 -1
- package/src/ai-tools.test.ts +7 -20
- package/src/ai-tools.ts +49 -153
- package/src/bug-intake.test.ts +1 -1
- package/src/build-page.ts +9 -9
- package/src/content.test.ts +26 -1
- package/src/content.ts +47 -13
- package/src/css-scope-scan.ts +1 -1
- package/src/escaped-line-breaks.ts +221 -0
- package/src/index.ts +1 -0
- package/src/interactions.ts +5 -5
- package/src/link-refs.test.ts +1 -0
- package/src/lists/default-types.ts +39 -0
- package/src/lists/fields.ts +1 -0
- package/src/lists/list-fixtures.test.ts +9 -3
- package/src/lists/render-data.ts +9 -0
- package/src/lists/render-deps.ts +6 -3
- package/src/lists/resolve.test.ts +182 -5
- package/src/lists/resolve.ts +101 -3
- package/src/nested-module-render.test.ts +39 -0
- package/src/nested-module-render.ts +35 -0
- package/src/redirect-plan.test.ts +25 -0
- package/src/redirect-plan.ts +24 -0
- package/src/seo.test.ts +58 -0
- package/src/seo.ts +49 -0
- package/src/source-text-transport.test.ts +98 -3
- package/src/source-text-transport.ts +15 -4
- package/src/template-engine-lists.test.ts +55 -0
- package/src/template-sections.ts +18 -4
- package/src/version.ts +1 -1
package/src/ai-tools.ts
CHANGED
|
@@ -17,15 +17,14 @@ import {
|
|
|
17
17
|
contentInstanceDeleteSchema,
|
|
18
18
|
contentInstanceUpdateSchema,
|
|
19
19
|
forkPlacementContentSchema,
|
|
20
|
-
MODULE_CSS_MAX,
|
|
21
|
-
MODULE_HTML_MAX,
|
|
22
|
-
MODULE_JS_MAX,
|
|
23
20
|
type ModuleField,
|
|
24
21
|
moduleFieldSchema,
|
|
22
|
+
pageShortTitleSchema,
|
|
25
23
|
pageTypeSlugSchema,
|
|
26
24
|
setPlacementContentSchema,
|
|
27
25
|
slugSchema,
|
|
28
26
|
} from "./content.js";
|
|
27
|
+
import type { SourceLanguage } from "./escaped-line-breaks.js";
|
|
29
28
|
import { LIST_ITEM_ATTR_RE } from "./lists/item-attr.js";
|
|
30
29
|
import { pluginPreviewSelectionSchema } from "./plugin-preview.js";
|
|
31
30
|
import { redirectTargetPathSchema } from "./redirect-target.js";
|
|
@@ -71,9 +70,10 @@ function stripSurroundingQuotes(v: unknown): unknown {
|
|
|
71
70
|
* is handled in one place instead of refused per tool.
|
|
72
71
|
*
|
|
73
72
|
* @param schema - the string schema (length caps, min) the decoded value must satisfy.
|
|
73
|
+
* @param language - the grammar of the value (`html`, `css`, `js`).
|
|
74
74
|
*/
|
|
75
|
-
export function sourceTextArg<T extends z.ZodType<string>>(schema: T) {
|
|
76
|
-
return z.preprocess(decodeSourceTextTransport, schema);
|
|
75
|
+
export function sourceTextArg<T extends z.ZodType<string>>(schema: T, language: SourceLanguage) {
|
|
76
|
+
return z.preprocess((value) => decodeSourceTextTransport(value, language), schema);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
export const positionInputSchema = z.preprocess(
|
|
@@ -87,63 +87,6 @@ export const positionInputSchema = z.preprocess(
|
|
|
87
87
|
]),
|
|
88
88
|
);
|
|
89
89
|
|
|
90
|
-
export const editModuleToolInput = z
|
|
91
|
-
.object({
|
|
92
|
-
moduleId: z.string().uuid(),
|
|
93
|
-
displayName: z.string().min(1).max(128).optional(),
|
|
94
|
-
/**
|
|
95
|
-
* v0.12.0 — rewrite the module's purpose. Optional; passing it
|
|
96
|
-
* updates the `list_modules` catalog your future self will read.
|
|
97
|
-
*/
|
|
98
|
-
description: z.string().max(1000).optional(),
|
|
99
|
-
/** v0.12.0 — re-classify the module's role tag. */
|
|
100
|
-
kind: z.enum(["chrome", "hero", "content", "cta", "utility"]).optional(),
|
|
101
|
-
/**
|
|
102
|
-
* v0.12.3 (issue #106) — re-classify the module's stable `type` (the
|
|
103
|
-
* class a parent's `allowedModuleTypes` whitelist matches against,
|
|
104
|
-
* e.g. `button`). Rarely needed; usually derived from displayName at
|
|
105
|
-
* create time. Set it to make this module satisfy a parent's allowlist.
|
|
106
|
-
*/
|
|
107
|
-
type: slugSchema.optional(),
|
|
108
|
-
html: sourceTextArg(z.string().max(MODULE_HTML_MAX)).optional(),
|
|
109
|
-
css: sourceTextArg(z.string().max(MODULE_CSS_MAX)).optional(),
|
|
110
|
-
js: sourceTextArg(z.string().max(MODULE_JS_MAX)).optional(),
|
|
111
|
-
/**
|
|
112
|
-
* v0.4.0 — module field schema. Each field declares one substitution
|
|
113
|
-
* slot referenced in the module HTML as `{{name}}`. Page placements
|
|
114
|
-
* fill these via `set_page_module_content`. Optional on edits — pass
|
|
115
|
-
* to replace the declared schema.
|
|
116
|
-
*/
|
|
117
|
-
fields: z.array(moduleFieldSchema).max(64).optional(),
|
|
118
|
-
/** issue #164 slice 2 — see addModuleToolInput.bindThemeLiterals. */
|
|
119
|
-
bindThemeLiterals: z.boolean().optional(),
|
|
120
|
-
})
|
|
121
|
-
.strict()
|
|
122
|
-
// e2e-livedit run 40351690739: a call with only a (placeholder)
|
|
123
|
-
// moduleId reached the op and came back as a bare "module not found".
|
|
124
|
-
// An edit that changes nothing is refused before any lookup, with the
|
|
125
|
-
// fields that make it one.
|
|
126
|
-
.refine((v) => Object.keys(v).some((k) => k !== "moduleId" && k !== "bindThemeLiterals"), {
|
|
127
|
-
message:
|
|
128
|
-
"edit_module changes nothing: pass what to change (html, css, js, fields, displayName, description, kind or type). list_modules shows the modules; add_module creates a new one.",
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* v0.4.0 — `set_page_module_content` AI tool. Fills the content values for
|
|
133
|
-
* a module placement on a specific page. Page-bound + branch-isolated per
|
|
134
|
-
* chat until publish (unlike `edit_module` which is global + immediate).
|
|
135
|
-
*/
|
|
136
|
-
export const setPageModuleContentToolInput = z
|
|
137
|
-
.object({
|
|
138
|
-
pageId: z.string().uuid(),
|
|
139
|
-
blockName: z.string().min(1).max(64),
|
|
140
|
-
position: z.number().int().nonnegative(),
|
|
141
|
-
/** Map of module field name → value. Fully replaces existing values. */
|
|
142
|
-
contentValues: z.record(z.string(), z.unknown()),
|
|
143
|
-
})
|
|
144
|
-
.strict();
|
|
145
|
-
export type SetPageModuleContentToolInput = z.infer<typeof setPageModuleContentToolInput>;
|
|
146
|
-
|
|
147
90
|
export const siteMemoryProposeToolInput = z
|
|
148
91
|
.object({
|
|
149
92
|
slot: z.enum(["purpose", "brand-voice", "tone", "banned-phrases", "instructions", "glossary"]),
|
|
@@ -218,9 +161,9 @@ export const addModuleToolInput = z
|
|
|
218
161
|
description: z.string().max(1000).optional(),
|
|
219
162
|
kind: z.enum(["chrome", "hero", "content", "cta", "utility"]).optional(),
|
|
220
163
|
type: slugSchema.optional(),
|
|
221
|
-
html: sourceTextArg(z.string().min(1).max(50_000)).optional(),
|
|
222
|
-
css: sourceTextArg(z.string().max(50_000)).optional(),
|
|
223
|
-
js: sourceTextArg(z.string().max(50_000)).optional(),
|
|
164
|
+
html: sourceTextArg(z.string().min(1).max(50_000), "html").optional(),
|
|
165
|
+
css: sourceTextArg(z.string().max(50_000), "css").optional(),
|
|
166
|
+
js: sourceTextArg(z.string().max(50_000), "js").optional(),
|
|
224
167
|
fields: z.array(moduleFieldSchema).max(64).optional(),
|
|
225
168
|
bindThemeLiterals: z.boolean().optional(),
|
|
226
169
|
/**
|
|
@@ -233,8 +176,8 @@ export const addModuleToolInput = z
|
|
|
233
176
|
* stores the values as the minted module's `fields[].default` —
|
|
234
177
|
* which requires explicit `fields` (moduleize field names are
|
|
235
178
|
* unknowable up front) and is rejected for `moduleId` reuse (a
|
|
236
|
-
* shared module renders its stored defaults;
|
|
237
|
-
* them).
|
|
179
|
+
* shared module renders its stored defaults; update_modules_many
|
|
180
|
+
* changes them).
|
|
238
181
|
*/
|
|
239
182
|
values: z.record(z.string(), z.unknown()).optional(),
|
|
240
183
|
})
|
|
@@ -245,7 +188,7 @@ export const addModuleToolInput = z
|
|
|
245
188
|
ctx.addIssue({
|
|
246
189
|
code: z.ZodIssueCode.custom,
|
|
247
190
|
message:
|
|
248
|
-
"Reusing a module on a layout renders the module's STORED field defaults — `values` has nothing to bind to. Drop `values` (the chrome shows the module as-is), or change the shared copy via
|
|
191
|
+
"Reusing a module on a layout renders the module's STORED field defaults — `values` has nothing to bind to. Drop `values` (the chrome shows the module as-is), or change the shared copy via update_modules_many.",
|
|
249
192
|
});
|
|
250
193
|
} else if (input.fields === undefined || input.fields.length === 0) {
|
|
251
194
|
ctx.addIssue({
|
|
@@ -269,7 +212,7 @@ export const addModuleToolInput = z
|
|
|
269
212
|
// (§1A/§11), so we do NOT reject a moduleId call that also carries authoring
|
|
270
213
|
// fields. Those fields are not applied here; the tool HANDLER surfaces an
|
|
271
214
|
// INFO for any ignored field whose value DIFFERS from the module's stored
|
|
272
|
-
// one, pointing at
|
|
215
|
+
// one, pointing at update_modules_many — so a dropped change is never silent (§2).
|
|
273
216
|
if (input.moduleId !== undefined) return;
|
|
274
217
|
if (input.displayName === undefined || input.html === undefined) {
|
|
275
218
|
ctx.addIssue({
|
|
@@ -361,44 +304,6 @@ export const deleteContentInstanceToolInput = contentInstanceDeleteSchema;
|
|
|
361
304
|
export const setPlacementContentToolInput = setPlacementContentSchema;
|
|
362
305
|
export const forkPlacementContentToolInput = forkPlacementContentSchema;
|
|
363
306
|
|
|
364
|
-
export const AI_TOOLS = [
|
|
365
|
-
"edit_module",
|
|
366
|
-
"site_memory_propose",
|
|
367
|
-
"add_module",
|
|
368
|
-
"create_page",
|
|
369
|
-
// audit #3 — page metadata (name/title/slug/template/status) is one tool for
|
|
370
|
-
// 1..200 pages: `update_pages_many`. The former rename_page / set_page_title
|
|
371
|
-
// / change_page_slug were single-field wrappers over the same pages.update.
|
|
372
|
-
"update_pages_many",
|
|
373
|
-
// audit #4 — one delete tool for 1..200 pages, disposition per page.
|
|
374
|
-
// The former singular delete_page folded in (n=1 is a one-item array).
|
|
375
|
-
"delete_pages_many",
|
|
376
|
-
// one remove-module tool routed by target (page|layout).
|
|
377
|
-
"remove_module_from",
|
|
378
|
-
"update_theme",
|
|
379
|
-
"set_template_layout",
|
|
380
|
-
"create_layout",
|
|
381
|
-
"set_site_defaults",
|
|
382
|
-
"duplicate_page",
|
|
383
|
-
"repoint_page_template",
|
|
384
|
-
"move_module",
|
|
385
|
-
"reorder_module",
|
|
386
|
-
"set_nav_menu",
|
|
387
|
-
"add_plugin_to_page",
|
|
388
|
-
// v0.12.0 — content_instances + placement binding
|
|
389
|
-
"list_content_instances",
|
|
390
|
-
"get_content_instance",
|
|
391
|
-
"create_content_instance",
|
|
392
|
-
"set_content_instance_values",
|
|
393
|
-
"delete_content_instance",
|
|
394
|
-
"set_placement_content",
|
|
395
|
-
"fork_placement_content",
|
|
396
|
-
// issue #299 — bulk-first build path (§11)
|
|
397
|
-
"build_page",
|
|
398
|
-
"create_content_instances",
|
|
399
|
-
"set_page_module_content_many",
|
|
400
|
-
] as const;
|
|
401
|
-
export type AiToolName = (typeof AI_TOOLS)[number];
|
|
402
307
|
export type AddPluginToPageToolInput = z.infer<typeof addPluginToPageToolInput>;
|
|
403
308
|
export type ListContentInstancesToolInput = z.infer<typeof listContentInstancesToolInput>;
|
|
404
309
|
export type GetContentInstanceToolInput = z.infer<typeof getContentInstanceToolInput>;
|
|
@@ -473,7 +378,7 @@ export const CHAT_MAX_ATTACHMENTS = 4;
|
|
|
473
378
|
* `position` / `field` (#917) — plus `item` / `key` for an entry of a
|
|
474
379
|
* local list field (epic #1107) — name the placement field the text
|
|
475
380
|
* comes from when the preview marked it, so the AI can address it in
|
|
476
|
-
* `
|
|
381
|
+
* `set_page_module_content_many` without inspecting the page first.
|
|
477
382
|
* `listItem` is the `data-caelo-list-item` of the list entry the element
|
|
478
383
|
* sits in (epic #1107, `docs/lists.md` §13): the chip then names that
|
|
479
384
|
* entry — of a shared list, or of a module's own list — not just the
|
|
@@ -676,8 +581,8 @@ export const createTemplateToolInput = z
|
|
|
676
581
|
.object({
|
|
677
582
|
slug: slugInputSchema,
|
|
678
583
|
displayName: z.string().min(1).max(256),
|
|
679
|
-
html: sourceTextArg(z.string().min(1).max(2_000_000)),
|
|
680
|
-
css: sourceTextArg(z.string().max(2_000_000)).default(""),
|
|
584
|
+
html: sourceTextArg(z.string().min(1).max(2_000_000), "html"),
|
|
585
|
+
css: sourceTextArg(z.string().max(2_000_000), "css").default(""),
|
|
681
586
|
layoutId: z.string().uuid().optional(),
|
|
682
587
|
/** Epic #1107 — the page type (slug) this template serves; omit for the site default. */
|
|
683
588
|
pageType: pageTypeSlugSchema.optional(),
|
|
@@ -728,8 +633,8 @@ export const createLayoutToolInput = z
|
|
|
728
633
|
.object({
|
|
729
634
|
slug: slugInputSchema,
|
|
730
635
|
displayName: z.string().min(1).max(200),
|
|
731
|
-
html: sourceTextArg(z.string().min(1).max(50_000)),
|
|
732
|
-
css: sourceTextArg(z.string().max(50_000)).optional(),
|
|
636
|
+
html: sourceTextArg(z.string().min(1).max(50_000), "html"),
|
|
637
|
+
css: sourceTextArg(z.string().max(50_000), "css").optional(),
|
|
733
638
|
blocks: z
|
|
734
639
|
.array(
|
|
735
640
|
z
|
|
@@ -765,8 +670,9 @@ export const duplicatePageToolInput = z
|
|
|
765
670
|
.object({
|
|
766
671
|
sourcePageId: z.string().uuid(),
|
|
767
672
|
newSlug: slugInputSchema,
|
|
768
|
-
newName: z.string().min(1).max(256).optional(),
|
|
769
673
|
newTitle: z.string().min(1).max(256).optional(),
|
|
674
|
+
/** #1142 — the copy's breadcrumb label; omitted = newTitle when given, else the source's. */
|
|
675
|
+
newShortTitle: pageShortTitleSchema.optional(),
|
|
770
676
|
targetTemplateId: z.string().uuid().optional(),
|
|
771
677
|
/** Where the copy sits: a parent page id, or null for the top level.
|
|
772
678
|
* Omitted = next to the source (same parent). */
|
|
@@ -794,32 +700,40 @@ export const repointPageTemplateToolInput = z
|
|
|
794
700
|
})
|
|
795
701
|
.strict();
|
|
796
702
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
703
|
+
/**
|
|
704
|
+
* `move_module` position: `top` | `bottom` | a 0-based index in the target
|
|
705
|
+
* block, plus `up` / `down` (one slot within the module's CURRENT block).
|
|
706
|
+
* Same quote-stripping + numeric-string coercion as `positionInputSchema`.
|
|
707
|
+
*/
|
|
708
|
+
export const movePositionInputSchema = z.preprocess(
|
|
709
|
+
stripSurroundingQuotes,
|
|
710
|
+
z.union([
|
|
711
|
+
z.enum(["top", "bottom", "up", "down"]),
|
|
712
|
+
z.preprocess(
|
|
713
|
+
(v) => (typeof v === "string" && /^\d+$/.test(v) ? Number.parseInt(v, 10) : v),
|
|
714
|
+
z.number().int().min(0).max(1000),
|
|
715
|
+
),
|
|
716
|
+
]),
|
|
717
|
+
);
|
|
806
718
|
|
|
807
|
-
|
|
719
|
+
/**
|
|
720
|
+
* `move_module` — moves a placement to `toBlockName` (omitted = the block it
|
|
721
|
+
* is in now, i.e. a reorder) at `position`. `up` / `down` only make sense
|
|
722
|
+
* inside the current block; the handler refuses them with another block.
|
|
723
|
+
*/
|
|
724
|
+
export const moveModuleToolInput = z
|
|
808
725
|
.object({
|
|
809
726
|
pageId: z.string().uuid(),
|
|
810
727
|
moduleId: z.string().uuid(),
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
* target position within the same block.
|
|
814
|
-
*/
|
|
815
|
-
direction: z.union([z.enum(["up", "down"]), z.number().int().min(0).max(1000)]),
|
|
728
|
+
toBlockName: z.string().min(1).max(80).optional(),
|
|
729
|
+
position: movePositionInputSchema,
|
|
816
730
|
})
|
|
817
731
|
.strict();
|
|
818
732
|
|
|
819
733
|
/**
|
|
820
734
|
* P7 — `find_media`. Searches the media library by alt-text /
|
|
821
735
|
* filename / mime. Each row carries the media UUID (what
|
|
822
|
-
* `set_theme_asset` / `
|
|
736
|
+
* `set_theme_asset` / `set_media_alt_many` require) plus a pre-resolved
|
|
823
737
|
* variant URL. The AI's only media-inventory surface — the `## Media`
|
|
824
738
|
* system-prompt block was removed by the context diet (#300), so every
|
|
825
739
|
* asset lookup ("an image of a sunlit office") goes through this tool.
|
|
@@ -864,7 +778,7 @@ export const regenerateMediaVariantsToolInput = z
|
|
|
864
778
|
export type RegenerateMediaVariantsToolInput = z.infer<typeof regenerateMediaVariantsToolInput>;
|
|
865
779
|
|
|
866
780
|
/**
|
|
867
|
-
* P7 — `
|
|
781
|
+
* P7 — one item of `set_media_alt_many`. AI may improve a11y on an existing asset
|
|
868
782
|
* without a human round-trip (e.g. when the editor uploaded an image
|
|
869
783
|
* with the default alt and the AI reads its content). Updates only
|
|
870
784
|
* the alt field on `media_assets`.
|
|
@@ -894,7 +808,7 @@ export const setNotFoundPageToolInput = z.object({ pageId: z.string().uuid().nul
|
|
|
894
808
|
export type SetNotFoundPageToolInput = z.infer<typeof setNotFoundPageToolInput>;
|
|
895
809
|
|
|
896
810
|
/**
|
|
897
|
-
* 0181 — `
|
|
811
|
+
* 0181 — one item of `set_media_source_many`. AI records an asset's provenance (source
|
|
898
812
|
* kind + origin detail) and licence when known. All fields but the
|
|
899
813
|
* assetId are optional; the op COALESCEs so omitted fields stay
|
|
900
814
|
* unchanged.
|
|
@@ -910,7 +824,7 @@ export const setMediaSourceToolInput = z
|
|
|
910
824
|
export type SetMediaSourceToolInput = z.infer<typeof setMediaSourceToolInput>;
|
|
911
825
|
|
|
912
826
|
/**
|
|
913
|
-
* P8 — `
|
|
827
|
+
* P8 — one item of `set_page_seo_many`. Manual / panel writes to the per-page SEO
|
|
914
828
|
* sidecar. AI calls this only on explicit user intent
|
|
915
829
|
* ("set the home meta description to ..."). Doesn't bump fingerprints
|
|
916
830
|
* (autofilled_at / optimized_at).
|
|
@@ -931,7 +845,7 @@ export const setPageSeoToolInput = z
|
|
|
931
845
|
export type SetPageSeoToolInput = z.infer<typeof setPageSeoToolInput>;
|
|
932
846
|
|
|
933
847
|
/**
|
|
934
|
-
* P8 —
|
|
848
|
+
* P8 — one item of `autofill_page_seo_many`.
|
|
935
849
|
* Fill-once. Refuses when the page's SEO was already autofilled. The AI
|
|
936
850
|
* calls it before a page's first Stage; staging and production builds
|
|
937
851
|
* refuse pages without a meta description (deploy/missing-seo.ts).
|
|
@@ -945,22 +859,6 @@ export const autofillPageSeoToolInput = z
|
|
|
945
859
|
.strict();
|
|
946
860
|
export type AutofillPageSeoToolInput = z.infer<typeof autofillPageSeoToolInput>;
|
|
947
861
|
|
|
948
|
-
/**
|
|
949
|
-
* P8 — `optimize_page_seo`. Explicit re-optimization. Always allowed.
|
|
950
|
-
* Takes optional context (keyword analysis / intent shifts / branding
|
|
951
|
-
* changes). The AI can call this across N pages in one chat turn —
|
|
952
|
-
* the resulting changes batch into one Publish-pill confirm.
|
|
953
|
-
*/
|
|
954
|
-
export const optimizePageSeoToolInput = z
|
|
955
|
-
.object({
|
|
956
|
-
pageId: z.string().uuid(),
|
|
957
|
-
metaDescription: z.string().min(1).max(320),
|
|
958
|
-
ogImageAssetId: z.string().uuid().nullable().optional(),
|
|
959
|
-
context: z.string().max(4000).optional(),
|
|
960
|
-
})
|
|
961
|
-
.strict();
|
|
962
|
-
export type OptimizePageSeoToolInput = z.infer<typeof optimizePageSeoToolInput>;
|
|
963
|
-
|
|
964
862
|
/**
|
|
965
863
|
* P8 AI-first review pass — bulk variants. Per CLAUDE.md §11, bulk
|
|
966
864
|
* tools save round-trips when the AI knows it has N changes to make
|
|
@@ -980,7 +878,7 @@ export type FindRedirectsToolInput = z.infer<typeof findRedirectsToolInput>;
|
|
|
980
878
|
|
|
981
879
|
export const bulkCreateRedirectsToolInput = z
|
|
982
880
|
.object({
|
|
983
|
-
|
|
881
|
+
items: z
|
|
984
882
|
.array(
|
|
985
883
|
z
|
|
986
884
|
.object({
|
|
@@ -1032,7 +930,7 @@ export type BulkDeleteRedirectsToolInput = z.infer<typeof bulkDeleteRedirectsToo
|
|
|
1032
930
|
|
|
1033
931
|
export const bulkOptimizeSeoToolInput = z
|
|
1034
932
|
.object({
|
|
1035
|
-
|
|
933
|
+
items: z
|
|
1036
934
|
.array(
|
|
1037
935
|
z
|
|
1038
936
|
.object({
|
|
@@ -1049,7 +947,6 @@ export const bulkOptimizeSeoToolInput = z
|
|
|
1049
947
|
.strict();
|
|
1050
948
|
export type BulkOptimizeSeoToolInput = z.infer<typeof bulkOptimizeSeoToolInput>;
|
|
1051
949
|
|
|
1052
|
-
export type EditModuleToolInput = z.infer<typeof editModuleToolInput>;
|
|
1053
950
|
export type SiteMemoryProposeToolInput = z.infer<typeof siteMemoryProposeToolInput>;
|
|
1054
951
|
export type CreateTemplateToolInput = z.infer<typeof createTemplateToolInput>;
|
|
1055
952
|
// v0.10.22 — `UpdateThemeToolInput` removed alongside `update_theme` tool.
|
|
@@ -1067,7 +964,6 @@ export type SetSiteDefaultsToolInput = z.infer<typeof setSiteDefaultsToolInput>;
|
|
|
1067
964
|
export type DuplicatePageToolInput = z.infer<typeof duplicatePageToolInput>;
|
|
1068
965
|
export type RepointPageTemplateToolInput = z.infer<typeof repointPageTemplateToolInput>;
|
|
1069
966
|
export type MoveModuleToolInput = z.infer<typeof moveModuleToolInput>;
|
|
1070
|
-
export type ReorderModuleToolInput = z.infer<typeof reorderModuleToolInput>;
|
|
1071
967
|
// v0.10.22 — `SetNavMenuToolInput` removed alongside `set_nav_menu` tool.
|
|
1072
968
|
|
|
1073
969
|
/**
|
package/src/bug-intake.test.ts
CHANGED
|
@@ -32,7 +32,7 @@ describe("computeBugFingerprint", () => {
|
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it("keeps different tools and different quoted field names apart", async () => {
|
|
35
|
-
expect(await auto("boom", "build_page")).not.toBe(await auto("boom", "
|
|
35
|
+
expect(await auto("boom", "build_page")).not.toBe(await auto("boom", "update_modules_many"));
|
|
36
36
|
expect(await auto('Invalid input at "page.slug"')).not.toBe(
|
|
37
37
|
await auto('Invalid input at "page.title"'),
|
|
38
38
|
);
|
package/src/build-page.ts
CHANGED
|
@@ -43,10 +43,10 @@ const contentValuesSchema = z.record(z.string(), z.unknown());
|
|
|
43
43
|
* existing content paths so build_page adds NO new semantics, only batching:
|
|
44
44
|
*
|
|
45
45
|
* - `inline` → mint a private (unsynced) content_instance carrying
|
|
46
|
-
* `values`, exactly what `
|
|
46
|
+
* `values`, exactly what `set_page_module_content_many` produces for a
|
|
47
47
|
* fresh placement.
|
|
48
48
|
* - `shared` → mint a reusable content_instance (purpose required —
|
|
49
|
-
* same decision-support contract as `
|
|
49
|
+
* same decision-support contract as `create_content_instances`) and
|
|
50
50
|
* bind this placement to it, `synced` by default.
|
|
51
51
|
* - `existing` → bind an already-minted content_instance (reuse-first
|
|
52
52
|
* per CLAUDE.md §1A) — the batched form of `set_placement_content`.
|
|
@@ -94,7 +94,7 @@ export type BuildPageContent = z.infer<typeof buildPageContentSchema>;
|
|
|
94
94
|
* extra authoring keys are TOLERATED (§1A/§11 — a placement that can
|
|
95
95
|
* succeed must not fail over an extra field) but NOT applied; the op
|
|
96
96
|
* surfaces an info naming any carried field whose value differs from the
|
|
97
|
-
* module's stored one, pointing at
|
|
97
|
+
* module's stored one, pointing at update_modules_many (§2 — no silent drop).
|
|
98
98
|
*
|
|
99
99
|
* The element-level superRefine reports issues WITH the element's array
|
|
100
100
|
* index in the Zod path, so a mode error fails as `modules[3]: …`.
|
|
@@ -159,9 +159,9 @@ export const buildPageModuleSchema = z
|
|
|
159
159
|
description: z.string().max(1000).optional(),
|
|
160
160
|
kind: moduleKindSchema.optional(),
|
|
161
161
|
type: slugSchema.optional(),
|
|
162
|
-
html: sourceTextArg(z.string().min(1).max(MODULE_HTML_MAX)).optional(),
|
|
163
|
-
css: sourceTextArg(z.string().max(MODULE_CSS_MAX)).optional(),
|
|
164
|
-
js: sourceTextArg(z.string().max(MODULE_JS_MAX)).optional(),
|
|
162
|
+
html: sourceTextArg(z.string().min(1).max(MODULE_HTML_MAX), "html").optional(),
|
|
163
|
+
css: sourceTextArg(z.string().max(MODULE_CSS_MAX), "css").optional(),
|
|
164
|
+
js: sourceTextArg(z.string().max(MODULE_JS_MAX), "js").optional(),
|
|
165
165
|
fields: z.array(moduleFieldSchema).max(64).optional(),
|
|
166
166
|
/** issue #164 slice 2 — opt-in mechanical token binding (tool layer). */
|
|
167
167
|
bindThemeLiterals: z.boolean().optional(),
|
|
@@ -192,7 +192,7 @@ export const buildPageModuleSchema = z
|
|
|
192
192
|
// (build_page places; it does not re-author a shared module). To avoid a
|
|
193
193
|
// SILENT dropped change (§2), the HANDLER surfaces an INFO in the result
|
|
194
194
|
// naming any ignored authoring field whose value DIFFERS from the module's
|
|
195
|
-
// stored one, and points at
|
|
195
|
+
// stored one, and points at update_modules_many. The schema can't do that check (no
|
|
196
196
|
// DB), so it just allows the entry through.
|
|
197
197
|
if (entry.moduleId !== undefined) return;
|
|
198
198
|
if (entry.displayName === undefined || entry.html === undefined) {
|
|
@@ -220,7 +220,7 @@ export const buildPageTargetSchema = z
|
|
|
220
220
|
pageId: z.string().uuid().optional(),
|
|
221
221
|
slug: slugSchema.optional(),
|
|
222
222
|
title: z.string().min(1).max(256).optional(),
|
|
223
|
-
|
|
223
|
+
shortTitle: pageCreateSchema.shape.shortTitle,
|
|
224
224
|
templateId: z.string().uuid().optional(),
|
|
225
225
|
status: pageStatusSchema.optional(),
|
|
226
226
|
/**
|
|
@@ -268,7 +268,7 @@ export const buildPageTargetSchema = z
|
|
|
268
268
|
[
|
|
269
269
|
"slug",
|
|
270
270
|
"title",
|
|
271
|
-
"
|
|
271
|
+
"shortTitle",
|
|
272
272
|
"templateId",
|
|
273
273
|
"status",
|
|
274
274
|
"parentPageId",
|
package/src/content.test.ts
CHANGED
|
@@ -91,7 +91,32 @@ describe("moduleCreateSchema", () => {
|
|
|
91
91
|
expect(moduleCreateSchema.safeParse({ ...base, js: 'xml += "]]>";' }).success).toBe(true);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
// e2e-livedit run 40532297620 — html / css whose line breaks arrived as
|
|
95
|
+
// literal `\n` sequences rendered `\n` text and broke the CSS. AI tool
|
|
96
|
+
// arguments are decoded first (`decodeEscapedLineBreaks`); every other
|
|
97
|
+
// writer is refused with the fix.
|
|
98
|
+
it("rejects html and css whose line breaks are escaped, with the fix", () => {
|
|
99
|
+
const base = { slug: "site-footer", displayName: "Site footer", html: "<footer></footer>" };
|
|
100
|
+
const html = moduleCreateSchema.safeParse({
|
|
101
|
+
...base,
|
|
102
|
+
html: '<footer class="f">\\n <nav>\\n <a href="/">Home</a>\\n </nav>\\n</footer>',
|
|
103
|
+
});
|
|
104
|
+
expect(html.success).toBe(false);
|
|
105
|
+
if (!html.success) {
|
|
106
|
+
expect(html.error.issues.map((i) => i.message).join(" ")).toContain("real line breaks");
|
|
107
|
+
}
|
|
108
|
+
const css = moduleUpdateSchema.safeParse({
|
|
109
|
+
moduleId: "8c2f2f2a-0000-4000-8000-000000000432",
|
|
110
|
+
css: ".f {\\n padding: 2rem;\\n}\\n.f a {\\n color: inherit;\\n}",
|
|
111
|
+
});
|
|
112
|
+
expect(css.success).toBe(false);
|
|
113
|
+
// One escaped newline inside a CSS string on a single line is not the pattern.
|
|
114
|
+
expect(
|
|
115
|
+
moduleCreateSchema.safeParse({ ...base, css: '.f::after { content: "\\n " }' }).success,
|
|
116
|
+
).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("rejects CDATA on the update path too (update_modules_many / edit_content funnel)", () => {
|
|
95
120
|
const r = moduleUpdateSchema.safeParse({
|
|
96
121
|
moduleId: "8c2f2f2a-0000-4000-8000-000000000432",
|
|
97
122
|
css: "/*<![CDATA[*/ .x{} /*]]>*/",
|
package/src/content.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { z } from "zod";
|
|
14
|
+
import { hasEscapedLineBreaks } from "./escaped-line-breaks.js";
|
|
14
15
|
import { listItemInputSchema } from "./lists/items.js";
|
|
15
16
|
import { LIST_MAX_ITEMS, sourceDefSchema } from "./lists/list-type.js";
|
|
16
17
|
import { LIST_REF_RE } from "./lists/refs.js";
|
|
@@ -126,14 +127,34 @@ export function htmlEscapedMarkupMessage(field: string, value: string): string |
|
|
|
126
127
|
return `${field} is HTML-escaped (<…> instead of <…>) — send the markup itself, unescaped.`;
|
|
127
128
|
}
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* See {@link jsonEncodedCodeMessage}: source whose line breaks arrived as
|
|
132
|
+
* literal `\n` sequences (`hasEscapedLineBreaks`) — the page shows the
|
|
133
|
+
* `\n` text and the CSS rules after it break. AI tool arguments are decoded
|
|
134
|
+
* first (`decodeEscapedLineBreaks`); this guards every other writer. Module
|
|
135
|
+
* js is checked by parsing it (admin-core `module-js-syntax.ts`), which
|
|
136
|
+
* catches this and every other syntax error.
|
|
137
|
+
*/
|
|
138
|
+
export function escapedLineBreaksMessage(field: "html" | "css", value: string): string | null {
|
|
139
|
+
if (!hasEscapedLineBreaks(value, field)) return null;
|
|
140
|
+
return `${field} has its line breaks escaped (literal \\n sequences, no real line break) — send the ${field} with real line breaks.`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const noCorruptMarkup = (field: "html") => (value: string, ctx: z.RefinementCtx) => {
|
|
144
|
+
const message =
|
|
145
|
+
jsonEncodedCodeMessage(field, value) ??
|
|
146
|
+
htmlEscapedMarkupMessage(field, value) ??
|
|
147
|
+
escapedLineBreaksMessage(field, value);
|
|
131
148
|
if (message) ctx.addIssue({ code: "custom", message });
|
|
132
149
|
};
|
|
133
150
|
const noJsonEncodedCode = (field: string) => (value: string, ctx: z.RefinementCtx) => {
|
|
134
151
|
const message = jsonEncodedCodeMessage(field, value);
|
|
135
152
|
if (message) ctx.addIssue({ code: "custom", message });
|
|
136
153
|
};
|
|
154
|
+
const noCorruptCss = (value: string, ctx: z.RefinementCtx) => {
|
|
155
|
+
const message = jsonEncodedCodeMessage("css", value) ?? escapedLineBreaksMessage("css", value);
|
|
156
|
+
if (message) ctx.addIssue({ code: "custom", message });
|
|
157
|
+
};
|
|
137
158
|
|
|
138
159
|
const moduleHtml = z
|
|
139
160
|
.string()
|
|
@@ -168,7 +189,7 @@ const moduleCss = z
|
|
|
168
189
|
.string()
|
|
169
190
|
.max(MODULE_CSS_MAX, `css exceeds ${MODULE_CSS_MAX} bytes`)
|
|
170
191
|
.refine(noCdata, cdataMessage("css"))
|
|
171
|
-
.superRefine(
|
|
192
|
+
.superRefine(noCorruptCss);
|
|
172
193
|
const moduleJs = z
|
|
173
194
|
.string()
|
|
174
195
|
.max(MODULE_JS_MAX, `js exceeds ${MODULE_JS_MAX} bytes`)
|
|
@@ -276,7 +297,7 @@ const moduleFieldModuleSchema = z
|
|
|
276
297
|
* stable `modules.type` class, e.g. `button` — NOT the unique
|
|
277
298
|
* `modules.slug` like `button-mpqxq3ch`). When absent, any module is
|
|
278
299
|
* permitted. The op-layer validator enforces the whitelist against
|
|
279
|
-
* the referenced module's `type` at `
|
|
300
|
+
* the referenced module's `type` at `content_instances.set_values`
|
|
280
301
|
* time. v0.12.3 (issue #106) renamed this from `allowedModuleSlugs`:
|
|
281
302
|
* an exact-slug whitelist could never match an AI-minted module
|
|
282
303
|
* because every minted slug carries a uniqueness suffix.
|
|
@@ -491,7 +512,7 @@ export type ModuleKind = (typeof MODULE_KINDS)[number];
|
|
|
491
512
|
/**
|
|
492
513
|
* v0.12.0 — description is `.default("")` at the Zod boundary so the
|
|
493
514
|
* 82+ legacy callers in tests/seed scripts keep working unchanged.
|
|
494
|
-
* AI tool descriptions (
|
|
515
|
+
* AI tool descriptions (add_module / build_page / update_modules_many) require the AI
|
|
495
516
|
* to supply a real description explicitly — see CLAUDE.md §1A.
|
|
496
517
|
*/
|
|
497
518
|
const moduleDescription = z.string().max(1000);
|
|
@@ -627,6 +648,15 @@ export const templateBlocksSetSchema = z
|
|
|
627
648
|
|
|
628
649
|
export const pageStatusSchema = z.enum(["draft", "published"]);
|
|
629
650
|
|
|
651
|
+
/** `pages.short_title` (#1142): a crumb-sized public label, never blank. */
|
|
652
|
+
export const pageShortTitleSchema = z
|
|
653
|
+
.string()
|
|
654
|
+
.trim()
|
|
655
|
+
.min(1, "the short title is the page's breadcrumb label and cannot be blank")
|
|
656
|
+
// Same bound as the title it is backfilled and defaulted from, so every
|
|
657
|
+
// stored page stays editable; keeping it crumb-sized is guidance.
|
|
658
|
+
.max(256);
|
|
659
|
+
|
|
630
660
|
/**
|
|
631
661
|
* Page payloads are `.strict()` — passing an `html` field is rejected by Zod
|
|
632
662
|
* before the handler runs. This is the §3.1 "no raw HTML on pages" invariant
|
|
@@ -636,9 +666,13 @@ export const pageStatusSchema = z.enum(["draft", "published"]);
|
|
|
636
666
|
export const pageCreateSchema = z
|
|
637
667
|
.object({
|
|
638
668
|
slug: slugSchema,
|
|
639
|
-
/** P6.7.5 — internal editor label. Defaults to title if omitted. */
|
|
640
|
-
name: z.string().min(1).max(256).optional(),
|
|
641
669
|
title: z.string().min(1).max(256),
|
|
670
|
+
/**
|
|
671
|
+
* #1142 — the page's public label in breadcrumbs, navigation and the
|
|
672
|
+
* page picker ("Pricing" for "Pricing for teams of every size").
|
|
673
|
+
* Omitted = the create-time resolver stores `title` (stored data).
|
|
674
|
+
*/
|
|
675
|
+
shortTitle: pageShortTitleSchema.optional(),
|
|
642
676
|
/**
|
|
643
677
|
* P6.7.6 — optional. When omitted, the handler resolves to
|
|
644
678
|
* `site_defaults.default_template_id` at create time. Stored data,
|
|
@@ -703,9 +737,9 @@ export const pageUpdateSchema = z
|
|
|
703
737
|
* Routes that load the page first should always pass it back.
|
|
704
738
|
*/
|
|
705
739
|
expectedVersion: z.number().int().nonnegative().optional(),
|
|
706
|
-
/** P6.7.5 — three independently-editable identifiers. */
|
|
707
|
-
name: z.string().min(1).max(256).optional(),
|
|
708
740
|
title: z.string().min(1).max(256).optional(),
|
|
741
|
+
/** #1142 — the breadcrumb / navigation label; see {@link pageCreateSchema}. */
|
|
742
|
+
shortTitle: pageShortTitleSchema.optional(),
|
|
709
743
|
slug: slugSchema.optional(),
|
|
710
744
|
templateId: z.string().uuid().optional(),
|
|
711
745
|
status: pageStatusSchema.optional(),
|
|
@@ -780,10 +814,10 @@ const contentValuesSchema = z.record(z.string(), z.unknown());
|
|
|
780
814
|
|
|
781
815
|
/**
|
|
782
816
|
* v0.12.0 — why this row exists as a shared/reusable instance.
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
817
|
+
* Shown by the AI's list_content_instances tool so the AI can decide
|
|
818
|
+
* *reuse the synced row* vs *fork to unsynced* vs *mint new* without
|
|
819
|
+
* asking the operator. Required by the AI's
|
|
820
|
+
* create_content_instances tool description; legacy callers may pass
|
|
787
821
|
* null (the migration-0093 unsynced rows have purpose=NULL).
|
|
788
822
|
* See CLAUDE.md §1A.
|
|
789
823
|
*/
|
package/src/css-scope-scan.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* that is not tied to the module bleeds into every other module on the
|
|
8
8
|
* page: an `h1 {}` in the hero restyles the pricing table's heading, a
|
|
9
9
|
* `.card {}` in one module restyles another module's cards. The
|
|
10
|
-
* convention (
|
|
10
|
+
* convention (update_modules_many description, design-quality skill) is to nest
|
|
11
11
|
* module CSS under the module's own root class; this scanner is the
|
|
12
12
|
* mechanical write-time check that rides the authoring tool's result,
|
|
13
13
|
* so the AI fixes the selector in the same turn. A warning, not a gate:
|