@happyvertical/smrt-core 0.55.12 → 0.55.13

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.
Files changed (39) hide show
  1. package/agents/cookbook.md +12 -3
  2. package/agents/object-runtime.md +11 -0
  3. package/dist/collection.d.ts.map +1 -1
  4. package/dist/collection.js +4 -1
  5. package/dist/collection.js.map +1 -1
  6. package/dist/cookbook/engine/document.d.ts.map +1 -1
  7. package/dist/cookbook/engine/document.js +16 -18
  8. package/dist/cookbook/engine/document.js.map +1 -1
  9. package/dist/cookbook/engine/types.d.ts +2 -0
  10. package/dist/cookbook/engine/types.d.ts.map +1 -1
  11. package/dist/cookbook/engine/types.js.map +1 -1
  12. package/dist/cookbook/v1.schema.json +60 -3
  13. package/dist/cookbook/validate.d.ts +40 -4
  14. package/dist/cookbook/validate.d.ts.map +1 -1
  15. package/dist/cookbook/validate.js +228 -58
  16. package/dist/cookbook/validate.js.map +1 -1
  17. package/dist/dispatch/bus.d.ts +9 -4
  18. package/dist/dispatch/bus.d.ts.map +1 -1
  19. package/dist/dispatch/bus.js +11 -6
  20. package/dist/dispatch/bus.js.map +1 -1
  21. package/dist/dispatch/collections/Dispatches.d.ts +15 -5
  22. package/dist/dispatch/collections/Dispatches.d.ts.map +1 -1
  23. package/dist/dispatch/collections/Dispatches.js +29 -9
  24. package/dist/dispatch/collections/Dispatches.js.map +1 -1
  25. package/dist/dispatch/types.d.ts +5 -1
  26. package/dist/dispatch/types.d.ts.map +1 -1
  27. package/dist/manifest/static-manifest.js +1 -1
  28. package/dist/manifest/static-manifest.js.map +1 -1
  29. package/dist/manifest/store.js +1 -1
  30. package/dist/manifest.json +1 -1
  31. package/dist/object.d.ts.map +1 -1
  32. package/dist/object.js +5 -1
  33. package/dist/object.js.map +1 -1
  34. package/dist/smrt-knowledge.json +7 -7
  35. package/dist/utils.d.ts +21 -0
  36. package/dist/utils.d.ts.map +1 -1
  37. package/dist/utils.js +52 -2
  38. package/dist/utils.js.map +1 -1
  39. package/package.json +10 -10
@@ -19,30 +19,52 @@ var SCHEMES = /* @__PURE__ */ new Set([
19
19
  "dark",
20
20
  "system"
21
21
  ]);
22
+ var SHELL_SLOTS = /* @__PURE__ */ new Set([
23
+ "header.start",
24
+ "header.center",
25
+ "header.end",
26
+ "footer.start",
27
+ "footer.center",
28
+ "footer.end",
29
+ "leftSidebar.header",
30
+ "leftSidebar.footer",
31
+ "rightSidebar.header",
32
+ "rightSidebar.footer"
33
+ ]);
22
34
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
23
35
  var isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
24
36
  var isStringRecord = (v, valid) => isObject(v) && !Object.hasOwn(v, "__proto__") && Object.values(v).every(valid);
25
- function checkPolicy(value, at, errors) {
26
- if (!isObject(value)) return errors.push(`${at} must be an object`);
27
- for (const key of ["objectRef", "fieldName"]) if (typeof value[key] !== "string" || !value[key]) errors.push(`${at}.${key} must be a non-empty string`);
28
- if (value.scopeType !== "app") errors.push(`${at}.scopeType must be "app"`);
37
+ var add = (diagnostics, severity, code, path, message) => {
38
+ diagnostics.push({
39
+ severity,
40
+ code,
41
+ path,
42
+ message
43
+ });
44
+ };
45
+ var error = (diagnostics, code, path, message) => add(diagnostics, "error", code, path, message);
46
+ var warning = (diagnostics, code, path, message) => add(diagnostics, "warning", code, path, message);
47
+ function checkPolicy(value, at, diagnostics) {
48
+ if (!isObject(value)) return error(diagnostics, "invalid_type", at, `${at} must be an object`);
49
+ for (const key of ["objectRef", "fieldName"]) if (typeof value[key] !== "string" || !value[key]) error(diagnostics, "invalid_value", `${at}.${key}`, `${at}.${key} must be a non-empty string`);
50
+ if (value.scopeType !== "app") error(diagnostics, "invalid_value", `${at}.scopeType`, `${at}.scopeType must be "app"`);
29
51
  for (const key of [
30
52
  "defaultValue",
31
53
  "help",
32
54
  "label"
33
55
  ]) {
34
56
  const v = value[key];
35
- if (v !== void 0 && v !== null && typeof v !== "string") errors.push(`${at}.${key} must be a string or null`);
57
+ if (v !== void 0 && v !== null && typeof v !== "string") error(diagnostics, "invalid_type", `${at}.${key}`, `${at}.${key} must be a string or null`);
36
58
  }
37
59
  const vis = value.visibility;
38
- if (vis != null && !(typeof vis === "string" && VISIBILITIES.has(vis))) errors.push(`${at}.visibility must be basic, advanced or hidden`);
60
+ if (vis != null && !(typeof vis === "string" && VISIBILITIES.has(vis))) error(diagnostics, "invalid_value", `${at}.visibility`, `${at}.visibility must be basic, advanced or hidden`);
39
61
  const order = value.displayOrder;
40
- if (order != null && !(typeof order === "number" && Number.isFinite(order))) errors.push(`${at}.displayOrder must be a finite number`);
41
- if (value.locked != null && typeof value.locked !== "boolean") errors.push(`${at}.locked must be a boolean`);
62
+ if (order != null && !(typeof order === "number" && Number.isFinite(order))) error(diagnostics, "invalid_value", `${at}.displayOrder`, `${at}.displayOrder must be a finite number`);
63
+ if (value.locked != null && typeof value.locked !== "boolean") error(diagnostics, "invalid_type", `${at}.locked`, `${at}.locked must be a boolean`);
42
64
  }
43
- function checkLayout(value, errors) {
44
- if (!isObject(value) || value.version !== 1) return errors.push("layout must be an object with version 1");
45
- for (const key of ["sectionOrder", "hidden"]) if (value[key] !== void 0 && !isStrings(value[key])) errors.push(`layout.${key} must be a list of strings`);
65
+ function checkLayout(value, diagnostics) {
66
+ if (!isObject(value) || value.version !== 1) return error(diagnostics, "invalid_value", "layout", "layout must be an object with version 1");
67
+ for (const key of ["sectionOrder", "hidden"]) if (value[key] !== void 0 && !isStrings(value[key])) error(diagnostics, "invalid_type", `layout.${key}`, `layout.${key} must be a list of strings`);
46
68
  for (const key of [
47
69
  "itemOrder",
48
70
  "moved",
@@ -50,33 +72,99 @@ function checkLayout(value, errors) {
50
72
  ]) {
51
73
  const v = value[key];
52
74
  if (v === void 0) continue;
53
- if (!(key === "itemOrder" ? isStringRecord(v, isStrings) : isStringRecord(v, (x) => typeof x === "string"))) errors.push(key === "itemOrder" ? "layout.itemOrder must map ids to lists of strings" : `layout.${key} must map ids to strings`);
75
+ if (!(key === "itemOrder" ? isStringRecord(v, isStrings) : isStringRecord(v, (x) => typeof x === "string"))) error(diagnostics, "invalid_type", `layout.${key}`, key === "itemOrder" ? "layout.itemOrder must map ids to lists of strings" : `layout.${key} must map ids to strings`);
76
+ }
77
+ if (isObject(value.placements)) {
78
+ for (const [id, slot] of Object.entries(value.placements)) if (typeof slot === "string" && !SHELL_SLOTS.has(slot)) error(diagnostics, "unknown_layout_slot", `layout.placements.${id}`, `Unknown shell slot "${slot}".`);
54
79
  }
55
80
  for (const key of [
56
81
  "sections",
57
82
  "items",
58
83
  "panels"
59
- ]) if (value[key] !== void 0 && !isStringRecord(value[key], (x) => isObject(x))) errors.push(`layout.${key} must map ids to objects`);
84
+ ]) if (value[key] !== void 0 && !isStringRecord(value[key], (x) => isObject(x))) error(diagnostics, "invalid_type", `layout.${key}`, `layout.${key} must map ids to objects`);
60
85
  const custom = value.customSections;
61
- if (custom !== void 0 && !(Array.isArray(custom) && custom.every((c) => isObject(c) && typeof c.id === "string" && typeof c.label === "string"))) errors.push("layout.customSections must be a list of { id, label }");
86
+ if (custom !== void 0 && !(Array.isArray(custom) && custom.every((c) => isObject(c) && typeof c.id === "string" && typeof c.label === "string"))) error(diagnostics, "invalid_type", "layout.customSections", "layout.customSections must be a list of { id, label }");
62
87
  }
63
- function checkTheme(value, errors) {
64
- if (!isObject(value)) return errors.push("theme must be an object");
65
- if (value.preset !== void 0 && typeof value.preset !== "string") errors.push("theme.preset must be a string");
66
- if (value.colorScheme !== void 0 && !(typeof value.colorScheme === "string" && SCHEMES.has(value.colorScheme))) errors.push("theme.colorScheme must be light, dark or system");
88
+ function checkTheme(value, diagnostics) {
89
+ if (!isObject(value)) return error(diagnostics, "invalid_type", "theme", "theme must be an object");
90
+ if (value.preset !== void 0 && typeof value.preset !== "string") error(diagnostics, "invalid_type", "theme.preset", "theme.preset must be a string");
91
+ if (value.colorScheme !== void 0 && !(typeof value.colorScheme === "string" && SCHEMES.has(value.colorScheme))) error(diagnostics, "invalid_value", "theme.colorScheme", "theme.colorScheme must be light, dark or system");
67
92
  const custom = value.custom;
68
93
  if (custom !== void 0) {
69
- if (!isObject(custom) || typeof custom.primary !== "string") errors.push("theme.custom must be an object with a string primary");
70
- else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) errors.push("theme.custom.primary must be a hex colour like #c2410c");
71
- else if (custom.fontFamily !== void 0 && typeof custom.fontFamily !== "string") errors.push("theme.custom.fontFamily must be a string");
94
+ if (!isObject(custom) || typeof custom.primary !== "string") error(diagnostics, "invalid_value", "theme.custom", "theme.custom must be an object with a string primary");
95
+ else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) error(diagnostics, "invalid_value", "theme.custom.primary", "theme.custom.primary must be a hex colour like #c2410c");
96
+ else if (custom.fontFamily !== void 0 && typeof custom.fontFamily !== "string") error(diagnostics, "invalid_type", "theme.custom.fontFamily", "theme.custom.fontFamily must be a string");
97
+ }
98
+ }
99
+ var OVERVIEW_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;
100
+ var WIDGET_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
101
+ var WIDGET_TYPE = /^[a-z][a-z0-9-]{0,31}$/;
102
+ var isOptionValue = (value) => value === null || typeof value === "string" || typeof value === "number" && Number.isFinite(value) || typeof value === "boolean";
103
+ function normalizeOverviewWidget(value, at, diagnostics) {
104
+ if (!isObject(value)) {
105
+ warning(diagnostics, "malformed_overview_widget", at, `${at} was dropped`);
106
+ return null;
72
107
  }
108
+ if ([
109
+ typeof value.id === "string" && WIDGET_ID.test(value.id),
110
+ typeof value.type === "string" && WIDGET_TYPE.test(value.type),
111
+ typeof value.span === "number" && Number.isInteger(value.span) && value.span >= 1 && value.span <= 4,
112
+ isObject(value.options) && Object.values(value.options).every(isOptionValue),
113
+ value.version === void 0 || typeof value.version === "number" && Number.isInteger(value.version) && value.version >= 1
114
+ ].every(Boolean)) return value;
115
+ warning(diagnostics, "malformed_overview_widget", at, `${at} was dropped because it is not a valid overview widget`);
116
+ return null;
73
117
  }
