@caelo-cms/shared 0.11.5 → 0.11.7
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 +206 -2
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +23 -9
- package/dist/ai-tools.js.map +1 -1
- package/dist/change-journal.d.ts +71 -0
- package/dist/change-journal.d.ts.map +1 -0
- package/dist/change-journal.js +252 -0
- package/dist/change-journal.js.map +1 -0
- package/dist/content.d.ts +214 -0
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +29 -11
- package/dist/content.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lists/fields.d.ts +5 -0
- package/dist/lists/fields.d.ts.map +1 -1
- package/dist/lists/fields.js +6 -1
- package/dist/lists/fields.js.map +1 -1
- package/dist/lists/list-type.d.ts.map +1 -1
- package/dist/lists/list-type.js +6 -3
- package/dist/lists/list-type.js.map +1 -1
- package/dist/lists/page-type.d.ts +1 -0
- package/dist/lists/page-type.d.ts.map +1 -1
- package/dist/lists/page-type.js +7 -0
- package/dist/lists/page-type.js.map +1 -1
- package/dist/live-events.d.ts +8 -4
- package/dist/live-events.d.ts.map +1 -1
- package/dist/live-events.js +18 -3
- package/dist/live-events.js.map +1 -1
- package/dist/media.d.ts +19 -0
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +16 -0
- package/dist/media.js.map +1 -1
- package/dist/phase-timer.d.ts +43 -2
- package/dist/phase-timer.d.ts.map +1 -1
- package/dist/phase-timer.js +170 -4
- package/dist/phase-timer.js.map +1 -1
- package/dist/pool-budget.d.ts +152 -0
- package/dist/pool-budget.d.ts.map +1 -0
- package/dist/pool-budget.js +151 -0
- package/dist/pool-budget.js.map +1 -0
- package/dist/stage-status.d.ts +89 -0
- package/dist/stage-status.d.ts.map +1 -0
- package/dist/stage-status.js +101 -0
- package/dist/stage-status.js.map +1 -0
- package/dist/themes-errors.js +1 -1
- package/dist/themes-errors.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/ai-tools.ts +23 -8
- package/src/change-journal.ts +325 -0
- package/src/content.ts +32 -11
- package/src/index.ts +3 -0
- package/src/lists/fields.ts +7 -1
- package/src/lists/list-fixtures.test.ts +1 -0
- package/src/lists/list-type.ts +6 -2
- package/src/lists/lists.test.ts +1 -0
- package/src/lists/page-type.ts +7 -0
- package/src/live-events.ts +18 -3
- package/src/media.test.ts +31 -0
- package/src/media.ts +17 -0
- package/src/phase-timer.test.ts +37 -0
- package/src/phase-timer.ts +182 -4
- package/src/pool-budget.test.ts +111 -0
- package/src/pool-budget.ts +234 -0
- package/src/stage-status.test.ts +75 -0
- package/src/stage-status.ts +112 -0
- package/src/themes-errors.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Issue #1267 — the change journal's registry: which `cms_admin` tables
|
|
5
|
+
* write journal rows, under which kind, and why every other table does not.
|
|
6
|
+
* The contract is `docs/dev/state-and-changes.md`.
|
|
7
|
+
*
|
|
8
|
+
* The journal (`entity_changes`, migration 0430) is written by generic row
|
|
9
|
+
* triggers that migrations attach with `caelo_journal_attach(...)`. This
|
|
10
|
+
* file is the readable copy of that set, and the CI test
|
|
11
|
+
* `change-journal-registry.integration.test.ts` holds both in step: every
|
|
12
|
+
* `cms_admin` table must be either journaled here with the exact trigger
|
|
13
|
+
* arguments the database carries, or exempt here with a written reason. A
|
|
14
|
+
* new table that is neither fails CI, so no table can silently fall
|
|
15
|
+
* outside the journal.
|
|
16
|
+
*
|
|
17
|
+
* Plugin storage tables (`plugin_<slug>.*`) are not listed: the host
|
|
18
|
+
* attaches the trigger when it provisions a plugin's schema, under the kind
|
|
19
|
+
* `plugin_storage` keyed on the plugin's id (never on its reusable slug), so
|
|
20
|
+
* a new plugin or domain needs no journal migration.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Every kind a journal row can carry. A consumer names the kinds it reads. */
|
|
24
|
+
export const JOURNAL_KINDS = [
|
|
25
|
+
"page",
|
|
26
|
+
"page_terms",
|
|
27
|
+
"page_seo",
|
|
28
|
+
"page_layout",
|
|
29
|
+
"page_module_content",
|
|
30
|
+
"module",
|
|
31
|
+
"template",
|
|
32
|
+
"template_blocks",
|
|
33
|
+
"layout",
|
|
34
|
+
"layout_blocks",
|
|
35
|
+
"layout_module",
|
|
36
|
+
"content_instance",
|
|
37
|
+
"list",
|
|
38
|
+
"list_type",
|
|
39
|
+
"page_type",
|
|
40
|
+
"theme",
|
|
41
|
+
"media",
|
|
42
|
+
"media_variant",
|
|
43
|
+
"media_crop",
|
|
44
|
+
"font",
|
|
45
|
+
"redirect",
|
|
46
|
+
"domain",
|
|
47
|
+
"site_defaults",
|
|
48
|
+
"site_settings",
|
|
49
|
+
"site_memory",
|
|
50
|
+
"skill",
|
|
51
|
+
"plugin",
|
|
52
|
+
"plugin_installation",
|
|
53
|
+
"plugin_grant",
|
|
54
|
+
"plugin_file",
|
|
55
|
+
"plugin_row",
|
|
56
|
+
"plugin_storage",
|
|
57
|
+
"snapshot",
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
60
|
+
/** One kind of the journal (`entity_changes.kind`). */
|
|
61
|
+
export type JournalKind = (typeof JOURNAL_KINDS)[number];
|
|
62
|
+
|
|
63
|
+
const KIND_SET: ReadonlySet<string> = new Set(JOURNAL_KINDS);
|
|
64
|
+
|
|
65
|
+
/** Is `value` a journal kind? */
|
|
66
|
+
export function isJournalKind(value: string): value is JournalKind {
|
|
67
|
+
return KIND_SET.has(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How a journaled table's rows map to journal rows (the trigger's
|
|
72
|
+
* arguments, `caelo_journal_attach` in migration 0430):
|
|
73
|
+
*
|
|
74
|
+
* - `row` — a live entity table. The journal row carries the entity id
|
|
75
|
+
* (`key`), the row's own branch when the table has one (`branch`), and
|
|
76
|
+
* the chat from the writing transaction's attribution settings.
|
|
77
|
+
* - `snapshot` — an entity snapshot table. Branch, chat and snapshot id
|
|
78
|
+
* come from the snapshot's `site_snapshots` header, and an inserted
|
|
79
|
+
* snapshot records the snapshot it replaces on the same line
|
|
80
|
+
* (`prev_snapshot_id`), so a consumer can diff the two states.
|
|
81
|
+
* - `header` — `site_snapshots` itself: Stage (`staged_at`), undo
|
|
82
|
+
* (`undone_at`) and lock takeover (`chat_branch_id`) are UPDATEs of it.
|
|
83
|
+
*/
|
|
84
|
+
export type JournalMode = "row" | "snapshot" | "header";
|
|
85
|
+
|
|
86
|
+
/** One journaled table, exactly as its trigger is attached. */
|
|
87
|
+
export interface JournaledTable {
|
|
88
|
+
readonly kind: JournalKind;
|
|
89
|
+
readonly mode: JournalMode;
|
|
90
|
+
/** The column that holds the entity id (`entity_changes.row_id`). */
|
|
91
|
+
readonly key: string;
|
|
92
|
+
/** The primary-key columns an UPDATE's old and new rows are paired on. */
|
|
93
|
+
readonly pair: readonly string[];
|
|
94
|
+
/** A column naming the entity this row belongs to (`entity_changes.parent_id`). */
|
|
95
|
+
readonly parent?: string;
|
|
96
|
+
/** The row's own branch column (`row` mode only). */
|
|
97
|
+
readonly branch?: string;
|
|
98
|
+
/** The column holding the owning plugin's id. */
|
|
99
|
+
readonly plugin?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const row = (
|
|
103
|
+
kind: JournalKind,
|
|
104
|
+
key: string,
|
|
105
|
+
pair: readonly string[],
|
|
106
|
+
extra: Partial<Pick<JournaledTable, "parent" | "branch" | "plugin">> = {},
|
|
107
|
+
): JournaledTable => ({ kind, mode: "row", key, pair, ...extra });
|
|
108
|
+
|
|
109
|
+
const snapshot = (
|
|
110
|
+
kind: JournalKind,
|
|
111
|
+
key: string,
|
|
112
|
+
extra: Partial<Pick<JournaledTable, "parent" | "plugin">> = {},
|
|
113
|
+
): JournaledTable => ({ kind, mode: "snapshot", key, pair: ["id"], ...extra });
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The journaled `cms_admin` tables. Everything an editor, a render, a
|
|
117
|
+
* build or a plugin derives from: content and its snapshots, media,
|
|
118
|
+
* settings that render into pages, redirects, plugins and their grants.
|
|
119
|
+
*/
|
|
120
|
+
export const JOURNALED_TABLES: Readonly<Record<string, JournaledTable>> = {
|
|
121
|
+
pages: row("page", "id", ["id"], { branch: "chat_branch_id" }),
|
|
122
|
+
page_terms: row("page_terms", "page_id", ["page_id", "list_id", "item_id"]),
|
|
123
|
+
pages_seo: row("page_seo", "page_id", ["page_id"]),
|
|
124
|
+
page_modules: row("page_layout", "page_id", ["page_id", "block_name", "position"]),
|
|
125
|
+
page_module_content: row("page_module_content", "id", ["id"], { parent: "page_id" }),
|
|
126
|
+
modules: row("module", "id", ["id"], { branch: "chat_branch_id" }),
|
|
127
|
+
templates: row("template", "id", ["id"], { branch: "chat_branch_id" }),
|
|
128
|
+
template_blocks: row("template_blocks", "template_id", ["template_id", "name"]),
|
|
129
|
+
layouts: row("layout", "id", ["id"], { branch: "chat_branch_id" }),
|
|
130
|
+
layout_blocks: row("layout_blocks", "layout_id", ["layout_id", "name"]),
|
|
131
|
+
layout_modules: row("layout_module", "layout_id", ["layout_id", "block_name", "position"]),
|
|
132
|
+
content_instances: row("content_instance", "id", ["id"], {
|
|
133
|
+
branch: "chat_branch_id",
|
|
134
|
+
parent: "module_id",
|
|
135
|
+
}),
|
|
136
|
+
lists: row("list", "id", ["id"], { branch: "chat_branch_id" }),
|
|
137
|
+
list_types: row("list_type", "id", ["id"], { branch: "chat_branch_id" }),
|
|
138
|
+
page_types: row("page_type", "id", ["id"], { branch: "chat_branch_id" }),
|
|
139
|
+
themes: row("theme", "id", ["id"]),
|
|
140
|
+
media_assets: row("media", "id", ["id"]),
|
|
141
|
+
media_variants: row("media_variant", "asset_id", ["asset_id", "variant"]),
|
|
142
|
+
media_crops: row("media_crop", "id", ["id"], { parent: "asset_id" }),
|
|
143
|
+
font_assets: row("font", "id", ["id"]),
|
|
144
|
+
redirects: row("redirect", "id", ["id"]),
|
|
145
|
+
domains: row("domain", "id", ["id"]),
|
|
146
|
+
site_defaults: row("site_defaults", "id", ["id"]),
|
|
147
|
+
site_settings: row("site_settings", "id", ["id"]),
|
|
148
|
+
site_ai_memory: row("site_memory", "id", ["id"]),
|
|
149
|
+
skills: row("skill", "id", ["id"]),
|
|
150
|
+
plugins: row("plugin", "id", ["id"]),
|
|
151
|
+
plugin_installation_versions: row("plugin_installation", "id", ["id"], { parent: "plugin_id" }),
|
|
152
|
+
plugin_capability_grants: row("plugin_grant", "id", ["id"], { parent: "plugin_id" }),
|
|
153
|
+
plugin_private_files: row("plugin_file", "id", ["plugin_id", "id"], { plugin: "plugin_id" }),
|
|
154
|
+
|
|
155
|
+
site_snapshots: { kind: "snapshot", mode: "header", key: "id", pair: ["id"] },
|
|
156
|
+
page_snapshots: snapshot("page", "page_id"),
|
|
157
|
+
page_layout_snapshots: snapshot("page_layout", "page_id"),
|
|
158
|
+
page_module_content_snapshots: snapshot("page_module_content", "page_module_content_id", {
|
|
159
|
+
parent: "page_id",
|
|
160
|
+
}),
|
|
161
|
+
module_snapshots: snapshot("module", "module_id"),
|
|
162
|
+
template_snapshots: snapshot("template", "template_id"),
|
|
163
|
+
layout_module_snapshots: snapshot("layout_module", "layout_id"),
|
|
164
|
+
content_instance_snapshots: snapshot("content_instance", "content_instance_id"),
|
|
165
|
+
list_snapshots: snapshot("list", "list_id"),
|
|
166
|
+
list_type_snapshots: snapshot("list_type", "list_type_id"),
|
|
167
|
+
page_type_snapshots: snapshot("page_type", "page_type_id"),
|
|
168
|
+
theme_snapshots: snapshot("theme", "theme_id"),
|
|
169
|
+
plugin_row_snapshots: snapshot("plugin_row", "row_id", {
|
|
170
|
+
parent: "table_name",
|
|
171
|
+
plugin: "plugin_id",
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const QUEUE = "a proposal queue; its changes reach editors as the op-classified `pending` topic";
|
|
176
|
+
const LOG = "an append-only log or history; nothing is derived from it";
|
|
177
|
+
const LEASE = "a lease or lock that coordinates processes; nothing is derived from it";
|
|
178
|
+
const SECURITY = "identity and permission rows, read per request by the auth layer";
|
|
179
|
+
const RUNTIME = "runtime bookkeeping of a process (health, progress, cache)";
|
|
180
|
+
const RETIRED = "retired by #1267; nothing reads it any more, migration 0442 drops it";
|
|
181
|
+
const RETIRED_SIGNAL =
|
|
182
|
+
"retired by #1267; nothing reads it, its writer triggers stay one release for the previous image (roll, rollback), migration 0442 drops both";
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Every `cms_admin` table that writes no journal rows, with the reason.
|
|
186
|
+
* Adding a table here is a decision a reviewer reads, not a default.
|
|
187
|
+
*/
|
|
188
|
+
export const JOURNAL_EXEMPT_TABLES: Readonly<Record<string, string>> = {
|
|
189
|
+
__drizzle_migrations: "the migration runner's own bookkeeping",
|
|
190
|
+
entity_changes: "the journal itself",
|
|
191
|
+
entity_change_horizons: "the journal's garbage-collection horizons",
|
|
192
|
+
change_consumers: "the journal's consumer cursors",
|
|
193
|
+
domain_events: RETIRED,
|
|
194
|
+
font_usage_changes: RETIRED_SIGNAL,
|
|
195
|
+
site_view_generation: RETIRED_SIGNAL,
|
|
196
|
+
plugin_event_cursors: RETIRED,
|
|
197
|
+
|
|
198
|
+
actors: SECURITY,
|
|
199
|
+
users: SECURITY,
|
|
200
|
+
roles: SECURITY,
|
|
201
|
+
permissions: SECURITY,
|
|
202
|
+
role_permissions: SECURITY,
|
|
203
|
+
user_roles: SECURITY,
|
|
204
|
+
sessions: SECURITY,
|
|
205
|
+
password_reset_tokens: SECURITY,
|
|
206
|
+
owner_bootstrap_tokens: SECURITY,
|
|
207
|
+
mcp_tokens: SECURITY,
|
|
208
|
+
mcp_remote_connections: SECURITY,
|
|
209
|
+
mcp_remote_access_blocks: SECURITY,
|
|
210
|
+
mcp_transport_sessions: SECURITY,
|
|
211
|
+
operator_oauth_client: SECURITY,
|
|
212
|
+
credential_handoffs:
|
|
213
|
+
"one-time credentials an approval left; delivered as the `credentials` topic",
|
|
214
|
+
user_preferences: "one person's editor preferences, read only by that person's own requests",
|
|
215
|
+
skill_pin_defaults: "one person's skill pins, read only by that person's own chats",
|
|
216
|
+
|
|
217
|
+
audit_events: LOG,
|
|
218
|
+
ai_calls: LOG,
|
|
219
|
+
ai_bug_reports: LOG,
|
|
220
|
+
ai_moduleize_attempts: LOG,
|
|
221
|
+
page_edit_log: LOG,
|
|
222
|
+
import_run_events: LOG,
|
|
223
|
+
platform_update_events: LOG,
|
|
224
|
+
plugin_errors: LOG,
|
|
225
|
+
redirect_syncs: LOG,
|
|
226
|
+
operator_access_sync_runs: LOG,
|
|
227
|
+
link_migration_runs: LOG,
|
|
228
|
+
comment_archive:
|
|
229
|
+
"visitor comments copied from cms_public: untrusted visitor data, nothing is derived from it",
|
|
230
|
+
|
|
231
|
+
chat_sessions:
|
|
232
|
+
"conversation metadata; what a chat changed is journaled through its snapshots, its run state reaches editors as the `agents` topic",
|
|
233
|
+
chat_messages: "conversation history (the provider's message log)",
|
|
234
|
+
chat_tool_results: "conversation history (tool results)",
|
|
235
|
+
chat_agent_runs: "running agents; reach editors as the op-classified `agents` topic",
|
|
236
|
+
chat_turn_approvals: "paused approval cards; reach editors as the `pending` topic",
|
|
237
|
+
chat_turn_leases: LEASE,
|
|
238
|
+
chat_entity_locks:
|
|
239
|
+
"cross-branch entity locks; a takeover re-points the snapshots, which are journaled",
|
|
240
|
+
chat_lock_takeovers: LOG,
|
|
241
|
+
chat_branch_publish_marks:
|
|
242
|
+
"which branch states reached main; the Stage that writes it also updates the journaled site_snapshots",
|
|
243
|
+
draft_entity_observations: "which draft entity states a chat has read (conflict detection)",
|
|
244
|
+
subagent_runs: "subagent bookkeeping of a chat turn",
|
|
245
|
+
mcp_approval_handoffs: "approval hand-offs to an MCP client",
|
|
246
|
+
tool_approval_actions: "approval decisions of chat tool calls",
|
|
247
|
+
plugin_tool_approval_bindings: "the binding of an approved plugin tool call to its arguments",
|
|
248
|
+
site_draft: "the shared draft branch's registry row; branch changes are journaled as snapshots",
|
|
249
|
+
genesis_drafts: "design drafts before a site exists; no page renders from them",
|
|
250
|
+
image_requests: "image generation requests; the resulting media asset is journaled",
|
|
251
|
+
media_alt_proposals:
|
|
252
|
+
"alt-text suggestions awaiting review; the accepted alt is journaled on media_assets",
|
|
253
|
+
entity_leases: LEASE,
|
|
254
|
+
deploy_leases: LEASE,
|
|
255
|
+
plugin_activation_leases: LEASE,
|
|
256
|
+
rate_limit_buckets: "visitor and login rate-limit counters",
|
|
257
|
+
rate_limit_profiles: "gateway rate-limit profiles, published to the gateway's projection",
|
|
258
|
+
plugin_rate_limit_overrides:
|
|
259
|
+
"gateway rate-limit overrides, published to the gateway's projection",
|
|
260
|
+
plugin_rate_limit_proposals: QUEUE,
|
|
261
|
+
|
|
262
|
+
ai_providers: "Owner AI configuration; read per call, nothing is derived from it",
|
|
263
|
+
ai_budgets: "Owner AI budgets; read per call, nothing is derived from it",
|
|
264
|
+
ai_pricing: "Owner AI pricing; read per call, nothing is derived from it",
|
|
265
|
+
email_config: "Owner email configuration; read per send",
|
|
266
|
+
telemetry_settings: "the install's telemetry consent",
|
|
267
|
+
staging_access:
|
|
268
|
+
"staging access settings, synced to the hosting provider on save (docs/dev/redirects-and-settings-sync.md)",
|
|
269
|
+
provisioning_outputs: "what the provisioner recorded about the install",
|
|
270
|
+
release_check_cache: RUNTIME,
|
|
271
|
+
deploy_targets: "deploy target configuration; read by the deploy ops",
|
|
272
|
+
deploy_runs: "deploy runs; reach editors as the op-classified `deploys` topic",
|
|
273
|
+
redirect_sync_state: RUNTIME,
|
|
274
|
+
redirect_changes:
|
|
275
|
+
"which transactions changed redirects, folded by the redirect sync; the journaled redirects table is the source",
|
|
276
|
+
static_bakes: "derived build output of plugin-baked regions",
|
|
277
|
+
content_instance_refs:
|
|
278
|
+
"derived (synchronous, #1287): the ids and media slugs each content instance's values reference, maintained by triggers on the journaled content_instances; rebuilt by content_instances.rebuild_refs",
|
|
279
|
+
plugin_data_build_state: RUNTIME,
|
|
280
|
+
plugin_data_stale_pages: "derived: which pages' baked plugin regions are stale",
|
|
281
|
+
plugin_runtime: RUNTIME,
|
|
282
|
+
plugin_schema_migrations: "plugin DDL bookkeeping",
|
|
283
|
+
plugin_schema_provisions: "plugin DDL bookkeeping",
|
|
284
|
+
plugin_private_file_chunks:
|
|
285
|
+
"the bytes of plugin_private_files rows, journaled through the file row",
|
|
286
|
+
public_projection_outbox: "the outbox of the cms_public projection, drained by its publisher",
|
|
287
|
+
platform_update_agent: RUNTIME,
|
|
288
|
+
platform_update_notifications: RUNTIME,
|
|
289
|
+
platform_update_policy: "the Owner's update policy, read by the update scheduler",
|
|
290
|
+
platform_update_requests: "update requests, run by the upgrader job",
|
|
291
|
+
platform_update_scheduler: RUNTIME,
|
|
292
|
+
|
|
293
|
+
import_runs: "site-import runs; reach editors as the op-classified `crawl` topic",
|
|
294
|
+
import_pages: "crawled pages of a site import, before they become pages",
|
|
295
|
+
import_run_links: "links found by a site import",
|
|
296
|
+
quality_audit_runs: "quality audits; reach editors as the op-classified `quality` topic",
|
|
297
|
+
quality_audit_pages: "quality audit results per page",
|
|
298
|
+
quality_baselines: "quality baselines per page, written by the audit worker",
|
|
299
|
+
quality_acceptances: "accepted quality findings; reach editors as the `quality` topic",
|
|
300
|
+
site_memory_proposals: QUEUE,
|
|
301
|
+
skill_proposals: QUEUE,
|
|
302
|
+
|
|
303
|
+
ai_providers_pending_actions: QUEUE,
|
|
304
|
+
deploy_pending_actions: QUEUE,
|
|
305
|
+
domain_pending_actions: QUEUE,
|
|
306
|
+
email_config_pending_actions: QUEUE,
|
|
307
|
+
layout_pending_actions: QUEUE,
|
|
308
|
+
mcp_token_pending_actions: QUEUE,
|
|
309
|
+
owner_settings_pending_actions: QUEUE,
|
|
310
|
+
plugin_pending_actions: QUEUE,
|
|
311
|
+
quality_pending_actions: QUEUE,
|
|
312
|
+
role_pending_actions: QUEUE,
|
|
313
|
+
site_defaults_pending_actions: QUEUE,
|
|
314
|
+
snapshot_revert_pending_actions: QUEUE,
|
|
315
|
+
template_pending_actions: QUEUE,
|
|
316
|
+
theme_pending_actions: QUEUE,
|
|
317
|
+
url_migration_pending_actions: QUEUE,
|
|
318
|
+
user_pending_actions: QUEUE,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
/** NOTIFY channel the journal trigger wakes consumers on (payload empty). */
|
|
322
|
+
export const JOURNAL_CHANNEL = "caelo_entity_changes";
|
|
323
|
+
|
|
324
|
+
/** NOTIFY channel carrying operation-classified live topics across instances. */
|
|
325
|
+
export const LIVE_TOPICS_CHANNEL = "caelo_live_topics";
|
package/src/content.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { hasEscapedLineBreaks } from "./escaped-line-breaks.js";
|
|
|
15
15
|
import { DEFAULT_LIST_TYPES, SITE_TEXTS_LIST_TYPE } from "./lists/default-types.js";
|
|
16
16
|
import { listItemInputSchema } from "./lists/items.js";
|
|
17
17
|
import { LIST_MAX_ITEMS, sourceDefSchema } from "./lists/list-type.js";
|
|
18
|
+
import { pageTypeDefSchema } from "./lists/page-type.js";
|
|
18
19
|
import { listPaginationSchema } from "./lists/pagination.js";
|
|
19
20
|
import { LIST_REF_RE } from "./lists/refs.js";
|
|
20
21
|
|
|
@@ -185,7 +186,7 @@ const noCdata = (value: string): boolean => !value.includes(CDATA_OPEN);
|
|
|
185
186
|
const cdataMessage = (field: "css" | "js"): string =>
|
|
186
187
|
`${field} contains an XML CDATA marker (${CDATA_OPEN}) — module ${field} is stored and served ` +
|
|
187
188
|
`verbatim, so CDATA wrappers reach the browser unparsed and break rendering. ` +
|
|
188
|
-
`Remove the wrapper
|
|
189
|
+
`Remove the wrapper: pass plain ${field === "css" ? "CSS" : "JavaScript"}.`;
|
|
189
190
|
|
|
190
191
|
const moduleCss = z
|
|
191
192
|
.string()
|
|
@@ -624,17 +625,37 @@ export const pageTypeSlugSchema = z
|
|
|
624
625
|
"page type slug: lowercase letters, digits, hyphens",
|
|
625
626
|
);
|
|
626
627
|
|
|
628
|
+
/**
|
|
629
|
+
* #1276 — slugs a new page type is deliberately distinct from: the
|
|
630
|
+
* duplicate guard refuses a new type whose slug normalises to an existing
|
|
631
|
+
* one (`blogs` next to `blog`) unless the caller names it here.
|
|
632
|
+
*/
|
|
633
|
+
export const pageTypeDistinctFromSchema = z.array(pageTypeSlugSchema).max(16);
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* #1276 — a page type created together with its first template
|
|
637
|
+
* (`templates.create` `newPageType`): the definition without `templates`
|
|
638
|
+
* and `defaultTemplate`, which are the new template.
|
|
639
|
+
*/
|
|
640
|
+
export const newPageTypeSchema = pageTypeDefSchema
|
|
641
|
+
.omit({ templates: true, defaultTemplate: true })
|
|
642
|
+
.extend({ distinctFrom: pageTypeDistinctFromSchema.optional() })
|
|
643
|
+
.strict();
|
|
644
|
+
export type NewPageTypeInput = z.infer<typeof newPageTypeSchema>;
|
|
645
|
+
|
|
627
646
|
export const templateCreateSchema = z
|
|
628
647
|
.object({
|
|
629
648
|
slug: slugSchema,
|
|
630
649
|
displayName: displayNameSchema,
|
|
631
650
|
/**
|
|
632
|
-
* Epic #1107 — the page type
|
|
633
|
-
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
651
|
+
* Epic #1107 / #1276 — the page type this template serves. Exactly one
|
|
652
|
+
* of `pageType` (an existing type's slug: the template is appended to
|
|
653
|
+
* its `templates`) and `newPageType` (a new type created in the same
|
|
654
|
+
* transaction with this template as its `defaultTemplate`) is
|
|
655
|
+
* required; `templates.create` refuses neither or both.
|
|
636
656
|
*/
|
|
637
657
|
pageType: pageTypeSlugSchema.optional(),
|
|
658
|
+
newPageType: newPageTypeSchema.optional(),
|
|
638
659
|
html: z
|
|
639
660
|
.string()
|
|
640
661
|
.max(TEMPLATE_HTML_MAX, `html exceeds ${TEMPLATE_HTML_MAX} bytes`)
|
|
@@ -736,9 +757,9 @@ export const pageCreateSchema = z
|
|
|
736
757
|
*/
|
|
737
758
|
shortTitle: pageShortTitleSchema.optional(),
|
|
738
759
|
/**
|
|
739
|
-
*
|
|
740
|
-
* `
|
|
741
|
-
*
|
|
760
|
+
* Optional: one of the page type's templates; omitted = the type's
|
|
761
|
+
* `defaultTemplate`. Without `pageType` it must belong to exactly one
|
|
762
|
+
* page type, which the page then gets (#1276).
|
|
742
763
|
*/
|
|
743
764
|
templateId: z.string().uuid().optional(),
|
|
744
765
|
/**
|
|
@@ -756,9 +777,9 @@ export const pageCreateSchema = z
|
|
|
756
777
|
*/
|
|
757
778
|
parentPageId: z.string().uuid().nullable().optional(),
|
|
758
779
|
/**
|
|
759
|
-
* Epic #1107 — what the page IS (page type slug).
|
|
760
|
-
*
|
|
761
|
-
*
|
|
780
|
+
* Epic #1107 — what the page IS (page type slug). Required unless
|
|
781
|
+
* `templateId` belongs to exactly one page type; there is no stored
|
|
782
|
+
* default type (#1276), so a create with neither is refused.
|
|
762
783
|
*/
|
|
763
784
|
pageType: pageTypeSlugSchema.optional(),
|
|
764
785
|
/** Property values, validated against the page type. */
|
package/src/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from "./base-css.js";
|
|
|
9
9
|
export * from "./bug-intake.js";
|
|
10
10
|
export * from "./build-page.js";
|
|
11
11
|
export * from "./cap-failures.js";
|
|
12
|
+
export * from "./change-journal.js";
|
|
12
13
|
export * from "./cloud-shell-install.js";
|
|
13
14
|
export * from "./content.js";
|
|
14
15
|
export * from "./context.js";
|
|
@@ -55,6 +56,7 @@ export * from "./page-log.js";
|
|
|
55
56
|
export * from "./phase-timer.js";
|
|
56
57
|
export * from "./platform-updates.js";
|
|
57
58
|
export * from "./plugin-preview.js";
|
|
59
|
+
export * from "./pool-budget.js";
|
|
58
60
|
export * from "./preview-compose.js";
|
|
59
61
|
export * from "./preview-scanner.js";
|
|
60
62
|
export * from "./proposal-status.js";
|
|
@@ -77,6 +79,7 @@ export * from "./seo.js";
|
|
|
77
79
|
export * from "./skills.js";
|
|
78
80
|
export * from "./snapshots.js";
|
|
79
81
|
export * from "./source-text-transport.js";
|
|
82
|
+
export * from "./stage-status.js";
|
|
80
83
|
export * from "./staging-gate.js";
|
|
81
84
|
export * from "./static-cache-policy.js";
|
|
82
85
|
export * from "./static-security-headers.js";
|
package/src/lists/fields.ts
CHANGED
|
@@ -56,6 +56,12 @@ export const LIST_RESERVED_NAMES: ReadonlySet<string> = new Set([
|
|
|
56
56
|
/** Name prefixes reserved as a family (see {@link LIST_RESERVED_NAMES}). */
|
|
57
57
|
export const LIST_RESERVED_PREFIXES = ["is_", "has_", "count_"] as const;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The reserved names as one clause for an error (#1277: the tool
|
|
61
|
+
* description no longer lists them, so the refusal does).
|
|
62
|
+
*/
|
|
63
|
+
export const LIST_RESERVED_NAMES_CLAUSE = `reserved: ${[...LIST_RESERVED_NAMES].join(", ")}, ${LIST_RESERVED_PREFIXES.map((p) => `${p}*`).join(", ")}`;
|
|
64
|
+
|
|
59
65
|
/** True when `name` is taken by the engine (see {@link LIST_RESERVED_NAMES}). */
|
|
60
66
|
export function isReservedListName(name: string): boolean {
|
|
61
67
|
return LIST_RESERVED_NAMES.has(name) || LIST_RESERVED_PREFIXES.some((p) => name.startsWith(p));
|
|
@@ -256,7 +262,7 @@ export function checkFieldDef(field: FieldDef, path: string): ListIssue[] {
|
|
|
256
262
|
if (isReservedListName(field.name)) {
|
|
257
263
|
issues.push({
|
|
258
264
|
path,
|
|
259
|
-
message: `field name "${field.name}" is reserved by the template engine — pick another name.`,
|
|
265
|
+
message: `field name "${field.name}" is reserved by the template engine (${LIST_RESERVED_NAMES_CLAUSE}) — pick another name.`,
|
|
260
266
|
});
|
|
261
267
|
}
|
|
262
268
|
if (field.default !== undefined) {
|
package/src/lists/list-type.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
fieldDefSchema,
|
|
18
18
|
fieldExtSchema,
|
|
19
19
|
isReservedListName,
|
|
20
|
+
LIST_RESERVED_NAMES_CLAUSE,
|
|
20
21
|
type ListIssue,
|
|
21
22
|
listNameSchema,
|
|
22
23
|
} from "./fields.js";
|
|
@@ -180,7 +181,10 @@ export function checkListTypeDef(def: ListTypeDef): ListIssue[] {
|
|
|
180
181
|
for (const [kind, node] of Object.entries(def.nodes)) {
|
|
181
182
|
const at = `nodes.${kind}`;
|
|
182
183
|
if (isReservedListName(kind)) {
|
|
183
|
-
issues.push({
|
|
184
|
+
issues.push({
|
|
185
|
+
path: at,
|
|
186
|
+
message: `node kind "${kind}" is a reserved name (${LIST_RESERVED_NAMES_CLAUSE}) — rename it.`,
|
|
187
|
+
});
|
|
184
188
|
}
|
|
185
189
|
const seen = new Set<string>();
|
|
186
190
|
node.fields.forEach((f, i) => {
|
|
@@ -193,7 +197,7 @@ export function checkListTypeDef(def: ListTypeDef): ListIssue[] {
|
|
|
193
197
|
if (isReservedListName(node.children.name)) {
|
|
194
198
|
issues.push({
|
|
195
199
|
path: `${at}.children.name`,
|
|
196
|
-
message: `children name "${node.children.name}" is reserved — pick another loop name.`,
|
|
200
|
+
message: `children name "${node.children.name}" is reserved (${LIST_RESERVED_NAMES_CLAUSE}) — pick another loop name.`,
|
|
197
201
|
});
|
|
198
202
|
}
|
|
199
203
|
if (seen.has(node.children.name)) {
|
package/src/lists/lists.test.ts
CHANGED
|
@@ -295,6 +295,7 @@ describe("page types and addresses", () => {
|
|
|
295
295
|
slug: "news",
|
|
296
296
|
name: "News",
|
|
297
297
|
purpose: "Dated announcements",
|
|
298
|
+
collection: true,
|
|
298
299
|
templates: [PAGE],
|
|
299
300
|
defaultTemplate: LIST,
|
|
300
301
|
properties: [{ name: "teaser", label: "T", type: "module" }],
|
package/src/lists/page-type.ts
CHANGED
|
@@ -17,6 +17,13 @@ export const pageTypeDefSchema = z
|
|
|
17
17
|
slug: z.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/),
|
|
18
18
|
name: z.string().min(1).max(200),
|
|
19
19
|
purpose: z.string().min(1).max(1000),
|
|
20
|
+
/**
|
|
21
|
+
* A collection of many similar pages meant to be listed or queried
|
|
22
|
+
* (blog, news, products, jobs, docs) — true; standalone pages (home,
|
|
23
|
+
* about / team / contact, legal, landing, utility) — false. The
|
|
24
|
+
* manual-page-listing check looks only at collection types (0450).
|
|
25
|
+
*/
|
|
26
|
+
collection: z.boolean(),
|
|
20
27
|
/** Template ids a page of this type may bind to. */
|
|
21
28
|
templates: z.array(z.string().uuid()).min(1).max(64),
|
|
22
29
|
defaultTemplate: z.string().uuid(),
|
package/src/live-events.ts
CHANGED
|
@@ -11,9 +11,13 @@
|
|
|
11
11
|
* it is safe to hand to every signed-in tab. The read that follows goes
|
|
12
12
|
* through the endpoint's own permission checks.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* `observeCommittedOperations`). Only WRITE operations are
|
|
16
|
-
* classified as a change would make every refresh announce
|
|
14
|
+
* Operational topics come from the names of operations that committed (the
|
|
15
|
+
* Query API's `observeCommittedOperations`). Only WRITE operations are
|
|
16
|
+
* listed: a read classified as a change would make every refresh announce
|
|
17
|
+
* another change. Entity-derived topics (`draft`) come from the change
|
|
18
|
+
* journal instead (issue #1267): they fire exactly when a committed write
|
|
19
|
+
* changed a journaled row, on whichever instance it committed, and never
|
|
20
|
+
* for a write that changed nothing.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
/** One surface an editor tab may need to re-read. */
|
|
@@ -30,6 +34,12 @@ export const LIVE_TOPICS = [
|
|
|
30
34
|
"crawl",
|
|
31
35
|
/** Deploy runs (Stage, Publish live, rollback). */
|
|
32
36
|
"deploys",
|
|
37
|
+
/**
|
|
38
|
+
* Site content changed: a journaled row committed (a chat's or an MCP
|
|
39
|
+
* agent's write, a Stage, an undo, a settings change). From the change
|
|
40
|
+
* journal, never from an op name (#1267).
|
|
41
|
+
*/
|
|
42
|
+
"draft",
|
|
33
43
|
] as const;
|
|
34
44
|
|
|
35
45
|
export type LiveTopic = (typeof LIVE_TOPICS)[number];
|
|
@@ -70,6 +80,11 @@ const EXACT: Readonly<Record<string, readonly LiveTopic[]>> = {
|
|
|
70
80
|
// An external agent's tool call: first sight of it lists it as running,
|
|
71
81
|
// and it may have queued a card.
|
|
72
82
|
"chat.touch_external_agent": ["agents", "pending"],
|
|
83
|
+
// The same presence row, written by Power-MCP's bookkeeping around each
|
|
84
|
+
// tool call (#1253): both mark the agent present (as the touch did), and
|
|
85
|
+
// the finish follows a card the tool may have queued.
|
|
86
|
+
"mcp.begin_tool_call": ["agents"],
|
|
87
|
+
"mcp.finish_tool_call": ["agents", "pending"],
|
|
73
88
|
"chat.record_turn_approvals": ["pending"],
|
|
74
89
|
"chat.decide_turn_approval": ["pending"],
|
|
75
90
|
"chat.supersede_turn_approvals": ["pending"],
|
package/src/media.test.ts
CHANGED
|
@@ -103,12 +103,43 @@ describe("media schemas", () => {
|
|
|
103
103
|
height: 1080,
|
|
104
104
|
sizeBytes: 1024,
|
|
105
105
|
storageKey: `${"a".repeat(64)}/orig.jpg`,
|
|
106
|
+
digest: {
|
|
107
|
+
contentSha256: "b".repeat(64),
|
|
108
|
+
size: 1024,
|
|
109
|
+
gzipSha256: "c".repeat(64),
|
|
110
|
+
gzipSize: 1000,
|
|
111
|
+
profile: "gzip9-0123456789abcdef",
|
|
112
|
+
},
|
|
106
113
|
},
|
|
107
114
|
],
|
|
108
115
|
});
|
|
109
116
|
expect(r.success).toBe(true);
|
|
110
117
|
});
|
|
111
118
|
|
|
119
|
+
it("mediaUploadInputSchema requires each variant's published digests (#1271)", () => {
|
|
120
|
+
const r = mediaUploadInputSchema.safeParse({
|
|
121
|
+
sha256: "a".repeat(64),
|
|
122
|
+
originalName: "hero.jpg",
|
|
123
|
+
mime: "image/jpeg",
|
|
124
|
+
sizeBytes: 1024,
|
|
125
|
+
width: 1920,
|
|
126
|
+
height: 1080,
|
|
127
|
+
alt: "",
|
|
128
|
+
storageKey: `${"a".repeat(64)}/orig.jpg`,
|
|
129
|
+
variants: [
|
|
130
|
+
{
|
|
131
|
+
variant: "orig",
|
|
132
|
+
format: "jpeg",
|
|
133
|
+
width: 1920,
|
|
134
|
+
height: 1080,
|
|
135
|
+
sizeBytes: 1024,
|
|
136
|
+
storageKey: `${"a".repeat(64)}/orig.jpg`,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
expect(r.success).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
112
143
|
it("mediaUploadInputSchema rejects non-hex sha256", () => {
|
|
113
144
|
const r = mediaUploadInputSchema.safeParse({
|
|
114
145
|
sha256: "not-a-sha",
|
package/src/media.ts
CHANGED
|
@@ -295,6 +295,21 @@ const mediaRefPattern =
|
|
|
295
295
|
|
|
296
296
|
const sha256Schema = z.string().regex(/^[0-9a-f]{64}$/, "must be hex sha256");
|
|
297
297
|
|
|
298
|
+
/**
|
|
299
|
+
* #1271 — the published digests of one stored media file (the static
|
|
300
|
+
* generator's `published-digest.ts`), stored with its variant row so no
|
|
301
|
+
* build reads the file to name or upload it.
|
|
302
|
+
*/
|
|
303
|
+
export const publishedDigestSchema = z
|
|
304
|
+
.object({
|
|
305
|
+
contentSha256: sha256Schema,
|
|
306
|
+
size: z.number().int().nonnegative(),
|
|
307
|
+
gzipSha256: sha256Schema,
|
|
308
|
+
gzipSize: z.number().int().nonnegative(),
|
|
309
|
+
profile: z.string().min(1).max(64),
|
|
310
|
+
})
|
|
311
|
+
.strict();
|
|
312
|
+
|
|
298
313
|
/**
|
|
299
314
|
* #531 — `library` assets are listed and placeable on pages; `reference`
|
|
300
315
|
* assets exist only to guide image generation/editing: hidden from the
|
|
@@ -345,6 +360,8 @@ export const mediaUploadInputSchema = z
|
|
|
345
360
|
height: z.number().int().positive().nullable(),
|
|
346
361
|
sizeBytes: z.number().int().positive(),
|
|
347
362
|
storageKey: z.string().min(1),
|
|
363
|
+
/** #1271 — taken from the bytes the writer stored (runMediaPipeline). */
|
|
364
|
+
digest: publishedDigestSchema,
|
|
348
365
|
}),
|
|
349
366
|
)
|
|
350
367
|
.min(1),
|