@caelo-cms/shared 0.10.22 → 0.10.24
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 +289 -273
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +342 -323
- package/dist/ai-tools.js.map +1 -1
- package/dist/auth-forms.d.ts.map +1 -1
- package/dist/auth-forms.js +4 -1
- package/dist/auth-forms.js.map +1 -1
- package/dist/base-css.d.ts +35 -0
- package/dist/base-css.d.ts.map +1 -0
- package/dist/base-css.js +40 -0
- package/dist/base-css.js.map +1 -0
- package/dist/build-page.d.ts +330 -0
- package/dist/build-page.d.ts.map +1 -0
- package/dist/build-page.js +282 -0
- package/dist/build-page.js.map +1 -0
- package/dist/content.d.ts +322 -9
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +354 -11
- package/dist/content.js.map +1 -1
- package/dist/css-gradient-scan.d.ts +14 -0
- package/dist/css-gradient-scan.d.ts.map +1 -0
- package/dist/css-gradient-scan.js +81 -0
- package/dist/css-gradient-scan.js.map +1 -0
- package/dist/css-var-scan.d.ts +56 -0
- package/dist/css-var-scan.d.ts.map +1 -0
- package/dist/css-var-scan.js +97 -0
- package/dist/css-var-scan.js.map +1 -0
- package/dist/design-draft-shell.d.ts +21 -0
- package/dist/design-draft-shell.d.ts.map +1 -0
- package/dist/design-draft-shell.js +81 -0
- package/dist/design-draft-shell.js.map +1 -0
- package/dist/design-manifest.d.ts +36 -0
- package/dist/design-manifest.d.ts.map +1 -0
- package/dist/design-manifest.js +90 -0
- package/dist/design-manifest.js.map +1 -0
- package/dist/fonts.d.ts +89 -0
- package/dist/fonts.d.ts.map +1 -0
- package/dist/fonts.js +241 -0
- package/dist/fonts.js.map +1 -0
- package/dist/genesis-inventory.d.ts +32 -0
- package/dist/genesis-inventory.d.ts.map +1 -0
- package/dist/genesis-inventory.js +186 -0
- package/dist/genesis-inventory.js.map +1 -0
- package/dist/genesis.d.ts +102 -0
- package/dist/genesis.d.ts.map +1 -0
- package/dist/genesis.js +145 -0
- package/dist/genesis.js.map +1 -0
- package/dist/i18n.d.ts +29 -36
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +53 -128
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.d.ts +23 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +44 -0
- package/dist/interactions.js.map +1 -0
- package/dist/media.d.ts +101 -16
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +126 -15
- package/dist/media.js.map +1 -1
- package/dist/page-log.d.ts +94 -0
- package/dist/page-log.d.ts.map +1 -0
- package/dist/page-log.js +111 -0
- package/dist/page-log.js.map +1 -0
- package/dist/preview-compose.d.ts +85 -12
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +157 -67
- package/dist/preview-compose.js.map +1 -1
- package/dist/proposal-status.d.ts +40 -0
- package/dist/proposal-status.d.ts.map +1 -0
- package/dist/proposal-status.js +34 -0
- package/dist/proposal-status.js.map +1 -0
- package/dist/responsive-images.d.ts +64 -0
- package/dist/responsive-images.d.ts.map +1 -0
- package/dist/responsive-images.js +98 -0
- package/dist/responsive-images.js.map +1 -0
- package/dist/safe-keys.d.ts +9 -0
- package/dist/safe-keys.d.ts.map +1 -0
- package/dist/safe-keys.js +20 -0
- package/dist/safe-keys.js.map +1 -0
- package/dist/seo.d.ts +11 -16
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +7 -23
- package/dist/seo.js.map +1 -1
- package/dist/skills.d.ts +14 -68
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +19 -113
- package/dist/skills.js.map +1 -1
- package/dist/strip-cdata.d.ts +7 -0
- package/dist/strip-cdata.d.ts.map +1 -0
- package/dist/strip-cdata.js +48 -0
- package/dist/strip-cdata.js.map +1 -0
- package/dist/structured-sets.d.ts +6 -52
- package/dist/structured-sets.d.ts.map +1 -1
- package/dist/structured-sets.js +6 -68
- package/dist/structured-sets.js.map +1 -1
- package/dist/subagents.d.ts +105 -3
- package/dist/subagents.d.ts.map +1 -1
- package/dist/subagents.js +224 -41
- package/dist/subagents.js.map +1 -1
- package/dist/template-engine.d.ts +85 -0
- package/dist/template-engine.d.ts.map +1 -0
- package/dist/template-engine.js +403 -0
- package/dist/template-engine.js.map +1 -0
- package/dist/theme-importers/auto-detect.d.ts +26 -0
- package/dist/theme-importers/auto-detect.d.ts.map +1 -0
- package/dist/theme-importers/auto-detect.js +42 -0
- package/dist/theme-importers/auto-detect.js.map +1 -0
- package/dist/theme-importers/css-comments.d.ts +12 -0
- package/dist/theme-importers/css-comments.d.ts.map +1 -0
- package/dist/theme-importers/css-comments.js +15 -0
- package/dist/theme-importers/css-comments.js.map +1 -0
- package/dist/theme-importers/dtcg.d.ts +46 -0
- package/dist/theme-importers/dtcg.d.ts.map +1 -0
- package/dist/theme-importers/dtcg.js +111 -0
- package/dist/theme-importers/dtcg.js.map +1 -0
- package/dist/theme-importers/loose.d.ts +3 -0
- package/dist/theme-importers/loose.d.ts.map +1 -0
- package/dist/theme-importers/loose.js +76 -0
- package/dist/theme-importers/loose.js.map +1 -0
- package/dist/theme-importers/shadcn.d.ts +24 -0
- package/dist/theme-importers/shadcn.d.ts.map +1 -0
- package/dist/theme-importers/shadcn.js +135 -0
- package/dist/theme-importers/shadcn.js.map +1 -0
- package/dist/theme-importers/style-dictionary.d.ts +17 -0
- package/dist/theme-importers/style-dictionary.d.ts.map +1 -0
- package/dist/theme-importers/style-dictionary.js +125 -0
- package/dist/theme-importers/style-dictionary.js.map +1 -0
- package/dist/theme-importers/tailwind.d.ts +3 -0
- package/dist/theme-importers/tailwind.d.ts.map +1 -0
- package/dist/theme-importers/tailwind.js +218 -0
- package/dist/theme-importers/tailwind.js.map +1 -0
- package/dist/theme-literal-binding.d.ts +37 -0
- package/dist/theme-literal-binding.d.ts.map +1 -0
- package/dist/theme-literal-binding.js +138 -0
- package/dist/theme-literal-binding.js.map +1 -0
- package/dist/theme-normalize.d.ts +31 -0
- package/dist/theme-normalize.d.ts.map +1 -0
- package/dist/theme-normalize.js +587 -0
- package/dist/theme-normalize.js.map +1 -0
- package/dist/theme-ramp.d.ts +55 -0
- package/dist/theme-ramp.d.ts.map +1 -0
- package/dist/theme-ramp.js +149 -0
- package/dist/theme-ramp.js.map +1 -0
- package/dist/theme-render.d.ts +105 -0
- package/dist/theme-render.d.ts.map +1 -0
- package/dist/theme-render.js +441 -0
- package/dist/theme-render.js.map +1 -0
- package/dist/themes-errors.d.ts +109 -0
- package/dist/themes-errors.d.ts.map +1 -0
- package/dist/themes-errors.js +170 -0
- package/dist/themes-errors.js.map +1 -0
- package/dist/themes.d.ts +343 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +697 -0
- package/dist/themes.js.map +1 -0
- package/dist/version.d.ts +7 -4
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -3
- package/dist/version.js.map +1 -1
- package/package.json +10 -2
- package/src/__tests__/redos-hardening.test.ts +160 -0
- package/src/ai-tools-add-module-modes.test.ts +106 -0
- package/src/ai-tools-position.test.ts +134 -0
- package/src/ai-tools.test.ts +81 -0
- package/src/ai-tools.ts +1105 -0
- package/src/auth-forms.ts +36 -0
- package/src/base-css.ts +42 -0
- package/src/build-page.test.ts +228 -0
- package/src/build-page.ts +319 -0
- package/src/cap-failures.ts +67 -0
- package/src/content.test.ts +170 -0
- package/src/content.ts +620 -0
- package/src/context.ts +43 -0
- package/src/css-gradient-scan.ts +88 -0
- package/src/css-var-scan.test.ts +96 -0
- package/src/css-var-scan.ts +144 -0
- package/src/derive-module-type.test.ts +80 -0
- package/src/design-draft-shell.test.ts +85 -0
- package/src/design-draft-shell.ts +109 -0
- package/src/design-manifest.ts +93 -0
- package/src/fonts.test.ts +157 -0
- package/src/fonts.ts +296 -0
- package/src/genesis-inventory.test.ts +86 -0
- package/src/genesis-inventory.ts +215 -0
- package/src/genesis-sanitize.test.ts +35 -0
- package/src/genesis.ts +158 -0
- package/src/i18n.test.ts +58 -0
- package/src/i18n.ts +91 -0
- package/src/index.test.ts +10 -0
- package/src/index.ts +59 -0
- package/src/interactions.ts +48 -0
- package/src/logger.ts +147 -0
- package/src/media.test.ts +160 -0
- package/src/media.ts +355 -0
- package/src/page-log.test.ts +163 -0
- package/src/page-log.ts +124 -0
- package/src/preview-compose.test.ts +637 -0
- package/src/preview-compose.ts +656 -0
- package/src/preview-scanner.test.ts +96 -0
- package/src/preview-scanner.ts +214 -0
- package/src/proposal-status.test.ts +69 -0
- package/src/proposal-status.ts +40 -0
- package/src/responsive-images.test.ts +104 -0
- package/src/responsive-images.ts +151 -0
- package/src/result.ts +29 -0
- package/src/safe-keys.ts +21 -0
- package/src/seo.test.ts +194 -0
- package/src/seo.ts +233 -0
- package/src/skills.ts +48 -0
- package/src/snapshots.test.ts +80 -0
- package/src/snapshots.ts +81 -0
- package/src/strip-cdata.test.ts +41 -0
- package/src/strip-cdata.ts +50 -0
- package/src/structured-sets.ts +114 -0
- package/src/subagents.test.ts +262 -0
- package/src/subagents.ts +432 -0
- package/src/template-engine.test.ts +379 -0
- package/src/template-engine.ts +520 -0
- package/src/theme-gradient.test.ts +92 -0
- package/src/theme-importers/__tests__/proto-pollution.test.ts +54 -0
- package/src/theme-importers/auto-detect.ts +84 -0
- package/src/theme-importers/css-comments.ts +15 -0
- package/src/theme-importers/dtcg.ts +106 -0
- package/src/theme-importers/loose.ts +76 -0
- package/src/theme-importers/shadcn.ts +133 -0
- package/src/theme-importers/style-dictionary.ts +125 -0
- package/src/theme-importers/tailwind.ts +217 -0
- package/src/theme-literal-binding.test.ts +71 -0
- package/src/theme-literal-binding.ts +159 -0
- package/src/theme-motion.test.ts +115 -0
- package/src/theme-normalize-envelope.test.ts +43 -0
- package/src/theme-normalize-gradient.test.ts +135 -0
- package/src/theme-normalize.ts +661 -0
- package/src/theme-ramp.ts +187 -0
- package/src/theme-render-sanitize.test.ts +45 -0
- package/src/theme-render.test.ts +119 -0
- package/src/theme-render.ts +487 -0
- package/src/theme-shadow.test.ts +56 -0
- package/src/themes-errors.ts +199 -0
- package/src/themes.ts +842 -0
- package/src/version.ts +66 -0
- package/dist/translation.d.ts +0 -127
- package/dist/translation.d.ts.map +0 -1
- package/dist/translation.js +0 -208
- package/dist/translation.js.map +0 -1
package/src/content.ts
ADDED
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Zod schemas for the Phase 3 content layer. Lives in @caelo-cms/shared so the
|
|
5
|
+
* Validator (Query API ops) and the SvelteKit form actions can both import
|
|
6
|
+
* from a single source.
|
|
7
|
+
*
|
|
8
|
+
* The Page schemas are `.strict()` — Zod rejects any extra key, which is how
|
|
9
|
+
* the §3.1 "no raw HTML on pages" invariant is enforced *in code*: a payload
|
|
10
|
+
* trying to set `html` on a page fails Zod parse before any handler runs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
|
|
15
|
+
/** Lowercase slug, hyphenated, 1–64 chars, no leading/trailing hyphen. */
|
|
16
|
+
export const slugSchema = z
|
|
17
|
+
.string()
|
|
18
|
+
.regex(
|
|
19
|
+
/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/,
|
|
20
|
+
"slug must be lowercase letters, digits, or hyphens (1–64 chars, no leading/trailing hyphen)",
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* v0.12.3 (issue #106) — derive a module's stable `type` from its
|
|
25
|
+
* displayName. The `type` is the semantic class of a module (`button`,
|
|
26
|
+
* `hero`, `pricing-card`) shared by every instance of that class — it is
|
|
27
|
+
* what a parent module's `allowedModuleTypes` whitelist matches against.
|
|
28
|
+
*
|
|
29
|
+
* This is the slug *base* WITHOUT the `-<timestamp>` uniqueness suffix
|
|
30
|
+
* that `slugify()` appends: a module's `slug` is its unique row identity
|
|
31
|
+
* (`button-mpqxq3ch`), its `type` is the reusable class (`button`). The
|
|
32
|
+
* tool-side `slugify` composes as `deriveModuleType(name) + "-" + suffix`
|
|
33
|
+
* so the two can never diverge.
|
|
34
|
+
*
|
|
35
|
+
* Mirrors `slugify`'s normalization (lowercase, non-alphanumerics → single
|
|
36
|
+
* hyphen, trim hyphens, 40-char cap, `"module"` stem fallback) so a
|
|
37
|
+
* round-trip is predictable.
|
|
38
|
+
*/
|
|
39
|
+
export function deriveModuleType(displayName: string): string {
|
|
40
|
+
const base = displayName
|
|
41
|
+
.toLowerCase()
|
|
42
|
+
// Collapse every run of non-alphanumerics to a SINGLE hyphen first, so the
|
|
43
|
+
// string can never contain consecutive hyphens after this point.
|
|
44
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
45
|
+
// Cap BEFORE trimming the edge hyphen — trimming first then slicing could
|
|
46
|
+
// truncate mid-token and leave a trailing hyphen (e.g. the 40th char is a
|
|
47
|
+
// hyphen), which would make `slug = type + "-" + suffix` double-hyphenate.
|
|
48
|
+
.slice(0, 40)
|
|
49
|
+
// Trim a single leading/trailing hyphen. `^-|-$` (not `^-+|-+$`) is both
|
|
50
|
+
// sufficient — the collapse above guarantees no `--` runs — and free of the
|
|
51
|
+
// polynomial backtracking CodeQL flags on `-+$` over uncontrolled input.
|
|
52
|
+
.replace(/^-|-$/g, "");
|
|
53
|
+
return base.length > 0 ? base : "module";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* v0.12.3 (issue #106) — mint a unique module slug from a displayName:
|
|
58
|
+
* the stable `type` base (`deriveModuleType`) + a base36 timestamp
|
|
59
|
+
* suffix. Because the slug is always `type + "-" + suffix`, a module's
|
|
60
|
+
* `type` is guaranteed to be a prefix of its `slug` — the two can never
|
|
61
|
+
* drift. Shared by every module-minting AI tool so the rule lives in one
|
|
62
|
+
* place.
|
|
63
|
+
*
|
|
64
|
+
* @param suffix - override the uniqueness suffix (defaults to a base36
|
|
65
|
+
* timestamp). Pass a stable value in tests for determinism.
|
|
66
|
+
*/
|
|
67
|
+
export function slugifyModuleName(
|
|
68
|
+
displayName: string,
|
|
69
|
+
suffix: string = Date.now().toString(36),
|
|
70
|
+
): string {
|
|
71
|
+
return `${deriveModuleType(displayName)}-${suffix}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* v0.12.3 (issue #106) — slug for a module minted per page-section
|
|
76
|
+
* (`build_page` / legacy compose). Includes the section index so two sections
|
|
77
|
+
* sharing a displayName don't collide on the unique slug constraint.
|
|
78
|
+
*/
|
|
79
|
+
export function slugifyModuleSection(
|
|
80
|
+
displayName: string,
|
|
81
|
+
idx: number,
|
|
82
|
+
suffix: string = Date.now().toString(36),
|
|
83
|
+
): string {
|
|
84
|
+
return `${deriveModuleType(displayName)}-${idx}-${suffix}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** BCP-47 shape: 2-letter language with optional 2-letter region. P9 widens this. */
|
|
88
|
+
export const localeSchema = z
|
|
89
|
+
.string()
|
|
90
|
+
.regex(/^[a-z]{2}(-[A-Z]{2})?$/, "locale must be 'xx' or 'xx-YY' (BCP-47 shape)");
|
|
91
|
+
|
|
92
|
+
const displayNameSchema = z.string().min(1).max(128);
|
|
93
|
+
|
|
94
|
+
/** Caps are arbitrary but cheap — paste-of-binary mistakes get caught. */
|
|
95
|
+
export const MODULE_HTML_MAX = 256 * 1024;
|
|
96
|
+
export const MODULE_CSS_MAX = 128 * 1024;
|
|
97
|
+
export const MODULE_JS_MAX = 256 * 1024;
|
|
98
|
+
export const TEMPLATE_HTML_MAX = 512 * 1024;
|
|
99
|
+
export const TEMPLATE_CSS_MAX = 128 * 1024;
|
|
100
|
+
|
|
101
|
+
const moduleHtml = z.string().max(MODULE_HTML_MAX, `html exceeds ${MODULE_HTML_MAX} bytes`);
|
|
102
|
+
const moduleCss = z.string().max(MODULE_CSS_MAX, `css exceeds ${MODULE_CSS_MAX} bytes`);
|
|
103
|
+
const moduleJs = z.string().max(MODULE_JS_MAX, `js exceeds ${MODULE_JS_MAX} bytes`);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* v0.4.0 — module field schema. Each field declares one substitution slot in
|
|
107
|
+
* the module's templated HTML (`{{fieldName}}`) and the kind of content it
|
|
108
|
+
* holds. Field values live on each page placement (v0.4.0: in
|
|
109
|
+
* `page_module_content.content_values`; v0.12.0 onward: in
|
|
110
|
+
* `content_instances.values`).
|
|
111
|
+
*
|
|
112
|
+
* v0.12.0 — extended to nine kinds. Primitive kinds (text/richtext/url/
|
|
113
|
+
* image/number/boolean/link) substitute via `{{fieldName}}` placeholders.
|
|
114
|
+
* The two new nested kinds reference another module by id + content_instance:
|
|
115
|
+
*
|
|
116
|
+
* - `module` — single nested module reference. HTML slot syntax: `{{>fieldName}}`.
|
|
117
|
+
* Value shape: `{ moduleId, contentInstanceId }`.
|
|
118
|
+
* - `module-list` — ordered array of nested module references. HTML slot
|
|
119
|
+
* syntax: `{{#fieldName}}…inner…{{/fieldName}}` — `inner` renders once
|
|
120
|
+
* per element. Value shape: `Array<{ moduleId, contentInstanceId }>`.
|
|
121
|
+
*
|
|
122
|
+
* Schema is a discriminated union by `kind` so the validator rejects, e.g., a
|
|
123
|
+
* `text` kind that carries `allowedModuleTypes` (which only nested kinds use)
|
|
124
|
+
* or a `module-list` that carries a `default` (nested kinds populate from
|
|
125
|
+
* referenced content_instances, not from defaults).
|
|
126
|
+
*/
|
|
127
|
+
export const MODULE_FIELD_PRIMITIVE_KINDS = [
|
|
128
|
+
"text",
|
|
129
|
+
"richtext",
|
|
130
|
+
"url",
|
|
131
|
+
"image",
|
|
132
|
+
"number",
|
|
133
|
+
"boolean",
|
|
134
|
+
"link",
|
|
135
|
+
] as const;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* v0.12.0 — list-of-primitive field kinds. The grammar mirrors
|
|
139
|
+
* `module-list`'s `{{#field}}{{/field}}` iteration but the elements
|
|
140
|
+
* are primitives (or a fixed {label,href} pair for link-list), not
|
|
141
|
+
* `{moduleId, contentInstanceId}` refs.
|
|
142
|
+
*
|
|
143
|
+
* Use `text-list` for "list of strings" — menu labels, tag chips,
|
|
144
|
+
* bullet points where each item is just text. Inner template
|
|
145
|
+
* references `{{.}}` (Mustache convention) or `{{item}}` to mean
|
|
146
|
+
* the current element.
|
|
147
|
+
*
|
|
148
|
+
* Use `link-list` for "list of links" — primary nav, footer columns,
|
|
149
|
+
* sidebar menus. Each element is `{href, label}`. Inner template
|
|
150
|
+
* uses `{{href}}` + `{{label}}` per iteration.
|
|
151
|
+
*
|
|
152
|
+
* For lists with richer per-item structure (cards with image + title
|
|
153
|
+
* + body + CTA), use `module-list` pointing at a sub-module — that's
|
|
154
|
+
* what nested modules are for.
|
|
155
|
+
*/
|
|
156
|
+
export const MODULE_FIELD_LIST_KINDS = ["text-list", "link-list"] as const;
|
|
157
|
+
|
|
158
|
+
export const MODULE_FIELD_NESTED_KINDS = ["module", "module-list"] as const;
|
|
159
|
+
|
|
160
|
+
/** All eleven v0.12.0 kinds. */
|
|
161
|
+
export const MODULE_FIELD_KINDS = [
|
|
162
|
+
...MODULE_FIELD_PRIMITIVE_KINDS,
|
|
163
|
+
...MODULE_FIELD_LIST_KINDS,
|
|
164
|
+
...MODULE_FIELD_NESTED_KINDS,
|
|
165
|
+
] as const;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Canonical union of every field kind — used by callers that need to
|
|
169
|
+
* type a single `kind` field (e.g. the shared template engine's
|
|
170
|
+
* `TemplateField.kind`, the static-gen field parser). Importing this
|
|
171
|
+
* instead of `string` catches typos like `'tetx-list'` at compile time.
|
|
172
|
+
*/
|
|
173
|
+
export type ModuleFieldKind = (typeof MODULE_FIELD_KINDS)[number];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Field kinds the shared template engine iterates via the
|
|
177
|
+
* `{{#name}}…{{/name}}` Mustache-section operator. Adding a new
|
|
178
|
+
* list-shaped kind to MODULE_FIELD_LIST_KINDS / MODULE_FIELD_NESTED_KINDS
|
|
179
|
+
* automatically extends this set; the engine's section-dispatch branch
|
|
180
|
+
* keys off it (`packages/shared/src/template-engine.ts`).
|
|
181
|
+
*/
|
|
182
|
+
export const MODULE_FIELD_SECTION_KINDS = [...MODULE_FIELD_LIST_KINDS, "module-list"] as const;
|
|
183
|
+
|
|
184
|
+
const moduleFieldName = z.string().regex(/^[a-z][a-z0-9_]{0,63}$/, "name must be snake_case");
|
|
185
|
+
const moduleFieldLabel = z.string().min(1).max(128);
|
|
186
|
+
|
|
187
|
+
const moduleFieldPrimitiveSchema = z
|
|
188
|
+
.object({
|
|
189
|
+
name: moduleFieldName,
|
|
190
|
+
kind: z.enum(MODULE_FIELD_PRIMITIVE_KINDS),
|
|
191
|
+
label: moduleFieldLabel,
|
|
192
|
+
/** Default value used when a placement's content_instance has no override. */
|
|
193
|
+
default: z.unknown().optional(),
|
|
194
|
+
})
|
|
195
|
+
.strict();
|
|
196
|
+
|
|
197
|
+
const moduleFieldModuleSchema = z
|
|
198
|
+
.object({
|
|
199
|
+
name: moduleFieldName,
|
|
200
|
+
kind: z.literal("module"),
|
|
201
|
+
label: moduleFieldLabel,
|
|
202
|
+
/**
|
|
203
|
+
* Optional whitelist of module *types* that may fill this slot (the
|
|
204
|
+
* stable `modules.type` class, e.g. `button` — NOT the unique
|
|
205
|
+
* `modules.slug` like `button-mpqxq3ch`). When absent, any module is
|
|
206
|
+
* permitted. The op-layer validator enforces the whitelist against
|
|
207
|
+
* the referenced module's `type` at `set_content_instance_values`
|
|
208
|
+
* time. v0.12.3 (issue #106) renamed this from `allowedModuleSlugs`:
|
|
209
|
+
* an exact-slug whitelist could never match an AI-minted module
|
|
210
|
+
* because every minted slug carries a uniqueness suffix.
|
|
211
|
+
*/
|
|
212
|
+
allowedModuleTypes: z.array(slugSchema).max(32).optional(),
|
|
213
|
+
})
|
|
214
|
+
.strict();
|
|
215
|
+
|
|
216
|
+
const moduleFieldModuleListSchema = z
|
|
217
|
+
.object({
|
|
218
|
+
name: moduleFieldName,
|
|
219
|
+
kind: z.literal("module-list"),
|
|
220
|
+
label: moduleFieldLabel,
|
|
221
|
+
/** v0.12.3 (issue #106) — whitelist of stable module `type`s (not
|
|
222
|
+
* unique slugs). See `moduleFieldModuleSchema.allowedModuleTypes`. */
|
|
223
|
+
allowedModuleTypes: z.array(slugSchema).max(32).optional(),
|
|
224
|
+
/** Minimum count of elements. Validator enforces at write time. */
|
|
225
|
+
min: z.number().int().nonnegative().optional(),
|
|
226
|
+
/** Maximum count of elements. Validator enforces at write time. */
|
|
227
|
+
max: z.number().int().positive().max(256).optional(),
|
|
228
|
+
})
|
|
229
|
+
.strict();
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* v0.12.0 — list-of-strings field. Inner template uses `{{.}}` (or
|
|
233
|
+
* the alias `{{item}}`) to reference the current element. Default
|
|
234
|
+
* is an optional array of strings rendered when the placement's
|
|
235
|
+
* content_instance has no override.
|
|
236
|
+
*/
|
|
237
|
+
const moduleFieldTextListSchema = z
|
|
238
|
+
.object({
|
|
239
|
+
name: moduleFieldName,
|
|
240
|
+
kind: z.literal("text-list"),
|
|
241
|
+
label: moduleFieldLabel,
|
|
242
|
+
min: z.number().int().nonnegative().optional(),
|
|
243
|
+
max: z.number().int().positive().max(256).optional(),
|
|
244
|
+
default: z.array(z.string()).optional(),
|
|
245
|
+
})
|
|
246
|
+
.strict();
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* v0.12.0 — list-of-{label,href} field. Inner template uses
|
|
250
|
+
* `{{label}}` + `{{href}}` per iteration. Targets the common
|
|
251
|
+
* primary-nav / footer-column / sidebar-menu pattern without
|
|
252
|
+
* forcing a sub-module per link.
|
|
253
|
+
*/
|
|
254
|
+
const moduleFieldLinkListSchema = z
|
|
255
|
+
.object({
|
|
256
|
+
name: moduleFieldName,
|
|
257
|
+
kind: z.literal("link-list"),
|
|
258
|
+
label: moduleFieldLabel,
|
|
259
|
+
min: z.number().int().nonnegative().optional(),
|
|
260
|
+
max: z.number().int().positive().max(256).optional(),
|
|
261
|
+
default: z.array(z.object({ label: z.string(), href: z.string() }).strict()).optional(),
|
|
262
|
+
})
|
|
263
|
+
.strict();
|
|
264
|
+
|
|
265
|
+
export const moduleFieldSchema = z.discriminatedUnion("kind", [
|
|
266
|
+
moduleFieldPrimitiveSchema,
|
|
267
|
+
moduleFieldTextListSchema,
|
|
268
|
+
moduleFieldLinkListSchema,
|
|
269
|
+
moduleFieldModuleSchema,
|
|
270
|
+
moduleFieldModuleListSchema,
|
|
271
|
+
]);
|
|
272
|
+
export type ModuleField = z.infer<typeof moduleFieldSchema>;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The shape of a single nested-module reference inside `content_instances.values`
|
|
276
|
+
* when the field's kind is `module` or — repeated inside an array — `module-list`.
|
|
277
|
+
*/
|
|
278
|
+
export const moduleRefSchema = z
|
|
279
|
+
.object({
|
|
280
|
+
moduleId: z.string().uuid(),
|
|
281
|
+
contentInstanceId: z.string().uuid(),
|
|
282
|
+
})
|
|
283
|
+
.strict();
|
|
284
|
+
export type ModuleRef = z.infer<typeof moduleRefSchema>;
|
|
285
|
+
|
|
286
|
+
const moduleFieldsArray = z
|
|
287
|
+
.array(moduleFieldSchema)
|
|
288
|
+
.max(64, "modules may declare at most 64 fields")
|
|
289
|
+
.superRefine((arr, ctx) => {
|
|
290
|
+
const seen = new Set<string>();
|
|
291
|
+
for (const f of arr) {
|
|
292
|
+
if (seen.has(f.name)) {
|
|
293
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `duplicate field name: ${f.name}` });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
seen.add(f.name);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* v0.12.0 — coarse role tag for the AI's `## Modules` catalog block.
|
|
302
|
+
* Constrained by `modules_kind_check` in migration 0095. Adding a
|
|
303
|
+
* new kind means: (1) bump the SQL CHECK, (2) update this schema,
|
|
304
|
+
* (3) update formatModulesBlock so the AI knows when to use it.
|
|
305
|
+
*/
|
|
306
|
+
export const MODULE_KINDS = ["chrome", "hero", "content", "cta", "utility"] as const;
|
|
307
|
+
export const moduleKindSchema = z.enum(MODULE_KINDS);
|
|
308
|
+
export type ModuleKind = (typeof MODULE_KINDS)[number];
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* v0.12.0 — description is `.default("")` at the Zod boundary so the
|
|
312
|
+
* 82+ legacy callers in tests/seed scripts keep working unchanged.
|
|
313
|
+
* AI tool descriptions (create_module / edit_module) require the AI
|
|
314
|
+
* to supply a real description explicitly — see CLAUDE.md §1A.
|
|
315
|
+
*/
|
|
316
|
+
const moduleDescription = z.string().max(1000);
|
|
317
|
+
|
|
318
|
+
export const moduleCreateSchema = z
|
|
319
|
+
.object({
|
|
320
|
+
slug: slugSchema,
|
|
321
|
+
displayName: displayNameSchema,
|
|
322
|
+
description: moduleDescription.default(""),
|
|
323
|
+
kind: moduleKindSchema.default("content"),
|
|
324
|
+
/**
|
|
325
|
+
* v0.12.3 (issue #106) — stable semantic class shared by every
|
|
326
|
+
* instance of this module (`button`, `pricing-card`). What a parent
|
|
327
|
+
* module's `allowedModuleTypes` whitelist matches against. Optional:
|
|
328
|
+
* when omitted, `modules.create` derives it from `displayName` via
|
|
329
|
+
* `deriveModuleType()`. Pass it explicitly to reuse an existing
|
|
330
|
+
* class (e.g. a second `button` variant should share `type: "button"`).
|
|
331
|
+
*/
|
|
332
|
+
type: slugSchema.optional(),
|
|
333
|
+
html: moduleHtml,
|
|
334
|
+
css: moduleCss.default(""),
|
|
335
|
+
js: moduleJs.default(""),
|
|
336
|
+
fields: moduleFieldsArray.default([]),
|
|
337
|
+
})
|
|
338
|
+
.strict();
|
|
339
|
+
|
|
340
|
+
export const moduleUpdateSchema = z
|
|
341
|
+
.object({
|
|
342
|
+
moduleId: z.string().uuid(),
|
|
343
|
+
displayName: displayNameSchema.optional(),
|
|
344
|
+
description: moduleDescription.optional(),
|
|
345
|
+
kind: moduleKindSchema.optional(),
|
|
346
|
+
/** v0.12.3 (issue #106) — re-classify a module's stable `type`. */
|
|
347
|
+
type: slugSchema.optional(),
|
|
348
|
+
html: moduleHtml.optional(),
|
|
349
|
+
css: moduleCss.optional(),
|
|
350
|
+
js: moduleJs.optional(),
|
|
351
|
+
fields: moduleFieldsArray.optional(),
|
|
352
|
+
})
|
|
353
|
+
.strict();
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* v0.12.0 — page-type tag pages inherit from their template.
|
|
357
|
+
* Constrained by templates_kind_check in migration 0096. Surfaced
|
|
358
|
+
* in the AI's `## Pages` block so the AI sees three modules-on-
|
|
359
|
+
* product-pages as a pattern. See CLAUDE.md §1A.
|
|
360
|
+
*/
|
|
361
|
+
export const TEMPLATE_KINDS = [
|
|
362
|
+
"home",
|
|
363
|
+
"landing",
|
|
364
|
+
"product",
|
|
365
|
+
"blog",
|
|
366
|
+
"doc",
|
|
367
|
+
"content",
|
|
368
|
+
"utility",
|
|
369
|
+
] as const;
|
|
370
|
+
export const templateKindSchema = z.enum(TEMPLATE_KINDS);
|
|
371
|
+
export type TemplateKind = (typeof TEMPLATE_KINDS)[number];
|
|
372
|
+
|
|
373
|
+
export const templateCreateSchema = z
|
|
374
|
+
.object({
|
|
375
|
+
slug: slugSchema,
|
|
376
|
+
displayName: displayNameSchema,
|
|
377
|
+
/** v0.12.0 — what kind of page binds to this template. */
|
|
378
|
+
kind: templateKindSchema.default("content"),
|
|
379
|
+
html: z.string().max(TEMPLATE_HTML_MAX, `html exceeds ${TEMPLATE_HTML_MAX} bytes`),
|
|
380
|
+
css: z.string().max(TEMPLATE_CSS_MAX, `css exceeds ${TEMPLATE_CSS_MAX} bytes`).default(""),
|
|
381
|
+
/**
|
|
382
|
+
* P6.7.6 — every template binds to one layout. When omitted, the
|
|
383
|
+
* handler resolves to `site_defaults.default_layout_id` at create
|
|
384
|
+
* time. (Stored data, not a render-time fallback — see CLAUDE.md §2.)
|
|
385
|
+
*/
|
|
386
|
+
layoutId: z.string().uuid().optional(),
|
|
387
|
+
/**
|
|
388
|
+
* Optional block-set metadata — SYMMETRIC with `create_layout` and
|
|
389
|
+
* `propose_update_template`, which both take `blocks`. Omit it and the
|
|
390
|
+
* handler auto-derives one block per `<caelo-slot name="X">` in `html`
|
|
391
|
+
* (displayName = name, position = order of appearance) — the common case.
|
|
392
|
+
* Pass it to give blocks a nicer `displayName` or an explicit `position`;
|
|
393
|
+
* every entry's `name` MUST match a `<caelo-slot>` in `html` (a block with
|
|
394
|
+
* no slot renders nothing), else the create fails loudly (CLAUDE.md §2).
|
|
395
|
+
*/
|
|
396
|
+
blocks: z
|
|
397
|
+
.array(
|
|
398
|
+
z.object({
|
|
399
|
+
name: z.string().min(1).max(80),
|
|
400
|
+
displayName: z.string().min(1).max(200),
|
|
401
|
+
position: z.number().int().min(0).max(1000),
|
|
402
|
+
}),
|
|
403
|
+
)
|
|
404
|
+
.optional(),
|
|
405
|
+
})
|
|
406
|
+
.strict();
|
|
407
|
+
|
|
408
|
+
export const templateBlockSchema = z
|
|
409
|
+
.object({
|
|
410
|
+
name: slugSchema,
|
|
411
|
+
displayName: displayNameSchema,
|
|
412
|
+
position: z.number().int().nonnegative(),
|
|
413
|
+
})
|
|
414
|
+
.strict();
|
|
415
|
+
|
|
416
|
+
export const templateUpdateSchema = z
|
|
417
|
+
.object({
|
|
418
|
+
templateId: z.string().uuid(),
|
|
419
|
+
displayName: displayNameSchema.optional(),
|
|
420
|
+
/** v0.12.0 — re-classify the template's page-type kind. */
|
|
421
|
+
kind: templateKindSchema.optional(),
|
|
422
|
+
html: z.string().max(TEMPLATE_HTML_MAX).optional(),
|
|
423
|
+
css: z.string().max(TEMPLATE_CSS_MAX).optional(),
|
|
424
|
+
/** P6.7.6 — re-point the template to a different layout. */
|
|
425
|
+
layoutId: z.string().uuid().optional(),
|
|
426
|
+
/**
|
|
427
|
+
* v0.2.65 — Optional block-set replacement. When present, the
|
|
428
|
+
* update atomically applies the provided block list to
|
|
429
|
+
* `template_blocks` (DELETE-then-INSERT, same path as
|
|
430
|
+
* `template_blocks.set`). Critical for AI-driven flows: the AI's
|
|
431
|
+
* `propose_update_template` previously only wrote the html string
|
|
432
|
+
* and never touched the block table, so an approved proposal that
|
|
433
|
+
* added `<!-- block:content -->` markup left the page unable to
|
|
434
|
+
* find a "content" block. Allowing blocks here lets one
|
|
435
|
+
* propose+execute round add both the markup AND the block
|
|
436
|
+
* definition atomically.
|
|
437
|
+
*/
|
|
438
|
+
blocks: z.array(templateBlockSchema).optional(),
|
|
439
|
+
})
|
|
440
|
+
.strict();
|
|
441
|
+
|
|
442
|
+
export const templateBlocksSetSchema = z
|
|
443
|
+
.object({
|
|
444
|
+
templateId: z.string().uuid(),
|
|
445
|
+
blocks: z.array(templateBlockSchema),
|
|
446
|
+
})
|
|
447
|
+
.strict();
|
|
448
|
+
|
|
449
|
+
export const pageStatusSchema = z.enum(["draft", "published"]);
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Page payloads are `.strict()` — passing an `html` field is rejected by Zod
|
|
453
|
+
* before the handler runs. This is the §3.1 "no raw HTML on pages" invariant
|
|
454
|
+
* at the Validator boundary, complementing the schema-level guarantee that
|
|
455
|
+
* the `pages` table has no `html` column.
|
|
456
|
+
*/
|
|
457
|
+
export const pageCreateSchema = z
|
|
458
|
+
.object({
|
|
459
|
+
slug: slugSchema,
|
|
460
|
+
locale: localeSchema.default("en"),
|
|
461
|
+
/** P6.7.5 — internal editor label. Defaults to title if omitted. */
|
|
462
|
+
name: z.string().min(1).max(256).optional(),
|
|
463
|
+
title: z.string().min(1).max(256),
|
|
464
|
+
/**
|
|
465
|
+
* P6.7.6 — optional. When omitted, the handler resolves to
|
|
466
|
+
* `site_defaults.default_template_id` at create time. Stored data,
|
|
467
|
+
* not a render-time fallback (CLAUDE.md §2 no-fallbacks).
|
|
468
|
+
*/
|
|
469
|
+
templateId: z.string().uuid().optional(),
|
|
470
|
+
/**
|
|
471
|
+
* Optional — omit to let `pages.create` pick a context-aware default:
|
|
472
|
+
* `published` on a bootstrap site (0 live published pages), else
|
|
473
|
+
* `draft`. A bootstrap homepage MUST ship or the first Stage has
|
|
474
|
+
* nothing to serve ("0 published pages for env='staging'"). Pass an
|
|
475
|
+
* explicit value to override. (Was `.default("draft")`, which silently
|
|
476
|
+
* left the first page unpublished.)
|
|
477
|
+
*/
|
|
478
|
+
status: pageStatusSchema.optional(),
|
|
479
|
+
})
|
|
480
|
+
.strict();
|
|
481
|
+
|
|
482
|
+
export const pageUpdateSchema = z
|
|
483
|
+
.object({
|
|
484
|
+
pageId: z.string().uuid(),
|
|
485
|
+
/**
|
|
486
|
+
* Optional optimistic-concurrency token. When present, the op rejects
|
|
487
|
+
* with HandlerError("Conflict") if the row's version no longer matches.
|
|
488
|
+
* Routes that load the page first should always pass it back.
|
|
489
|
+
*/
|
|
490
|
+
expectedVersion: z.number().int().nonnegative().optional(),
|
|
491
|
+
/** P6.7.5 — three independently-editable identifiers. */
|
|
492
|
+
name: z.string().min(1).max(256).optional(),
|
|
493
|
+
title: z.string().min(1).max(256).optional(),
|
|
494
|
+
slug: slugSchema.optional(),
|
|
495
|
+
templateId: z.string().uuid().optional(),
|
|
496
|
+
status: pageStatusSchema.optional(),
|
|
497
|
+
/**
|
|
498
|
+
* Only meaningful together with `slug`. A slug change rewrites the page's
|
|
499
|
+
* public URL, so the op creates a 301 from the old path by default and
|
|
500
|
+
* rewrites every nav-menu / link-list / module-body link that pointed at
|
|
501
|
+
* it — all inside the same transaction as the slug write.
|
|
502
|
+
*
|
|
503
|
+
* `'skip'` suppresses ONLY the redirect (the link rewrites still run) and
|
|
504
|
+
* is for the rare case where the operator explicitly says the old URL
|
|
505
|
+
* should 404. Defaults to `'auto'`: silently dropping the 301 would strand
|
|
506
|
+
* every inbound link, which is never a safe default (CLAUDE.md §2).
|
|
507
|
+
*/
|
|
508
|
+
redirectFromOld: z.enum(["auto", "skip"]).optional(),
|
|
509
|
+
})
|
|
510
|
+
.strict();
|
|
511
|
+
|
|
512
|
+
export const pageSetModulesSchema = z
|
|
513
|
+
.object({
|
|
514
|
+
pageId: z.string().uuid(),
|
|
515
|
+
expectedVersion: z.number().int().nonnegative().optional(),
|
|
516
|
+
blocks: z.array(
|
|
517
|
+
z
|
|
518
|
+
.object({
|
|
519
|
+
blockName: slugSchema,
|
|
520
|
+
moduleIds: z.array(z.string().uuid()),
|
|
521
|
+
})
|
|
522
|
+
.strict(),
|
|
523
|
+
),
|
|
524
|
+
})
|
|
525
|
+
.strict();
|
|
526
|
+
|
|
527
|
+
export type ModuleCreateInput = z.infer<typeof moduleCreateSchema>;
|
|
528
|
+
export type ModuleUpdateInput = z.infer<typeof moduleUpdateSchema>;
|
|
529
|
+
export type TemplateCreateInput = z.infer<typeof templateCreateSchema>;
|
|
530
|
+
export type TemplateUpdateInput = z.infer<typeof templateUpdateSchema>;
|
|
531
|
+
export type TemplateBlocksSetInput = z.infer<typeof templateBlocksSetSchema>;
|
|
532
|
+
export type PageCreateInput = z.infer<typeof pageCreateSchema>;
|
|
533
|
+
export type PageUpdateInput = z.infer<typeof pageUpdateSchema>;
|
|
534
|
+
export type PageSetModulesInput = z.infer<typeof pageSetModulesSchema>;
|
|
535
|
+
|
|
536
|
+
// ─── v0.12.0 — content_instances ops ─────────────────────────────────
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* v0.12.0 — content sync mode on a placement.
|
|
540
|
+
*
|
|
541
|
+
* `synced` — editing the placement's content_instance propagates to
|
|
542
|
+
* every other placement bound to the same row.
|
|
543
|
+
* `unsynced` — placement holds a private row. Default.
|
|
544
|
+
*/
|
|
545
|
+
export const syncModeSchema = z.enum(["synced", "unsynced"]);
|
|
546
|
+
export type SyncMode = z.infer<typeof syncModeSchema>;
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* `values` jsonb shape — keyed by module field name. Values are arbitrary
|
|
550
|
+
* jsonb. For nested-module field kinds, the value satisfies `moduleRefSchema`
|
|
551
|
+
* (single) or `moduleRefSchema[]` (list); the renderer + write-side validator
|
|
552
|
+
* enforces shape against the module's declared `fields[]`.
|
|
553
|
+
*/
|
|
554
|
+
const contentValuesSchema = z.record(z.string(), z.unknown());
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* v0.12.0 — why this row exists as a shared/reusable instance.
|
|
558
|
+
* Surfaced in the `## Content Library` system-prompt block so the AI
|
|
559
|
+
* can decide *reuse the synced row* vs *fork to unsynced* vs *mint
|
|
560
|
+
* new* without a tool round-trip. Required by the AI's
|
|
561
|
+
* create_content_instance tool description; legacy callers may pass
|
|
562
|
+
* null (the migration-0093 unsynced rows have purpose=NULL).
|
|
563
|
+
* See CLAUDE.md §1A.
|
|
564
|
+
*/
|
|
565
|
+
const contentInstancePurpose = z.string().max(1000);
|
|
566
|
+
|
|
567
|
+
export const contentInstanceCreateSchema = z
|
|
568
|
+
.object({
|
|
569
|
+
moduleId: z.string().uuid(),
|
|
570
|
+
slug: slugSchema.optional(),
|
|
571
|
+
displayName: z.string().min(1).max(128).optional(),
|
|
572
|
+
purpose: contentInstancePurpose.optional(),
|
|
573
|
+
values: contentValuesSchema.default({}),
|
|
574
|
+
})
|
|
575
|
+
.strict();
|
|
576
|
+
|
|
577
|
+
export const contentInstanceUpdateSchema = z
|
|
578
|
+
.object({
|
|
579
|
+
id: z.string().uuid(),
|
|
580
|
+
/** Fully replaces existing values. */
|
|
581
|
+
values: contentValuesSchema,
|
|
582
|
+
/** Optional optimistic-concurrency token; mirrors pages.update. */
|
|
583
|
+
expectedVersion: z.number().int().nonnegative().optional(),
|
|
584
|
+
/** Optional rename in the same write; mirrors how pages.update accepts metadata edits. */
|
|
585
|
+
slug: slugSchema.nullable().optional(),
|
|
586
|
+
displayName: z.string().min(1).max(128).nullable().optional(),
|
|
587
|
+
/** v0.12.0 — rewrite the purpose (or clear via null). */
|
|
588
|
+
purpose: contentInstancePurpose.nullable().optional(),
|
|
589
|
+
})
|
|
590
|
+
.strict();
|
|
591
|
+
|
|
592
|
+
export const contentInstanceDeleteSchema = z
|
|
593
|
+
.object({
|
|
594
|
+
id: z.string().uuid(),
|
|
595
|
+
})
|
|
596
|
+
.strict();
|
|
597
|
+
|
|
598
|
+
export const setPlacementContentSchema = z
|
|
599
|
+
.object({
|
|
600
|
+
pageId: z.string().uuid(),
|
|
601
|
+
blockName: z.string().min(1).max(80),
|
|
602
|
+
position: z.number().int().nonnegative(),
|
|
603
|
+
contentInstanceId: z.string().uuid(),
|
|
604
|
+
syncMode: syncModeSchema,
|
|
605
|
+
})
|
|
606
|
+
.strict();
|
|
607
|
+
|
|
608
|
+
export const forkPlacementContentSchema = z
|
|
609
|
+
.object({
|
|
610
|
+
pageId: z.string().uuid(),
|
|
611
|
+
blockName: z.string().min(1).max(80),
|
|
612
|
+
position: z.number().int().nonnegative(),
|
|
613
|
+
})
|
|
614
|
+
.strict();
|
|
615
|
+
|
|
616
|
+
export type ContentInstanceCreateInput = z.infer<typeof contentInstanceCreateSchema>;
|
|
617
|
+
export type ContentInstanceUpdateInput = z.infer<typeof contentInstanceUpdateSchema>;
|
|
618
|
+
export type ContentInstanceDeleteInput = z.infer<typeof contentInstanceDeleteSchema>;
|
|
619
|
+
export type SetPlacementContentInput = z.infer<typeof setPlacementContentSchema>;
|
|
620
|
+
export type ForkPlacementContentInput = z.infer<typeof forkPlacementContentSchema>;
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Execution identity threaded through every Query API call. The Database Adapter
|
|
5
|
+
* reads this to set `SET LOCAL caelo.actor_id / actor_kind / plugin_id` on the
|
|
6
|
+
* transaction so RLS policies can scope rows.
|
|
7
|
+
*
|
|
8
|
+
* `actorId` being NULL must mean "no identity, deny by default" — the adapter
|
|
9
|
+
* sets the session var to an empty string and every RLS policy's `NULLIF(...,'')::uuid`
|
|
10
|
+
* yields NULL, which matches nothing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type ActorKind = "human" | "ai" | "plugin" | "system";
|
|
14
|
+
|
|
15
|
+
export interface ExecutionContext {
|
|
16
|
+
readonly actorId: string;
|
|
17
|
+
readonly actorKind: ActorKind;
|
|
18
|
+
/** Present only when the caller is a plugin. */
|
|
19
|
+
readonly pluginId?: string;
|
|
20
|
+
/** Opaque id for audit trace / log correlation. */
|
|
21
|
+
readonly requestId: string;
|
|
22
|
+
/**
|
|
23
|
+
* P5: when set, all snapshot rows emitted by ops in this transaction
|
|
24
|
+
* carry this chat_branch_id. Reads that opt into branch-aware mode
|
|
25
|
+
* resolve the chat's branch state when present, else fall back to main.
|
|
26
|
+
*/
|
|
27
|
+
readonly chatBranchId?: string;
|
|
28
|
+
/**
|
|
29
|
+
* P5: when set, snapshots emitted during this op group under the same
|
|
30
|
+
* chat_task_id so the timeline UX (P10A's task-grouped collapsing) can
|
|
31
|
+
* fold consecutive AI actions into one entry.
|
|
32
|
+
*/
|
|
33
|
+
readonly chatTaskId?: string;
|
|
34
|
+
/**
|
|
35
|
+
* P10.5: parent attribution for subagent invocations. When the
|
|
36
|
+
* `spawn_subagent` tool calls runChatTurn for the child, it carries
|
|
37
|
+
* these fields so the existing ai_calls + audit_events writers
|
|
38
|
+
* persist them to the new schema columns. Same code paths; just
|
|
39
|
+
* more attribution data threaded through.
|
|
40
|
+
*/
|
|
41
|
+
readonly parentChatSessionId?: string;
|
|
42
|
+
readonly parentAiCallId?: string;
|
|
43
|
+
}
|