74
- function checkOverview(value, at, errors) {
75
- if (!isObject(value) || value.version !== 1) return errors.push(`${at} must be an object with version 1`);
76
- for (const key of ["order", "removed"]) if (value[key] !== void 0 && !isStrings(value[key])) errors.push(`${at}.${key} must be a list of strings`);
77
- const added = value.added;
78
- if (added !== void 0 && !(Array.isArray(added) && added.every((w) => isObject(w)))) errors.push(`${at}.added must be a list of widgets`);
79
- if (value.changed !== void 0 && !isStringRecord(value.changed, (x) => isObject(x))) errors.push(`${at}.changed must map widget ids to objects`);
118
+ function normalizeOverviewPatch(value, at, diagnostics) {
119
+ if (!isObject(value)) {
120
+ warning(diagnostics, "malformed_overview_change", at, `${at} was dropped`);
121
+ return null;
122
+ }
123
+ if ((value.span === void 0 || typeof value.span === "number" && Number.isInteger(value.span) && value.span >= 1 && value.span <= 4) && (value.options === void 0 || isObject(value.options) && Object.values(value.options).every(isOptionValue)) && (value.version === void 0 || typeof value.version === "number" && Number.isInteger(value.version) && value.version >= 1)) return value;
124
+ warning(diagnostics, "malformed_overview_change", at, `${at} was dropped because it is not a valid widget change`);
125
+ return null;
126
+ }
127
+ function normalizeOverview(value, at, diagnostics) {
128
+ if (!isObject(value) || value.version !== 1) {
129
+ warning(diagnostics, "malformed_overview", at, `${at} was dropped because it must be an object with version 1`);
130
+ return null;
131
+ }
132
+ const override = {
133
+ ...value,
134
+ version: 1
135
+ };
136
+ for (const key of ["order", "removed"]) if (value[key] !== void 0 && !isStrings(value[key])) {
137
+ warning(diagnostics, "malformed_overview_list", `${at}.${key}`, `${at}.${key} was dropped because it must be a list of strings`);
138
+ delete override[key];
139
+ } else if (value[key] !== void 0) override[key] = [...new Set(value[key])];
140
+ if (value.added !== void 0) if (!Array.isArray(value.added)) {
141
+ warning(diagnostics, "malformed_overview_widgets", `${at}.added`, `${at}.added was dropped because it must be a list of widgets`);
142
+ delete override.added;
143
+ } else {
144
+ const added = value.added.flatMap((widget, index) => {
145
+ const normalized = normalizeOverviewWidget(widget, `${at}.added[${index}]`, diagnostics);
146
+ return normalized ? [normalized] : [];
147
+ });
148
+ if (added.length) override.added = added;
149
+ else delete override.added;
150
+ }
151
+ if (value.changed !== void 0) if (!isObject(value.changed) || Object.hasOwn(value.changed, "__proto__")) {
152
+ warning(diagnostics, "malformed_overview_changes", `${at}.changed`, `${at}.changed was dropped because it must map widget ids to objects`);
153
+ delete override.changed;
154
+ } else {
155
+ const changed = {};
156
+ for (const [id, patch] of Object.entries(value.changed)) {
157
+ if (!WIDGET_ID.test(id)) {
158
+ warning(diagnostics, "malformed_overview_change", `${at}.changed.${id}`, `${at}.changed.${id} was dropped because its widget id is invalid`);
159
+ continue;
160
+ }
161
+ const normalized = normalizeOverviewPatch(patch, `${at}.changed.${id}`, diagnostics);
162
+ if (normalized) changed[id] = normalized;
163
+ }
164
+ if (Object.keys(changed).length) override.changed = changed;
165
+ else delete override.changed;
166
+ }
167
+ return override;
80
168
  }
81
169
  /**
82
170
  * Structural validation of an unknown value (parsed JSON). Returns the
@@ -84,56 +172,130 @@ function checkOverview(value, at, errors) {
84
172
  * recipes sorted and unique) or every problem found, each a readable line
85
173
  * naming the path. Unknown top-level keys are kept (additive fields in v1).
86
174
  *
87
- * Deep checks that need other packages (overview widgets against a page
88
- * definition, nav ids against recipes, theme presets) are the consumer's job;
89
- * recipe existence runs only when `options.recipes` is given.
175
+ * Deep checks use optional host-supplied catalogs. A structurally valid
176
+ * cookbook remains portable when those catalogs are absent. Invalid overview
177
+ * entries are dropped and reported, following the v1 overview contract.
90
178
  */
91
179
  function validateCookbook(input, options = {}) {
92
- if (!isObject(input)) return {
93
- ok: false,
94
- errors: ["expected a JSON object"]
95
- };
96
- const errors = [];
97
- const warnings = [];
180
+ const diagnostics = [];
181
+ if (!isObject(input)) {
182
+ error(diagnostics, "invalid_type", "", "expected a JSON object");
183
+ return {
184
+ ok: false,
185
+ errors: ["expected a JSON object"],
186
+ diagnostics
187
+ };
188
+ }
98
189
  const { $schema, version } = input;
99
- if ($schema !== void 0 && $schema !== "https://s-m-r-t.dev/schemas/cookbook/v1.json" && $schema !== "https://s-m-r-t.dev/schemas/blueprint/v1.json") errors.push(`$schema must be ${COOKBOOK_SCHEMA_URL}`);
100
- if (version === void 0) errors.push("version is required");
101
- else if (typeof version !== "number" || !Number.isInteger(version)) errors.push("version must be a whole number");
102
- else if (version > 1) errors.push(`version ${version} is newer than this reader understands (1)`);
103
- else if (version !== 1) errors.push(`version ${version} is not supported`);
104
- if ($schema === "https://s-m-r-t.dev/schemas/blueprint/v1.json") warnings.push("$schema uses the legacy blueprint URL; migrated");
105
- for (const key of ["name", "description"]) if (input[key] !== void 0 && typeof input[key] !== "string") errors.push(`${key} must be a string`);
190
+ if ($schema !== void 0 && $schema !== "https://s-m-r-t.dev/schemas/cookbook/v1.json" && $schema !== "https://s-m-r-t.dev/schemas/blueprint/v1.json") error(diagnostics, "invalid_schema", "$schema", `$schema must be ${COOKBOOK_SCHEMA_URL}`);
191
+ if (version === void 0) error(diagnostics, "missing_field", "version", "version is required");
192
+ else if (typeof version !== "number" || !Number.isInteger(version)) error(diagnostics, "invalid_type", "version", "version must be a whole number");
193
+ else if (version > 1) error(diagnostics, "unsupported_version", "version", `version ${version} is newer than this reader understands (1)`);
194
+ else if (version !== 1) error(diagnostics, "unsupported_version", "version", `version ${version} is not supported`);
195
+ if ($schema === "https://s-m-r-t.dev/schemas/blueprint/v1.json") warning(diagnostics, "migrated_schema", "$schema", "$schema uses the legacy blueprint URL; migrated");
196
+ for (const key of ["name", "description"]) if (input[key] !== void 0 && typeof input[key] !== "string") error(diagnostics, "invalid_type", key, `${key} must be a string`);
106
197
  let recipes = [];
107
- if (!isStrings(input.recipes)) errors.push("recipes must be a list of recipe ids");
198
+ if (!isStrings(input.recipes)) error(diagnostics, "invalid_type", "recipes", "recipes must be a list of recipe ids");
108
199
  else {
109
200
  recipes = [...new Set(input.recipes)].sort();
110
201
  if (options.recipes) {
111
202
  const known = new Set(options.recipes);
112
- for (const id of recipes) if (!known.has(id)) errors.push(`recipes: unknown recipe "${id}"`);
203
+ for (const id of recipes) if (!known.has(id)) error(diagnostics, "unknown_recipe", `recipes[${input.recipes.indexOf(id)}]`, `recipes: unknown recipe "${id}"`);
113
204
  }
114
205
  }
115
206
  let features = [];
116
- if (input.features !== void 0) if (!isStrings(input.features)) errors.push("features must be a list of model names");
207
+ if (input.features !== void 0) if (!isStrings(input.features)) error(diagnostics, "invalid_type", "features", "features must be a list of model names");
117
208
  else {
118
209
  features = [...new Set(input.features)].sort();
119
- if (features.length !== input.features.length) warnings.push("features listed a model more than once; deduplicated");
210
+ if (features.length !== input.features.length) warning(diagnostics, "duplicate_value", "features", "features listed a model more than once; deduplicated");
120
211
  }
121
212
  const policies = [];
122
- if (!Array.isArray(input.policies)) errors.push("policies must be a list");
213
+ if (!Array.isArray(input.policies)) error(diagnostics, "invalid_type", "policies", "policies must be a list");
123
214
  else for (const [i, row] of input.policies.entries()) {
124
- checkPolicy(row, `policies[${i}]`, errors);
215
+ checkPolicy(row, `policies[${i}]`, diagnostics);
125
216
  policies.push(row);
126
217
  }
127
218
  if (input.exposure !== void 0) {
128
- if (!isStringRecord(input.exposure, (s) => isStrings(s) && s.every((x) => SURFACES.has(x)))) errors.push("exposure must map model names to lists of api, mcp or cli");
219
+ if (!isStringRecord(input.exposure, (s) => isStrings(s) && s.every((x) => SURFACES.has(x)))) error(diagnostics, "invalid_value", "exposure", "exposure must map model names to lists of api, mcp or cli");
220
+ }
221
+ if (input.layout !== void 0) checkLayout(input.layout, diagnostics);
222
+ if (input.theme !== void 0) checkTheme(input.theme, diagnostics);
223
+ if (isObject(input.theme) && typeof input.theme.preset === "string") {
224
+ const presets = options.themePresets ? new Set(options.themePresets) : void 0;
225
+ if (presets && !presets.has(input.theme.preset)) error(diagnostics, "unknown_theme_preset", "theme.preset", `Unknown theme preset "${input.theme.preset}".`);
226
+ }
227
+ if (isObject(input.layout) && input.layout.version === 1 && options.layout) {
228
+ const sections = options.layout.sections ? new Set(options.layout.sections) : void 0;
229
+ const items = options.layout.items ? new Set(options.layout.items) : void 0;
230
+ const placementItems = options.layout.placementItems ? new Set(options.layout.placementItems) : void 0;
231
+ if (sections && Array.isArray(input.layout.customSections)) {
232
+ for (const section of input.layout.customSections) if (isObject(section) && typeof section.id === "string") sections.add(section.id);
233
+ }
234
+ const known = (id, kind) => kind === "section" ? sections?.has(id) !== false : kind === "item" ? items?.has(id) !== false : sections?.has(id) === true || items?.has(id) === true || !sections && !items;
235
+ const check = (id, path, kind) => {
236
+ if (typeof id === "string" && !known(id, kind)) error(diagnostics, "unknown_layout_id", path, `Unknown layout ${kind === "either" ? "section or item" : kind} "${id}".`);
237
+ };
238
+ if (isStrings(input.layout.sectionOrder)) input.layout.sectionOrder.forEach((id, index) => {
239
+ check(id, `layout.sectionOrder[${index}]`, "section");
240
+ });
241
+ if (isStrings(input.layout.hidden)) input.layout.hidden.forEach((id, index) => {
242
+ check(id, `layout.hidden[${index}]`, "either");
243
+ });
244
+ if (isObject(input.layout.itemOrder)) for (const [section, order] of Object.entries(input.layout.itemOrder)) {
245
+ check(section, `layout.itemOrder.${section}`, "section");
246
+ if (isStrings(order)) order.forEach((id, index) => {
247
+ check(id, `layout.itemOrder.${section}[${index}]`, "item");
248
+ });
249
+ }
250
+ if (isObject(input.layout.moved)) for (const [id, section] of Object.entries(input.layout.moved)) {
251
+ check(id, `layout.moved.${id}`, "item");
252
+ check(section, `layout.moved.${id}`, "section");
253
+ }
254
+ for (const key of ["sections", "items"]) {
255
+ if (!isObject(input.layout[key])) continue;
256
+ for (const id of Object.keys(input.layout[key])) check(id, `layout.${key}.${id}`, key === "sections" ? "section" : "item");
257
+ }
258
+ if (isObject(input.layout.placements)) {
259
+ for (const id of Object.keys(input.layout.placements)) if (placementItems && !placementItems.has(id)) error(diagnostics, "unknown_layout_id", `layout.placements.${id}`, `Unknown layout placement item "${id}".`);
260
+ }
261
+ }
262
+ const overviews = {};
263
+ if (input.overviews !== void 0) if (!isObject(input.overviews) || Object.hasOwn(input.overviews, "__proto__")) error(diagnostics, "invalid_type", "overviews", "overviews must map overview ids to overrides");
264
+ else {
265
+ const overviewIds = options.overviewIds ? new Set(options.overviewIds) : void 0;
266
+ for (const [id, value] of Object.entries(input.overviews)) {
267
+ const at = `overviews.${id}`;
268
+ if (!OVERVIEW_ID.test(id)) {
269
+ warning(diagnostics, "invalid_overview_id", at, `${at} was dropped because its overview id is invalid`);
270
+ continue;
271
+ }
272
+ if (overviewIds && !overviewIds.has(id)) {
273
+ warning(diagnostics, "unknown_overview", at, `Unknown overview "${id}" was dropped.`);
274
+ continue;
275
+ }
276
+ const override = normalizeOverview(value, at, diagnostics);
277
+ if (!override) continue;
278
+ if (!options.validateOverview) {
279
+ overviews[id] = override;
280
+ continue;
281
+ }
282
+ let checked;
283
+ try {
284
+ checked = options.validateOverview(id, override);
285
+ } catch {
286
+ warning(diagnostics, "overview_validation_failed", at, `Overview "${id}" was dropped because its host validator failed.`);
287
+ continue;
288
+ }
289
+ for (const issue of checked.diagnostics ?? []) warning(diagnostics, issue.code, issue.path || at, issue.message);
290
+ if (!checked.ok && checked.diagnostics.length === 0) warning(diagnostics, "overview_rejected", at, `Overview "${id}" was rejected by its host validator and dropped.`);
291
+ if (checked.ok && checked.override) overviews[id] = checked.override;
292
+ }
129
293
  }
130
- if (input.layout !== void 0) checkLayout(input.layout, errors);
131
- if (input.theme !== void 0) checkTheme(input.theme, errors);
132
- if (input.overviews !== void 0) if (!isObject(input.overviews) || Object.hasOwn(input.overviews, "__proto__")) errors.push("overviews must map overview ids to overrides");
133
- else for (const [id, value] of Object.entries(input.overviews)) checkOverview(value, `overviews.${id}`, errors);
294
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
134
295
  if (errors.length) return {
135
296
  ok: false,
136
- errors
297
+ errors,
298
+ diagnostics
137
299
  };
138
300
  const cookbook = {
139
301
  ...input,
@@ -146,11 +308,13 @@ function validateCookbook(input, options = {}) {
146
308
  if (input.exposure !== void 0) cookbook.exposure = input.exposure;
147
309
  if (input.layout !== void 0) cookbook.layout = input.layout;
148
310
  if (input.theme !== void 0) cookbook.theme = input.theme;
149
- if (input.overviews !== void 0) cookbook.overviews = input.overviews;
311
+ delete cookbook.overviews;
312
+ if (Object.keys(overviews).length) cookbook.overviews = overviews;
150
313
  return {
151
314
  ok: true,
152
315
  cookbook,
153
- warnings
316
+ warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").map((diagnostic) => diagnostic.message),
317
+ diagnostics
154
318
  };
155
319
  }
156
320
  /** Parse JSON text, then {@link validateCookbook} it. */
@@ -161,7 +325,13 @@ function parseCookbookText(text, options = {}) {
161
325
  } catch {
162
326
  return {
163
327
  ok: false,
164
- errors: ["not valid JSON"]
328
+ errors: ["not valid JSON"],
329
+ diagnostics: [{
330
+ severity: "error",
331
+ code: "invalid_json",
332
+ path: "",
333
+ message: "not valid JSON"
334
+ }]
165
335
  };
166
336
  }
167
337
  return validateCookbook(value, options);
@@ -1 +1 @@
1
- {"version":3,"file":"validate.js","names":[],"sources":["../../src/cookbook/validate.ts"],"sourcesContent":["import type {\n Cookbook,\n CookbookExposureSurface,\n CookbookLayout,\n CookbookOverviewOverride,\n CookbookPolicyRow,\n CookbookTheme,\n} from '@happyvertical/smrt-types';\n\nexport type { Cookbook } from '@happyvertical/smrt-types';\n\n/** What a v1 document carries in `$schema`. */\nexport const COOKBOOK_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/cookbook/v1.json';\n/** Accepted on read (the document used to be called a blueprint); never written. */\nexport const COOKBOOK_LEGACY_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/blueprint/v1.json';\nexport const COOKBOOK_VERSION = 1;\n\nexport interface CookbookValidateOptions {\n /**\n * The recipe ids that exist (from published manifests). When given, an id\n * outside it is an error. When absent no existence check runs: that is the\n * seam `smrt cookbook validate` fills.\n */\n recipes?: Iterable<string>;\n}\n\nexport type CookbookValidation =\n | { ok: true; cookbook: Cookbook; warnings: string[] }\n | { ok: false; errors: string[] };\n\nconst VISIBILITIES = new Set(['basic', 'advanced', 'hidden']);\nconst SURFACES = new Set(['api', 'mcp', 'cli']);\nconst SCHEMES = new Set(['light', 'dark', 'system']);\n\nconst isObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\nconst isStrings = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((x) => typeof x === 'string');\nconst isStringRecord = (v: unknown, valid: (x: unknown) => boolean) =>\n isObject(v) &&\n !Object.hasOwn(v, '__proto__') &&\n Object.values(v).every(valid);\n\nfunction checkPolicy(value: unknown, at: string, errors: string[]) {\n if (!isObject(value)) return errors.push(`${at} must be an object`);\n for (const key of ['objectRef', 'fieldName'] as const) {\n if (typeof value[key] !== 'string' || !value[key]) {\n errors.push(`${at}.${key} must be a non-empty string`);\n }\n }\n if (value.scopeType !== 'app') errors.push(`${at}.scopeType must be \"app\"`);\n for (const key of ['defaultValue', 'help', 'label'] as const) {\n const v = value[key];\n if (v !== undefined && v !== null && typeof v !== 'string') {\n errors.push(`${at}.${key} must be a string or null`);\n }\n }\n const vis = value.visibility;\n if (vis != null && !(typeof vis === 'string' && VISIBILITIES.has(vis))) {\n errors.push(`${at}.visibility must be basic, advanced or hidden`);\n }\n const order = value.displayOrder;\n if (order != null && !(typeof order === 'number' && Number.isFinite(order))) {\n errors.push(`${at}.displayOrder must be a finite number`);\n }\n if (value.locked != null && typeof value.locked !== 'boolean') {\n errors.push(`${at}.locked must be a boolean`);\n }\n}\n\nfunction checkLayout(value: unknown, errors: string[]) {\n if (!isObject(value) || value.version !== 1) {\n return errors.push('layout must be an object with version 1');\n }\n for (const key of ['sectionOrder', 'hidden'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n errors.push(`layout.${key} must be a list of strings`);\n }\n }\n for (const key of ['itemOrder', 'moved', 'placements'] as const) {\n const v = value[key];\n if (v === undefined) continue;\n const ok =\n key === 'itemOrder'\n ? isStringRecord(v, isStrings)\n : isStringRecord(v, (x) => typeof x === 'string');\n if (!ok) {\n errors.push(\n key === 'itemOrder'\n ? 'layout.itemOrder must map ids to lists of strings'\n : `layout.${key} must map ids to strings`,\n );\n }\n }\n for (const key of ['sections', 'items', 'panels'] as const) {\n if (\n value[key] !== undefined &&\n !isStringRecord(value[key], (x) => isObject(x))\n ) {\n errors.push(`layout.${key} must map ids to objects`);\n }\n }\n const custom = value.customSections;\n if (\n custom !== undefined &&\n !(\n Array.isArray(custom) &&\n custom.every(\n (c) =>\n isObject(c) &&\n typeof c.id === 'string' &&\n typeof c.label === 'string',\n )\n )\n ) {\n errors.push('layout.customSections must be a list of { id, label }');\n }\n}\n\nfunction checkTheme(value: unknown, errors: string[]) {\n if (!isObject(value)) return errors.push('theme must be an object');\n if (value.preset !== undefined && typeof value.preset !== 'string') {\n errors.push('theme.preset must be a string');\n }\n if (\n value.colorScheme !== undefined &&\n !(typeof value.colorScheme === 'string' && SCHEMES.has(value.colorScheme))\n ) {\n errors.push('theme.colorScheme must be light, dark or system');\n }\n const custom = value.custom;\n if (custom !== undefined) {\n if (!isObject(custom) || typeof custom.primary !== 'string') {\n errors.push('theme.custom must be an object with a string primary');\n } else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) {\n errors.push('theme.custom.primary must be a hex colour like #c2410c');\n } else if (\n custom.fontFamily !== undefined &&\n typeof custom.fontFamily !== 'string'\n ) {\n errors.push('theme.custom.fontFamily must be a string');\n }\n }\n}\n\nfunction checkOverview(value: unknown, at: string, errors: string[]) {\n if (!isObject(value) || value.version !== 1) {\n return errors.push(`${at} must be an object with version 1`);\n }\n for (const key of ['order', 'removed'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n errors.push(`${at}.${key} must be a list of strings`);\n }\n }\n const added = value.added;\n if (\n added !== undefined &&\n !(Array.isArray(added) && added.every((w) => isObject(w)))\n ) {\n errors.push(`${at}.added must be a list of widgets`);\n }\n if (\n value.changed !== undefined &&\n !isStringRecord(value.changed, (x) => isObject(x))\n ) {\n errors.push(`${at}.changed must map widget ids to objects`);\n }\n}\n\n/**\n * Structural validation of an unknown value (parsed JSON). Returns the\n * normalised document (legacy `$schema` rewritten, `features` defaulted,\n * recipes sorted and unique) or every problem found, each a readable line\n * naming the path. Unknown top-level keys are kept (additive fields in v1).\n *\n * Deep checks that need other packages (overview widgets against a page\n * definition, nav ids against recipes, theme presets) are the consumer's job;\n * recipe existence runs only when `options.recipes` is given.\n */\nexport function validateCookbook(\n input: unknown,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n if (!isObject(input)) {\n return { ok: false, errors: ['expected a JSON object'] };\n }\n const errors: string[] = [];\n const warnings: string[] = [];\n\n const { $schema, version } = input;\n if (\n $schema !== undefined &&\n $schema !== COOKBOOK_SCHEMA_URL &&\n $schema !== COOKBOOK_LEGACY_SCHEMA_URL\n ) {\n errors.push(`$schema must be ${COOKBOOK_SCHEMA_URL}`);\n }\n if (version === undefined) {\n errors.push('version is required');\n } else if (typeof version !== 'number' || !Number.isInteger(version)) {\n errors.push('version must be a whole number');\n } else if (version > COOKBOOK_VERSION) {\n errors.push(\n `version ${version} is newer than this reader understands (${COOKBOOK_VERSION})`,\n );\n } else if (version !== COOKBOOK_VERSION) {\n errors.push(`version ${version} is not supported`);\n }\n if ($schema === COOKBOOK_LEGACY_SCHEMA_URL) {\n warnings.push('$schema uses the legacy blueprint URL; migrated');\n }\n\n for (const key of ['name', 'description'] as const) {\n if (input[key] !== undefined && typeof input[key] !== 'string') {\n errors.push(`${key} must be a string`);\n }\n }\n\n let recipes: string[] = [];\n if (!isStrings(input.recipes)) {\n errors.push('recipes must be a list of recipe ids');\n } else {\n recipes = [...new Set(input.recipes)].sort();\n if (options.recipes) {\n const known = new Set(options.recipes);\n for (const id of recipes) {\n if (!known.has(id)) errors.push(`recipes: unknown recipe \"${id}\"`);\n }\n }\n }\n\n let features: string[] = [];\n if (input.features !== undefined) {\n if (!isStrings(input.features)) {\n errors.push('features must be a list of model names');\n } else {\n features = [...new Set(input.features)].sort();\n if (features.length !== input.features.length) {\n warnings.push('features listed a model more than once; deduplicated');\n }\n }\n }\n\n const policies: CookbookPolicyRow[] = [];\n if (!Array.isArray(input.policies)) {\n errors.push('policies must be a list');\n } else {\n for (const [i, row] of input.policies.entries()) {\n checkPolicy(row, `policies[${i}]`, errors);\n policies.push(row as CookbookPolicyRow);\n }\n }\n\n if (input.exposure !== undefined) {\n const ok = isStringRecord(\n input.exposure,\n (s) => isStrings(s) && s.every((x) => SURFACES.has(x)),\n );\n if (!ok)\n errors.push('exposure must map model names to lists of api, mcp or cli');\n }\n if (input.layout !== undefined) checkLayout(input.layout, errors);\n if (input.theme !== undefined) checkTheme(input.theme, errors);\n if (input.overviews !== undefined) {\n if (\n !isObject(input.overviews) ||\n Object.hasOwn(input.overviews, '__proto__')\n ) {\n errors.push('overviews must map overview ids to overrides');\n } else {\n for (const [id, value] of Object.entries(input.overviews)) {\n checkOverview(value, `overviews.${id}`, errors);\n }\n }\n }\n\n if (errors.length) return { ok: false, errors };\n\n const cookbook = {\n ...input,\n $schema: COOKBOOK_SCHEMA_URL,\n version: COOKBOOK_VERSION,\n recipes,\n features,\n policies,\n } as unknown as Cookbook;\n if (input.exposure !== undefined) {\n cookbook.exposure = input.exposure as Record<\n string,\n CookbookExposureSurface[]\n >;\n }\n if (input.layout !== undefined)\n cookbook.layout = input.layout as CookbookLayout;\n if (input.theme !== undefined) cookbook.theme = input.theme as CookbookTheme;\n if (input.overviews !== undefined) {\n cookbook.overviews = input.overviews as Record<\n string,\n CookbookOverviewOverride\n >;\n }\n return { ok: true, cookbook, warnings };\n}\n\n/** Parse JSON text, then {@link validateCookbook} it. */\nexport function parseCookbookText(\n text: string,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return { ok: false, errors: ['not valid JSON'] };\n }\n return validateCookbook(value, options);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"validate.js","names":[],"sources":["../../src/cookbook/validate.ts"],"sourcesContent":["import type {\n Cookbook,\n CookbookExposureSurface,\n CookbookLayout,\n CookbookOverviewOverride,\n CookbookOverviewWidget,\n CookbookPolicyRow,\n CookbookTheme,\n RecipeShellSlot,\n} from '@happyvertical/smrt-types';\n\nexport type { Cookbook } from '@happyvertical/smrt-types';\n\n/** What a v1 document carries in `$schema`. */\nexport const COOKBOOK_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/cookbook/v1.json';\n/** Accepted on read (the document used to be called a blueprint); never written. */\nexport const COOKBOOK_LEGACY_SCHEMA_URL =\n 'https://s-m-r-t.dev/schemas/blueprint/v1.json';\nexport const COOKBOOK_VERSION = 1;\n\nexport interface CookbookValidateOptions {\n /**\n * The recipe ids that exist (from published manifests). When given, an id\n * outside it is an error. When absent no existence check runs: that is the\n * seam `smrt cookbook validate` fills.\n */\n recipes?: Iterable<string>;\n /** Built-in or host-registered theme preset ids. */\n themePresets?: Iterable<string>;\n /** Manifest-derived ids the cookbook layout may reference. */\n layout?: {\n sections?: Iterable<string>;\n items?: Iterable<string>;\n /** Movable shell item ids resolved by the host (`dock:`, `slot:`, or host ids). */\n placementItems?: Iterable<string>;\n };\n /** Overview page ids defined by the resolved app shell. */\n overviewIds?: Iterable<string>;\n /**\n * Validate and canonicalize one structurally valid override against its page\n * definition and widget registry. The callback is trusted host code; the\n * cookbook value remains plain data and is never executed.\n */\n validateOverview?: (\n id: string,\n override: CookbookOverviewOverride,\n ) => CookbookOverviewValidation;\n}\n\nexport interface CookbookDiagnostic {\n severity: 'error' | 'warning';\n /** Stable machine-readable category. Host validators may add their own. */\n code: string;\n /** Path into the cookbook, using the existing dotted/indexed notation. */\n path: string;\n message: string;\n}\n\nexport type CookbookOverviewValidation =\n | {\n ok: true;\n /** `null` means the canonical override is empty and should be omitted. */\n override: CookbookOverviewOverride | null;\n diagnostics?: readonly CookbookDiagnostic[];\n }\n | { ok: false; diagnostics: readonly CookbookDiagnostic[] };\n\nexport type CookbookValidation =\n | {\n ok: true;\n cookbook: Cookbook;\n warnings: string[];\n diagnostics: CookbookDiagnostic[];\n }\n | { ok: false; errors: string[]; diagnostics: CookbookDiagnostic[] };\n\nconst VISIBILITIES = new Set(['basic', 'advanced', 'hidden']);\nconst SURFACES = new Set(['api', 'mcp', 'cli']);\nconst SCHEMES = new Set(['light', 'dark', 'system']);\nconst SHELL_SLOT_VALUES = [\n 'header.start',\n 'header.center',\n 'header.end',\n 'footer.start',\n 'footer.center',\n 'footer.end',\n 'leftSidebar.header',\n 'leftSidebar.footer',\n 'rightSidebar.header',\n 'rightSidebar.footer',\n] as const satisfies readonly RecipeShellSlot[];\nconst SHELL_SLOTS = new Set<string>(SHELL_SLOT_VALUES);\n\nconst isObject = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v);\nconst isStrings = (v: unknown): v is string[] =>\n Array.isArray(v) && v.every((x) => typeof x === 'string');\nconst isStringRecord = (v: unknown, valid: (x: unknown) => boolean) =>\n isObject(v) &&\n !Object.hasOwn(v, '__proto__') &&\n Object.values(v).every(valid);\n\nconst add = (\n diagnostics: CookbookDiagnostic[],\n severity: CookbookDiagnostic['severity'],\n code: string,\n path: string,\n message: string,\n): void => {\n diagnostics.push({ severity, code, path, message });\n};\n\nconst error = (\n diagnostics: CookbookDiagnostic[],\n code: string,\n path: string,\n message: string,\n): void => add(diagnostics, 'error', code, path, message);\n\nconst warning = (\n diagnostics: CookbookDiagnostic[],\n code: string,\n path: string,\n message: string,\n): void => add(diagnostics, 'warning', code, path, message);\n\nfunction checkPolicy(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n) {\n if (!isObject(value))\n return error(diagnostics, 'invalid_type', at, `${at} must be an object`);\n for (const key of ['objectRef', 'fieldName'] as const) {\n if (typeof value[key] !== 'string' || !value[key]) {\n error(\n diagnostics,\n 'invalid_value',\n `${at}.${key}`,\n `${at}.${key} must be a non-empty string`,\n );\n }\n }\n if (value.scopeType !== 'app')\n error(\n diagnostics,\n 'invalid_value',\n `${at}.scopeType`,\n `${at}.scopeType must be \"app\"`,\n );\n for (const key of ['defaultValue', 'help', 'label'] as const) {\n const v = value[key];\n if (v !== undefined && v !== null && typeof v !== 'string') {\n error(\n diagnostics,\n 'invalid_type',\n `${at}.${key}`,\n `${at}.${key} must be a string or null`,\n );\n }\n }\n const vis = value.visibility;\n if (vis != null && !(typeof vis === 'string' && VISIBILITIES.has(vis))) {\n error(\n diagnostics,\n 'invalid_value',\n `${at}.visibility`,\n `${at}.visibility must be basic, advanced or hidden`,\n );\n }\n const order = value.displayOrder;\n if (order != null && !(typeof order === 'number' && Number.isFinite(order))) {\n error(\n diagnostics,\n 'invalid_value',\n `${at}.displayOrder`,\n `${at}.displayOrder must be a finite number`,\n );\n }\n if (value.locked != null && typeof value.locked !== 'boolean') {\n error(\n diagnostics,\n 'invalid_type',\n `${at}.locked`,\n `${at}.locked must be a boolean`,\n );\n }\n}\n\nfunction checkLayout(value: unknown, diagnostics: CookbookDiagnostic[]) {\n if (!isObject(value) || value.version !== 1) {\n return error(\n diagnostics,\n 'invalid_value',\n 'layout',\n 'layout must be an object with version 1',\n );\n }\n for (const key of ['sectionOrder', 'hidden'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n error(\n diagnostics,\n 'invalid_type',\n `layout.${key}`,\n `layout.${key} must be a list of strings`,\n );\n }\n }\n for (const key of ['itemOrder', 'moved', 'placements'] as const) {\n const v = value[key];\n if (v === undefined) continue;\n const ok =\n key === 'itemOrder'\n ? isStringRecord(v, isStrings)\n : isStringRecord(v, (x) => typeof x === 'string');\n if (!ok) {\n error(\n diagnostics,\n 'invalid_type',\n `layout.${key}`,\n key === 'itemOrder'\n ? 'layout.itemOrder must map ids to lists of strings'\n : `layout.${key} must map ids to strings`,\n );\n }\n }\n if (isObject(value.placements)) {\n for (const [id, slot] of Object.entries(value.placements)) {\n if (typeof slot === 'string' && !SHELL_SLOTS.has(slot)) {\n error(\n diagnostics,\n 'unknown_layout_slot',\n `layout.placements.${id}`,\n `Unknown shell slot \"${slot}\".`,\n );\n }\n }\n }\n for (const key of ['sections', 'items', 'panels'] as const) {\n if (\n value[key] !== undefined &&\n !isStringRecord(value[key], (x) => isObject(x))\n ) {\n error(\n diagnostics,\n 'invalid_type',\n `layout.${key}`,\n `layout.${key} must map ids to objects`,\n );\n }\n }\n const custom = value.customSections;\n if (\n custom !== undefined &&\n !(\n Array.isArray(custom) &&\n custom.every(\n (c) =>\n isObject(c) &&\n typeof c.id === 'string' &&\n typeof c.label === 'string',\n )\n )\n ) {\n error(\n diagnostics,\n 'invalid_type',\n 'layout.customSections',\n 'layout.customSections must be a list of { id, label }',\n );\n }\n}\n\nfunction checkTheme(value: unknown, diagnostics: CookbookDiagnostic[]) {\n if (!isObject(value))\n return error(\n diagnostics,\n 'invalid_type',\n 'theme',\n 'theme must be an object',\n );\n if (value.preset !== undefined && typeof value.preset !== 'string') {\n error(\n diagnostics,\n 'invalid_type',\n 'theme.preset',\n 'theme.preset must be a string',\n );\n }\n if (\n value.colorScheme !== undefined &&\n !(typeof value.colorScheme === 'string' && SCHEMES.has(value.colorScheme))\n ) {\n error(\n diagnostics,\n 'invalid_value',\n 'theme.colorScheme',\n 'theme.colorScheme must be light, dark or system',\n );\n }\n const custom = value.custom;\n if (custom !== undefined) {\n if (!isObject(custom) || typeof custom.primary !== 'string') {\n error(\n diagnostics,\n 'invalid_value',\n 'theme.custom',\n 'theme.custom must be an object with a string primary',\n );\n } else if (!/^#[0-9a-fA-F]{6}$/.test(custom.primary)) {\n error(\n diagnostics,\n 'invalid_value',\n 'theme.custom.primary',\n 'theme.custom.primary must be a hex colour like #c2410c',\n );\n } else if (\n custom.fontFamily !== undefined &&\n typeof custom.fontFamily !== 'string'\n ) {\n error(\n diagnostics,\n 'invalid_type',\n 'theme.custom.fontFamily',\n 'theme.custom.fontFamily must be a string',\n );\n }\n }\n}\n\nconst OVERVIEW_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;\nconst WIDGET_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;\nconst WIDGET_TYPE = /^[a-z][a-z0-9-]{0,31}$/;\n\nconst isOptionValue = (value: unknown) =>\n value === null ||\n typeof value === 'string' ||\n (typeof value === 'number' && Number.isFinite(value)) ||\n typeof value === 'boolean';\n\nfunction normalizeOverviewWidget(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n): CookbookOverviewWidget | null {\n if (!isObject(value)) {\n warning(diagnostics, 'malformed_overview_widget', at, `${at} was dropped`);\n return null;\n }\n const valid = [\n typeof value.id === 'string' && WIDGET_ID.test(value.id),\n typeof value.type === 'string' && WIDGET_TYPE.test(value.type),\n typeof value.span === 'number' &&\n Number.isInteger(value.span) &&\n value.span >= 1 &&\n value.span <= 4,\n isObject(value.options) &&\n Object.values(value.options).every(isOptionValue),\n value.version === undefined ||\n (typeof value.version === 'number' &&\n Number.isInteger(value.version) &&\n value.version >= 1),\n ];\n if (valid.every(Boolean)) return value as unknown as CookbookOverviewWidget;\n warning(\n diagnostics,\n 'malformed_overview_widget',\n at,\n `${at} was dropped because it is not a valid overview widget`,\n );\n return null;\n}\n\nfunction normalizeOverviewPatch(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n): NonNullable<CookbookOverviewOverride['changed']>[string] | null {\n if (!isObject(value)) {\n warning(diagnostics, 'malformed_overview_change', at, `${at} was dropped`);\n return null;\n }\n const valid =\n (value.span === undefined ||\n (typeof value.span === 'number' &&\n Number.isInteger(value.span) &&\n value.span >= 1 &&\n value.span <= 4)) &&\n (value.options === undefined ||\n (isObject(value.options) &&\n Object.values(value.options).every(isOptionValue))) &&\n (value.version === undefined ||\n (typeof value.version === 'number' &&\n Number.isInteger(value.version) &&\n value.version >= 1));\n if (valid) {\n return value as NonNullable<CookbookOverviewOverride['changed']>[string];\n }\n warning(\n diagnostics,\n 'malformed_overview_change',\n at,\n `${at} was dropped because it is not a valid widget change`,\n );\n return null;\n}\n\nfunction normalizeOverview(\n value: unknown,\n at: string,\n diagnostics: CookbookDiagnostic[],\n): CookbookOverviewOverride | null {\n if (!isObject(value) || value.version !== 1) {\n warning(\n diagnostics,\n 'malformed_overview',\n at,\n `${at} was dropped because it must be an object with version 1`,\n );\n return null;\n }\n const override = {\n ...value,\n version: 1,\n } as unknown as CookbookOverviewOverride;\n for (const key of ['order', 'removed'] as const) {\n if (value[key] !== undefined && !isStrings(value[key])) {\n warning(\n diagnostics,\n 'malformed_overview_list',\n `${at}.${key}`,\n `${at}.${key} was dropped because it must be a list of strings`,\n );\n delete override[key];\n } else if (value[key] !== undefined) {\n override[key] = [...new Set(value[key])];\n }\n }\n if (value.added !== undefined) {\n if (!Array.isArray(value.added)) {\n warning(\n diagnostics,\n 'malformed_overview_widgets',\n `${at}.added`,\n `${at}.added was dropped because it must be a list of widgets`,\n );\n delete override.added;\n } else {\n const added = value.added.flatMap((widget, index) => {\n const normalized = normalizeOverviewWidget(\n widget,\n `${at}.added[${index}]`,\n diagnostics,\n );\n return normalized ? [normalized] : [];\n });\n if (added.length) override.added = added;\n else delete override.added;\n }\n }\n if (value.changed !== undefined) {\n if (!isObject(value.changed) || Object.hasOwn(value.changed, '__proto__')) {\n warning(\n diagnostics,\n 'malformed_overview_changes',\n `${at}.changed`,\n `${at}.changed was dropped because it must map widget ids to objects`,\n );\n delete override.changed;\n } else {\n const changed: NonNullable<CookbookOverviewOverride['changed']> = {};\n for (const [id, patch] of Object.entries(value.changed)) {\n if (!WIDGET_ID.test(id)) {\n warning(\n diagnostics,\n 'malformed_overview_change',\n `${at}.changed.${id}`,\n `${at}.changed.${id} was dropped because its widget id is invalid`,\n );\n continue;\n }\n const normalized = normalizeOverviewPatch(\n patch,\n `${at}.changed.${id}`,\n diagnostics,\n );\n if (normalized) changed[id] = normalized;\n }\n if (Object.keys(changed).length) override.changed = changed;\n else delete override.changed;\n }\n }\n return override;\n}\n\n/**\n * Structural validation of an unknown value (parsed JSON). Returns the\n * normalised document (legacy `$schema` rewritten, `features` defaulted,\n * recipes sorted and unique) or every problem found, each a readable line\n * naming the path. Unknown top-level keys are kept (additive fields in v1).\n *\n * Deep checks use optional host-supplied catalogs. A structurally valid\n * cookbook remains portable when those catalogs are absent. Invalid overview\n * entries are dropped and reported, following the v1 overview contract.\n */\nexport function validateCookbook(\n input: unknown,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n const diagnostics: CookbookDiagnostic[] = [];\n if (!isObject(input)) {\n error(diagnostics, 'invalid_type', '', 'expected a JSON object');\n return { ok: false, errors: ['expected a JSON object'], diagnostics };\n }\n\n const { $schema, version } = input;\n if (\n $schema !== undefined &&\n $schema !== COOKBOOK_SCHEMA_URL &&\n $schema !== COOKBOOK_LEGACY_SCHEMA_URL\n ) {\n error(\n diagnostics,\n 'invalid_schema',\n '$schema',\n `$schema must be ${COOKBOOK_SCHEMA_URL}`,\n );\n }\n if (version === undefined) {\n error(diagnostics, 'missing_field', 'version', 'version is required');\n } else if (typeof version !== 'number' || !Number.isInteger(version)) {\n error(\n diagnostics,\n 'invalid_type',\n 'version',\n 'version must be a whole number',\n );\n } else if (version > COOKBOOK_VERSION) {\n error(\n diagnostics,\n 'unsupported_version',\n 'version',\n `version ${version} is newer than this reader understands (${COOKBOOK_VERSION})`,\n );\n } else if (version !== COOKBOOK_VERSION) {\n error(\n diagnostics,\n 'unsupported_version',\n 'version',\n `version ${version} is not supported`,\n );\n }\n if ($schema === COOKBOOK_LEGACY_SCHEMA_URL) {\n warning(\n diagnostics,\n 'migrated_schema',\n '$schema',\n '$schema uses the legacy blueprint URL; migrated',\n );\n }\n\n for (const key of ['name', 'description'] as const) {\n if (input[key] !== undefined && typeof input[key] !== 'string') {\n error(diagnostics, 'invalid_type', key, `${key} must be a string`);\n }\n }\n\n let recipes: string[] = [];\n if (!isStrings(input.recipes)) {\n error(\n diagnostics,\n 'invalid_type',\n 'recipes',\n 'recipes must be a list of recipe ids',\n );\n } else {\n recipes = [...new Set(input.recipes)].sort();\n if (options.recipes) {\n const known = new Set(options.recipes);\n for (const id of recipes) {\n if (!known.has(id))\n error(\n diagnostics,\n 'unknown_recipe',\n `recipes[${input.recipes.indexOf(id)}]`,\n `recipes: unknown recipe \"${id}\"`,\n );\n }\n }\n }\n\n let features: string[] = [];\n if (input.features !== undefined) {\n if (!isStrings(input.features)) {\n error(\n diagnostics,\n 'invalid_type',\n 'features',\n 'features must be a list of model names',\n );\n } else {\n features = [...new Set(input.features)].sort();\n if (features.length !== input.features.length) {\n warning(\n diagnostics,\n 'duplicate_value',\n 'features',\n 'features listed a model more than once; deduplicated',\n );\n }\n }\n }\n\n const policies: CookbookPolicyRow[] = [];\n if (!Array.isArray(input.policies)) {\n error(diagnostics, 'invalid_type', 'policies', 'policies must be a list');\n } else {\n for (const [i, row] of input.policies.entries()) {\n checkPolicy(row, `policies[${i}]`, diagnostics);\n policies.push(row as CookbookPolicyRow);\n }\n }\n\n if (input.exposure !== undefined) {\n const ok = isStringRecord(\n input.exposure,\n (s) => isStrings(s) && s.every((x) => SURFACES.has(x)),\n );\n if (!ok)\n error(\n diagnostics,\n 'invalid_value',\n 'exposure',\n 'exposure must map model names to lists of api, mcp or cli',\n );\n }\n if (input.layout !== undefined) checkLayout(input.layout, diagnostics);\n if (input.theme !== undefined) checkTheme(input.theme, diagnostics);\n\n if (isObject(input.theme) && typeof input.theme.preset === 'string') {\n const presets = options.themePresets\n ? new Set(options.themePresets)\n : undefined;\n if (presets && !presets.has(input.theme.preset)) {\n error(\n diagnostics,\n 'unknown_theme_preset',\n 'theme.preset',\n `Unknown theme preset \"${input.theme.preset}\".`,\n );\n }\n }\n\n if (isObject(input.layout) && input.layout.version === 1 && options.layout) {\n const sections = options.layout.sections\n ? new Set(options.layout.sections)\n : undefined;\n const items = options.layout.items\n ? new Set(options.layout.items)\n : undefined;\n const placementItems = options.layout.placementItems\n ? new Set(options.layout.placementItems)\n : undefined;\n if (sections && Array.isArray(input.layout.customSections)) {\n for (const section of input.layout.customSections) {\n if (isObject(section) && typeof section.id === 'string') {\n sections.add(section.id);\n }\n }\n }\n const known = (id: string, kind: 'section' | 'item' | 'either') =>\n kind === 'section'\n ? sections?.has(id) !== false\n : kind === 'item'\n ? items?.has(id) !== false\n : sections?.has(id) === true ||\n items?.has(id) === true ||\n (!sections && !items);\n const check = (\n id: unknown,\n path: string,\n kind: 'section' | 'item' | 'either',\n ) => {\n if (typeof id === 'string' && !known(id, kind)) {\n error(\n diagnostics,\n 'unknown_layout_id',\n path,\n `Unknown layout ${kind === 'either' ? 'section or item' : kind} \"${id}\".`,\n );\n }\n };\n if (isStrings(input.layout.sectionOrder)) {\n input.layout.sectionOrder.forEach((id, index) => {\n check(id, `layout.sectionOrder[${index}]`, 'section');\n });\n }\n if (isStrings(input.layout.hidden)) {\n input.layout.hidden.forEach((id, index) => {\n check(id, `layout.hidden[${index}]`, 'either');\n });\n }\n if (isObject(input.layout.itemOrder)) {\n for (const [section, order] of Object.entries(input.layout.itemOrder)) {\n check(section, `layout.itemOrder.${section}`, 'section');\n if (isStrings(order)) {\n order.forEach((id, index) => {\n check(id, `layout.itemOrder.${section}[${index}]`, 'item');\n });\n }\n }\n }\n if (isObject(input.layout.moved)) {\n for (const [id, section] of Object.entries(input.layout.moved)) {\n check(id, `layout.moved.${id}`, 'item');\n check(section, `layout.moved.${id}`, 'section');\n }\n }\n for (const key of ['sections', 'items'] as const) {\n if (!isObject(input.layout[key])) continue;\n for (const id of Object.keys(input.layout[key])) {\n check(\n id,\n `layout.${key}.${id}`,\n key === 'sections' ? 'section' : 'item',\n );\n }\n }\n if (isObject(input.layout.placements)) {\n for (const id of Object.keys(input.layout.placements)) {\n if (placementItems && !placementItems.has(id)) {\n error(\n diagnostics,\n 'unknown_layout_id',\n `layout.placements.${id}`,\n `Unknown layout placement item \"${id}\".`,\n );\n }\n }\n }\n }\n\n const overviews: Record<string, CookbookOverviewOverride> = {};\n if (input.overviews !== undefined) {\n if (\n !isObject(input.overviews) ||\n Object.hasOwn(input.overviews, '__proto__')\n ) {\n error(\n diagnostics,\n 'invalid_type',\n 'overviews',\n 'overviews must map overview ids to overrides',\n );\n } else {\n const overviewIds = options.overviewIds\n ? new Set(options.overviewIds)\n : undefined;\n for (const [id, value] of Object.entries(input.overviews)) {\n const at = `overviews.${id}`;\n if (!OVERVIEW_ID.test(id)) {\n warning(\n diagnostics,\n 'invalid_overview_id',\n at,\n `${at} was dropped because its overview id is invalid`,\n );\n continue;\n }\n if (overviewIds && !overviewIds.has(id)) {\n warning(\n diagnostics,\n 'unknown_overview',\n at,\n `Unknown overview \"${id}\" was dropped.`,\n );\n continue;\n }\n const override = normalizeOverview(value, at, diagnostics);\n if (!override) continue;\n if (!options.validateOverview) {\n overviews[id] = override;\n continue;\n }\n let checked: CookbookOverviewValidation;\n try {\n checked = options.validateOverview(id, override);\n } catch {\n warning(\n diagnostics,\n 'overview_validation_failed',\n at,\n `Overview \"${id}\" was dropped because its host validator failed.`,\n );\n continue;\n }\n for (const issue of checked.diagnostics ?? []) {\n warning(diagnostics, issue.code, issue.path || at, issue.message);\n }\n if (!checked.ok && checked.diagnostics.length === 0) {\n warning(\n diagnostics,\n 'overview_rejected',\n at,\n `Overview \"${id}\" was rejected by its host validator and dropped.`,\n );\n }\n if (checked.ok && checked.override) overviews[id] = checked.override;\n }\n }\n }\n\n const errors = diagnostics\n .filter((diagnostic) => diagnostic.severity === 'error')\n .map((diagnostic) => diagnostic.message);\n if (errors.length) return { ok: false, errors, diagnostics };\n\n const cookbook = {\n ...input,\n $schema: COOKBOOK_SCHEMA_URL,\n version: COOKBOOK_VERSION,\n recipes,\n features,\n policies,\n } as unknown as Cookbook;\n if (input.exposure !== undefined) {\n cookbook.exposure = input.exposure as Record<\n string,\n CookbookExposureSurface[]\n >;\n }\n if (input.layout !== undefined)\n cookbook.layout = input.layout as CookbookLayout;\n if (input.theme !== undefined) cookbook.theme = input.theme as CookbookTheme;\n delete cookbook.overviews;\n if (Object.keys(overviews).length) cookbook.overviews = overviews;\n const warnings = diagnostics\n .filter((diagnostic) => diagnostic.severity === 'warning')\n .map((diagnostic) => diagnostic.message);\n return { ok: true, cookbook, warnings, diagnostics };\n}\n\n/** Parse JSON text, then {@link validateCookbook} it. */\nexport function parseCookbookText(\n text: string,\n options: CookbookValidateOptions = {},\n): CookbookValidation {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch {\n return {\n ok: false,\n errors: ['not valid JSON'],\n diagnostics: [\n {\n severity: 'error',\n code: 'invalid_json',\n path: '',\n message: 'not valid JSON',\n },\n ],\n };\n }\n return validateCookbook(value, options);\n}\n"],"mappings":"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"}
@@ -295,7 +295,11 @@ export declare class DispatchBus {
295
295
  * tenant or widen visibility to all tenants. Scoping rules:
296
296
  *
297
297
  * - tenancy off → no tenant filter (pre-tenancy behavior).
298
- * - active tenant T → that tenant's dispatches plus global (NULL) dispatches.
298
+ * - active tenant T → that tenant's dispatches only. Global (NULL-tenant)
299
+ * dispatches are system-only on reads (#3301): a global row's payload (for
300
+ * example a delivery emitted outside tenant context) may carry another
301
+ * tenant's recipients/subjects. Processing (`process`) still lets a tenant
302
+ * worker claim global work.
299
303
  * - tenancy on but no active tenant → global (NULL) dispatches only
300
304
  * (fail-closed; never all tenants).
301
305
  *
@@ -307,9 +311,10 @@ export declare class DispatchBus {
307
311
  * Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).
308
312
  *
309
313
  * Applies the same server-derived tenant predicate as {@link list}: a
310
- * subscriber in tenant A cannot fetch tenant B's dispatch by id, and when
311
- * tenancy is on with no active tenant only global (NULL) dispatches are
312
- * returned. Returns `null` when the dispatch exists but is out of scope.
314
+ * subscriber in tenant A cannot fetch tenant B's dispatch, or a global
315
+ * (NULL-tenant) dispatch, by id (#3301), and when tenancy is on with no
316
+ * active tenant only global (NULL) dispatches are returned. Returns `null`
317
+ * when the dispatch exists but is out of scope.
313
318
  */
314
319
  get(id: string): Promise<Dispatch | null>;
315
320
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/dispatch/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAiB5D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EAEnB,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAapB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,WAAW,CAAkB;IAErC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;IAE5D,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEhD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B;;OAEG;gBACS,EAAE,EAAE,iBAAiB;IAIjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkFjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC;IA0EpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAenD;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;IAcvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCjE;;;;;OAKG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC;IAyIlB;;;;;;;OAOG;IACG,KAAK,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBhE;;;;;OAKG;IACG,OAAO,CACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAYjC;;;;;;;;;;;;;;OAcG;IACG,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAUlE;;;;;;;OAOG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAS/C;;OAEG;IACG,iBAAiB,CACrB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAkBlC;;OAEG;IACH,OAAO,CAAC,cAAc;CAqCvB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CAwCtB"}
1
+ {"version":3,"file":"bus.d.ts","sourceRoot":"","sources":["../../src/dispatch/bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAiB5D,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kCAAkC,CAAC;AAKxE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,mBAAmB,EAEnB,sBAAsB,EACtB,oBAAoB,EACpB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AAapB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,QAAQ,CAA+C;IAC/D,OAAO,CAAC,WAAW,CAAkB;IAErC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAiB;IAE5D,wEAAwE;IACxE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAO;IAEhD;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,MAAM,CAAC,cAAc;IAa7B;;OAEG;gBACS,EAAE,EAAE,iBAAiB;IAIjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAkFjC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAoDG;IACG,IAAI,CACR,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,QAAQ,CAAC;IA0EpB;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACH,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI;IAenD;;;;;;OAMG;IACH,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;IAcvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAsCG;IACG,SAAS,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmCjE;;;;;OAKG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACG,OAAO,CACX,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC;IAyIlB;;;;;;;OAOG;IACG,KAAK,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBhE;;;;;OAKG;IACG,OAAO,CACX,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC;IAYjC;;;;;;;;;;;;;;;;;;OAkBG;IACG,IAAI,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAgBlE;;;;;;;;OAQG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAU/C;;OAEG;IACG,iBAAiB,CACrB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAkBlC;;OAEG;IACH,OAAO,CAAC,cAAc;CAqCvB;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CAwCtB"}
@@ -455,7 +455,11 @@ var DispatchBus = class DispatchBus {
455
455
  * tenant or widen visibility to all tenants. Scoping rules:
456
456
  *
457
457
  * - tenancy off → no tenant filter (pre-tenancy behavior).
458
- * - active tenant T → that tenant's dispatches plus global (NULL) dispatches.
458
+ * - active tenant T → that tenant's dispatches only. Global (NULL-tenant)
459
+ * dispatches are system-only on reads (#3301): a global row's payload (for
460
+ * example a delivery emitted outside tenant context) may carry another
461
+ * tenant's recipients/subjects. Processing (`process`) still lets a tenant
462
+ * worker claim global work.
459
463
  * - tenancy on but no active tenant → global (NULL) dispatches only
460
464
  * (fail-closed; never all tenants).
461
465
  *
@@ -468,19 +472,20 @@ var DispatchBus = class DispatchBus {
468
472
  return DispatchCollection.list(this.db, {
469
473
  ...safeOptions,
470
474
  tenantScope: resolveDispatchTenantScope()
471
- });
475
+ }, false);
472
476
  }
473
477
  /**
474
478
  * Gets a dispatch by ID, enforcing the active tenant scope (S5 #1398).
475
479
  *
476
480
  * Applies the same server-derived tenant predicate as {@link list}: a
477
- * subscriber in tenant A cannot fetch tenant B's dispatch by id, and when
478
- * tenancy is on with no active tenant only global (NULL) dispatches are
479
- * returned. Returns `null` when the dispatch exists but is out of scope.
481
+ * subscriber in tenant A cannot fetch tenant B's dispatch, or a global
482
+ * (NULL-tenant) dispatch, by id (#3301), and when tenancy is on with no
483
+ * active tenant only global (NULL) dispatches are returned. Returns `null`
484
+ * when the dispatch exists but is out of scope.
480
485
  */
481
486
  async get(id) {
482
487
  await this.initialize();
483
- return DispatchCollection.getScoped(this.db, id, resolveDispatchTenantScope());
488
+ return DispatchCollection.getScoped(this.db, id, resolveDispatchTenantScope(), false);
484
489
  }
485
490
  /**
486
491
  * List all subscriptions