@happyvertical/smrt-scanner 0.55.6 → 0.55.8

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.
@@ -78,7 +78,7 @@ var DATA_SURFACE_KINDS = /* @__PURE__ */ new Set([
78
78
  ]);
79
79
  var PLAYBOOK_PLANES = /* @__PURE__ */ new Set(["browser", "server"]);
80
80
  var FAILURE_POLICIES = /* @__PURE__ */ new Set(["abort", "continue"]);
81
- var QUALIFIED_MODEL_PATTERN = /^\S+:\S+$/;
81
+ var QUALIFIED_MODEL_PATTERN$1 = /^\S+:\S+$/;
82
82
  function intentToolName(id) {
83
83
  return id.replace(/[.-]/g, "_");
84
84
  }
@@ -424,7 +424,7 @@ function playbookDeclarationProblem(declaration, key, steps) {
424
424
  if (enabled !== void 0 && typeof enabled !== "boolean") return `playbook '${key}' declares a non-boolean \`enabled\`, which \`definePlaybook\` rejects rather than coercing \u2014 a truthy '"false"' would otherwise read as enabled.`;
425
425
  for (const step of declaration.steps ?? []) {
426
426
  const record = step;
427
- if (record.kind === "operation" && !QUALIFIED_MODEL_PATTERN.test(String(record.model))) return `playbook '${key}' has a step whose model '${String(record.model)}' is not a qualified pair such as '@happyvertical/smrt-commerce:Order'.`;
427
+ if (record.kind === "operation" && !QUALIFIED_MODEL_PATTERN$1.test(String(record.model))) return `playbook '${key}' has a step whose model '${String(record.model)}' is not a qualified pair such as '@happyvertical/smrt-commerce:Order'.`;
428
428
  }
429
429
  }
430
430
  function intentIdentityProblem(id) {
@@ -1157,8 +1157,314 @@ var InheritanceResolver = class {
1157
1157
  }
1158
1158
  };
1159
1159
  //#endregion
1160
+ //#region src/recipe-widgets.ts
1161
+ var RECIPE_WIDGET_OPTION_TYPES = [
1162
+ "text",
1163
+ "markdown",
1164
+ "identifier",
1165
+ "model",
1166
+ "integer",
1167
+ "number",
1168
+ "boolean",
1169
+ "enum"
1170
+ ];
1171
+ var RECIPE_WIDGET_KEYS = [
1172
+ "kind",
1173
+ "type",
1174
+ "export",
1175
+ "label",
1176
+ "description",
1177
+ "icon",
1178
+ "version",
1179
+ "migrate",
1180
+ "options",
1181
+ "data",
1182
+ "allowedIn",
1183
+ "defaultSpan",
1184
+ "minSpan",
1185
+ "maxSpan"
1186
+ ];
1187
+ var WIDGET_TYPE_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
1188
+ var OVERVIEW_PAGE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,95}$/;
1189
+ var OPTION_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9]{0,31}$/;
1190
+ var IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/;
1191
+ var MODEL_PATTERN = /^(?:(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*:)?[A-Za-z][A-Za-z0-9_]{0,63}$/;
1192
+ var QUALIFIED_MODEL_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*:[A-Za-z][A-Za-z0-9_]{0,63}$/;
1193
+ var ICON_PATTERN$1 = /^[A-Za-z][A-Za-z0-9_-]*$/;
1194
+ var MARKDOWN_MAX = 1e4;
1195
+ var OPTION_KEYS = /* @__PURE__ */ new Set([
1196
+ "key",
1197
+ "type",
1198
+ "label",
1199
+ "help",
1200
+ "required",
1201
+ "default",
1202
+ "min",
1203
+ "max",
1204
+ "maxLength",
1205
+ "choices"
1206
+ ]);
1207
+ function isRecord(value) {
1208
+ return !!value && typeof value === "object" && !Array.isArray(value);
1209
+ }
1210
+ function nonEmpty(value) {
1211
+ return typeof value === "string" && value.trim() !== "";
1212
+ }
1213
+ function readOption(raw, at, fail) {
1214
+ if (!isRecord(raw)) {
1215
+ fail(`${at} must be an object literal`);
1216
+ return;
1217
+ }
1218
+ let ok = true;
1219
+ const bad = (message) => {
1220
+ fail(`${at}${message}`);
1221
+ ok = false;
1222
+ };
1223
+ for (const key2 of Object.keys(raw)) if (!OPTION_KEYS.has(key2)) bad(` does not accept \`${key2}\``);
1224
+ const { key, type } = raw;
1225
+ if (typeof key !== "string" || !OPTION_KEY_PATTERN.test(key)) bad(".key must be a camelCase name of at most 32 letters and digits");
1226
+ if (typeof type !== "string" || !RECIPE_WIDGET_OPTION_TYPES.includes(type)) {
1227
+ bad(`.type must be one of ${RECIPE_WIDGET_OPTION_TYPES.join(", ")} (got \`${String(type)}\`)`);
1228
+ return;
1229
+ }
1230
+ if (!nonEmpty(raw.label)) bad(".label must be a non-empty string");
1231
+ if (raw.help !== void 0 && !nonEmpty(raw.help)) bad(".help must be a non-empty string");
1232
+ if (raw.required !== void 0 && typeof raw.required !== "boolean") bad(".required must be a boolean");
1233
+ const numeric = type === "integer" || type === "number";
1234
+ for (const bound of ["min", "max"]) {
1235
+ const value2 = raw[bound];
1236
+ if (value2 === void 0) continue;
1237
+ if (!numeric) bad(`.${bound} applies only to integer and number options`);
1238
+ else if (typeof value2 !== "number" || !Number.isFinite(value2)) bad(`.${bound} must be a finite number`);
1239
+ }
1240
+ if (typeof raw.min === "number" && typeof raw.max === "number" && raw.min > raw.max) bad(".min cannot exceed .max");
1241
+ const textual = type === "text" || type === "markdown";
1242
+ if (raw.maxLength !== void 0) {
1243
+ if (!textual) bad(".maxLength applies only to text and markdown options");
1244
+ else if (typeof raw.maxLength !== "number" || !Number.isSafeInteger(raw.maxLength) || raw.maxLength < 1) bad(".maxLength must be a positive integer");
1245
+ }
1246
+ let choiceValues;
1247
+ if (type === "enum") {
1248
+ const choices = raw.choices;
1249
+ if (!Array.isArray(choices) || choices.length === 0) bad(".choices must be a non-empty list of { value, label }");
1250
+ else {
1251
+ choiceValues = [];
1252
+ choices.forEach((choice, index) => {
1253
+ const where = `.choices[${index}]`;
1254
+ if (!isRecord(choice) || Object.keys(choice).some((k) => k !== "value" && k !== "label")) bad(`${where} must be { value, label } and nothing else`);
1255
+ else if (!nonEmpty(choice.value) || !nonEmpty(choice.label)) bad(`${where} needs a non-empty value and label`);
1256
+ else if (choiceValues?.includes(choice.value)) bad(`${where} repeats the value \`${choice.value}\``);
1257
+ else choiceValues?.push(choice.value);
1258
+ });
1259
+ }
1260
+ } else if (raw.choices !== void 0) bad(".choices applies only to enum options");
1261
+ const value = raw.default;
1262
+ if (value !== void 0) {
1263
+ const mismatch = (what) => bad(`.default must be ${what}`);
1264
+ switch (type) {
1265
+ case "text":
1266
+ case "markdown": {
1267
+ const max = typeof raw.maxLength === "number" ? raw.maxLength : type === "markdown" ? MARKDOWN_MAX : 200;
1268
+ if (typeof value !== "string") mismatch("a string");
1269
+ else if (value.length > max) mismatch(`at most ${max} characters`);
1270
+ break;
1271
+ }
1272
+ case "identifier":
1273
+ if (typeof value !== "string" || !IDENTIFIER_PATTERN.test(value)) mismatch("an identifier (letters, digits, `_`, `.`, `-`)");
1274
+ break;
1275
+ case "model":
1276
+ if (typeof value !== "string" || value.length > 160 || !MODEL_PATTERN.test(value)) mismatch("a model name (`Class`, `pkg:Class` or `@scope/pkg:Class`)");
1277
+ break;
1278
+ case "enum":
1279
+ if (typeof value !== "string" || choiceValues && !choiceValues.includes(value)) mismatch("one of the choices");
1280
+ break;
1281
+ case "boolean":
1282
+ if (typeof value !== "boolean") mismatch("a boolean");
1283
+ break;
1284
+ default: if (typeof value !== "number" || !Number.isFinite(value)) mismatch("a finite number");
1285
+ else if (type === "integer" && !Number.isSafeInteger(value)) mismatch("a safe integer");
1286
+ else if (typeof raw.min === "number" && value < raw.min || typeof raw.max === "number" && value > raw.max) mismatch("within min and max");
1287
+ }
1288
+ }
1289
+ if (!ok) return void 0;
1290
+ const out = {};
1291
+ for (const name of [
1292
+ "key",
1293
+ "type",
1294
+ "label",
1295
+ "help",
1296
+ "required",
1297
+ "default",
1298
+ "min",
1299
+ "max",
1300
+ "maxLength",
1301
+ "choices"
1302
+ ]) if (raw[name] !== void 0) out[name] = raw[name];
1303
+ if (type === "enum") out.choices = raw.choices.map((choice) => ({
1304
+ value: choice.value,
1305
+ label: choice.label
1306
+ }));
1307
+ return out;
1308
+ }
1309
+ function readOptions(raw, at, fail) {
1310
+ if (!Array.isArray(raw) || raw.length === 0) {
1311
+ fail(`${at}.options must be a non-empty list of option objects`);
1312
+ return;
1313
+ }
1314
+ const out = [];
1315
+ const keys = /* @__PURE__ */ new Set();
1316
+ let ok = true;
1317
+ raw.forEach((entry, index) => {
1318
+ const option = readOption(entry, `${at}.options[${index}]`, fail);
1319
+ if (!option) {
1320
+ ok = false;
1321
+ return;
1322
+ }
1323
+ if (keys.has(option.key)) {
1324
+ fail(`${at}.options[${index}].key \`${option.key}\` is already declared`);
1325
+ ok = false;
1326
+ return;
1327
+ }
1328
+ keys.add(option.key);
1329
+ out.push(option);
1330
+ });
1331
+ return ok ? out : void 0;
1332
+ }
1333
+ function readData(raw, at, fail, readRef) {
1334
+ if (!isRecord(raw)) {
1335
+ fail(`${at}.data must be an object literal`);
1336
+ return;
1337
+ }
1338
+ let ok = true;
1339
+ for (const key of Object.keys(raw)) if (key !== "load" && key !== "models") {
1340
+ fail(`${at}.data does not accept \`${key}\``);
1341
+ ok = false;
1342
+ }
1343
+ const out = {};
1344
+ if (raw.load !== void 0) {
1345
+ const ref = readRef(raw.load, `${at}.data`, "load");
1346
+ if (ref === void 0) ok = false;
1347
+ else out.load = ref;
1348
+ }
1349
+ if (raw.models !== void 0) {
1350
+ const models = raw.models;
1351
+ if (!Array.isArray(models) || models.length === 0) {
1352
+ fail(`${at}.data.models must be a non-empty list of qualified models`);
1353
+ ok = false;
1354
+ } else {
1355
+ const seen = /* @__PURE__ */ new Set();
1356
+ for (const [index, model] of models.entries()) if (typeof model !== "string" || model.length > 160 || !QUALIFIED_MODEL_PATTERN.test(model)) {
1357
+ fail(`${at}.data.models[${index}] must be a qualified class name like \`@scope/pkg:Class\``);
1358
+ ok = false;
1359
+ } else if (seen.has(model)) {
1360
+ fail(`${at}.data.models[${index}] repeats \`${model}\``);
1361
+ ok = false;
1362
+ } else seen.add(model);
1363
+ out.models = [...seen];
1364
+ }
1365
+ }
1366
+ if (ok && !out.load && !out.models) {
1367
+ fail(`${at}.data needs \`load\` or \`models\``);
1368
+ ok = false;
1369
+ }
1370
+ return ok ? out : void 0;
1371
+ }
1372
+ function readWidgetSurface(entry, at, fail, readRef) {
1373
+ let ok = true;
1374
+ const bad = (message) => {
1375
+ fail(`${at}${message}`);
1376
+ ok = false;
1377
+ };
1378
+ const out = {};
1379
+ const { type } = entry;
1380
+ if (typeof type !== "string" || !WIDGET_TYPE_PATTERN.test(type)) bad(".type must be a lowercase kebab id of at most 32 characters");
1381
+ else out.type = type;
1382
+ if (entry.description !== void 0) if (nonEmpty(entry.description)) out.description = entry.description;
1383
+ else bad(".description must be a non-empty string");
1384
+ if (entry.icon !== void 0) if (typeof entry.icon === "string" && ICON_PATTERN$1.test(entry.icon)) out.icon = entry.icon;
1385
+ else bad(".icon must be an icon name");
1386
+ let version = 1;
1387
+ if (entry.version !== void 0) if (typeof entry.version === "number" && Number.isSafeInteger(entry.version) && entry.version >= 1) {
1388
+ version = entry.version;
1389
+ out.version = entry.version;
1390
+ } else bad(".version must be a positive integer");
1391
+ if (entry.migrate !== void 0) {
1392
+ const ref = readRef(entry.migrate, at, "migrate");
1393
+ if (ref === void 0) ok = false;
1394
+ else if (version < 2) bad(".migrate needs a `version` above 1");
1395
+ else out.migrate = ref;
1396
+ }
1397
+ if (entry.options !== void 0) {
1398
+ const options = readOptions(entry.options, at, fail);
1399
+ if (options) out.options = options;
1400
+ else ok = false;
1401
+ }
1402
+ if (entry.data !== void 0) {
1403
+ const data = readData(entry.data, at, fail, readRef);
1404
+ if (data) out.data = data;
1405
+ else ok = false;
1406
+ }
1407
+ if (entry.allowedIn !== void 0) {
1408
+ const placements = entry.allowedIn;
1409
+ if (!Array.isArray(placements) || placements.length === 0) bad(".allowedIn must be a non-empty list of overview ids");
1410
+ else {
1411
+ const seen = /* @__PURE__ */ new Set();
1412
+ placements.forEach((id, index) => {
1413
+ if (typeof id !== "string" || !OVERVIEW_PAGE_ID_PATTERN.test(id)) bad(`.allowedIn[${index}] must be an overview id like \`events.home\``);
1414
+ else if (seen.has(id)) bad(`.allowedIn[${index}] repeats \`${id}\``);
1415
+ else seen.add(id);
1416
+ });
1417
+ out.allowedIn = [...seen];
1418
+ }
1419
+ }
1420
+ const spans = {
1421
+ minSpan: 1,
1422
+ defaultSpan: 1,
1423
+ maxSpan: 4
1424
+ };
1425
+ for (const name of [
1426
+ "defaultSpan",
1427
+ "minSpan",
1428
+ "maxSpan"
1429
+ ]) {
1430
+ const value = entry[name];
1431
+ if (value === void 0) continue;
1432
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 4) bad(`.${name} must be an integer from 1 to 4`);
1433
+ else {
1434
+ out[name] = value;
1435
+ spans[name] = value;
1436
+ }
1437
+ }
1438
+ if (ok && (spans.minSpan > spans.maxSpan || entry.defaultSpan !== void 0 && (spans.defaultSpan < spans.minSpan || spans.defaultSpan > spans.maxSpan))) bad(".minSpan <= .defaultSpan <= .maxSpan must hold");
1439
+ return ok ? out : void 0;
1440
+ }
1441
+ //#endregion
1160
1442
  //#region src/recipes.ts
1161
1443
  var RECIPE_BASE = "SmrtRecipe";
1444
+ var RECIPE_SHELL_SLOTS = [
1445
+ "header.start",
1446
+ "header.center",
1447
+ "header.end",
1448
+ "footer.start",
1449
+ "footer.center",
1450
+ "footer.end",
1451
+ "leftSidebar.header",
1452
+ "leftSidebar.footer",
1453
+ "rightSidebar.header",
1454
+ "rightSidebar.footer"
1455
+ ];
1456
+ var RECIPE_SURFACE_KINDS = [
1457
+ "shell-widget",
1458
+ "route",
1459
+ "settings-panel",
1460
+ "playground",
1461
+ "widget"
1462
+ ];
1463
+ var RECIPE_RUNTIMES = [
1464
+ "browser",
1465
+ "server",
1466
+ "both"
1467
+ ];
1162
1468
  var CORE_SPECIFIER = "@happyvertical/smrt-core";
1163
1469
  function isCoreSpecifier(source) {
1164
1470
  return source === CORE_SPECIFIER || source.startsWith(`${CORE_SPECIFIER}/`);
@@ -1171,8 +1477,28 @@ var FIELD_OPTION_KEYS = /* @__PURE__ */ new Set([
1171
1477
  "help",
1172
1478
  "order",
1173
1479
  "visibility",
1174
- "locked"
1480
+ "locked",
1481
+ "required"
1482
+ ]);
1483
+ var NAV_EXTRA_KEYS = [
1484
+ "icon",
1485
+ "description",
1486
+ "key",
1487
+ "noun"
1488
+ ];
1489
+ var GROUP_KEYS = /* @__PURE__ */ new Set([
1490
+ "id",
1491
+ "label",
1492
+ "summary"
1493
+ ]);
1494
+ var SECTION_KEYS = /* @__PURE__ */ new Set([
1495
+ "id",
1496
+ "label",
1497
+ "icon",
1498
+ "description"
1175
1499
  ]);
1500
+ var SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
1501
+ var ICON_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
1176
1502
  var VISIBILITIES = /* @__PURE__ */ new Set([
1177
1503
  "basic",
1178
1504
  "advanced",
@@ -1413,8 +1739,15 @@ function extractRecipes(input) {
1413
1739
  models: [],
1414
1740
  nav: [],
1415
1741
  requires: [],
1742
+ requiresAny: null,
1743
+ group: null,
1744
+ section: null,
1416
1745
  options: [],
1417
- help: null
1746
+ help: null,
1747
+ surfaces: null,
1748
+ providers: null,
1749
+ runtime: null,
1750
+ demoSeed: null
1418
1751
  };
1419
1752
  const seen = /* @__PURE__ */ new Set();
1420
1753
  for (const member of node.body.body) {
@@ -1447,16 +1780,36 @@ function extractRecipes(input) {
1447
1780
  const fields = objectEntries(entry, "static nav entries");
1448
1781
  const label = fields.find((field) => field.key === "label");
1449
1782
  const model = fields.find((field) => field.key === "model");
1450
- const extra = fields.find((field) => field.key !== "label" && field.key !== "model");
1451
- if (extra) throw new RecipeReadError(`static nav entries accept only \`label\` and \`model\`, not \`${extra.key}\``, extra.node);
1783
+ const extra = fields.find((field) => field.key !== "label" && field.key !== "model" && field.key !== "filter" && !NAV_EXTRA_KEYS.includes(field.key));
1784
+ if (extra) throw new RecipeReadError(`static nav entries accept only \`label\`, \`model\`, \`icon\`, \`description\`, \`key\`, \`noun\` and \`filter\`, not \`${extra.key}\``, extra.node);
1785
+ const extras = {};
1786
+ for (const name2 of NAV_EXTRA_KEYS) {
1787
+ const found = fields.find((field) => field.key === name2);
1788
+ if (found) extras[name2] = stringValue(found.value, `static nav ${name2}`);
1789
+ }
1790
+ const filter = fields.find((field) => field.key === "filter");
1452
1791
  if (!label || !model) throw new RecipeReadError("static nav entries need both `label` and `model`", entry);
1453
1792
  return {
1454
1793
  label: stringValue(label.value, "static nav label"),
1455
1794
  model: modelRef(model.value, imports, declaredHere, "static nav model", input.sourceText),
1456
- line: lineOf(input.sourceText, entry)
1795
+ line: lineOf(input.sourceText, entry),
1796
+ ...extras,
1797
+ ...filter ? { filter: literalValue(filter.value, "static nav filter") } : {}
1457
1798
  };
1458
1799
  });
1459
1800
  break;
1801
+ case "requiresAny":
1802
+ case "group":
1803
+ case "section":
1804
+ case "surfaces":
1805
+ case "providers":
1806
+ case "runtime":
1807
+ case "demoSeed":
1808
+ recipe[name] = {
1809
+ value: literalValue(value, `static ${name}`),
1810
+ line: lineOf(input.sourceText, member)
1811
+ };
1812
+ break;
1460
1813
  case "options":
1461
1814
  recipe.options = objectEntries(value, "static options").map((entry) => ({
1462
1815
  key: entry.key,
@@ -1616,10 +1969,10 @@ function readModelOptions(raw, model, classes, where, bad) {
1616
1969
  const entry = {};
1617
1970
  for (const [hint, hintValue] of Object.entries(hints)) {
1618
1971
  const at = `${where}.fields.${field}.${hint}`;
1619
- if (!FIELD_OPTION_KEYS.has(hint)) fail(`${at} is not a field option; use default, label, help, order, visibility, or locked`);
1972
+ if (!FIELD_OPTION_KEYS.has(hint)) fail(`${at} is not a field option; use default, label, help, order, visibility, locked, or required`);
1620
1973
  else if ((hint === "label" || hint === "help") && typeof hintValue !== "string") fail(`${at} must be a string`);
1621
1974
  else if (hint === "order" && (typeof hintValue !== "number" || !Number.isFinite(hintValue))) fail(`${at} must be a finite number`);
1622
- else if (hint === "locked" && typeof hintValue !== "boolean") fail(`${at} must be a boolean`);
1975
+ else if ((hint === "locked" || hint === "required") && typeof hintValue !== "boolean") fail(`${at} must be a boolean`);
1623
1976
  else if (hint === "visibility" && !(typeof hintValue === "string" && VISIBILITIES.has(hintValue))) fail(`${at} must be one of basic, advanced, hidden`);
1624
1977
  else entry[hint] = hintValue;
1625
1978
  }
@@ -1639,6 +1992,298 @@ function readModelOptions(raw, model, classes, where, bad) {
1639
1992
  } else fail(`${where}.${key} is not a model option; use \`fields\` or \`exposure\``);
1640
1993
  return ok ? out : void 0;
1641
1994
  }
1995
+ function readNavFilter(value, fail) {
1996
+ if (value === void 0) return void 0;
1997
+ if (!isPlainObject(value)) {
1998
+ fail("filter must be an object `{ field, value }`");
1999
+ return;
2000
+ }
2001
+ const extra = Object.keys(value).find((k) => k !== "field" && k !== "value");
2002
+ if (extra) {
2003
+ fail(`filter accepts only \`field\` and \`value\`, not \`${extra}\``);
2004
+ return;
2005
+ }
2006
+ if (typeof value.field !== "string" || typeof value.value !== "string") {
2007
+ fail("filter needs a string `field` and a string `value`");
2008
+ return;
2009
+ }
2010
+ return {
2011
+ field: value.field,
2012
+ value: value.value
2013
+ };
2014
+ }
2015
+ function readRequiresAny(raw, id, report) {
2016
+ if (!raw.requiresAny) return void 0;
2017
+ const { value, line } = raw.requiresAny;
2018
+ if (!Array.isArray(value) || value.length === 0 || !value.every((alt) => Array.isArray(alt) && alt.length > 0)) {
2019
+ report("requiresAny must be a non-empty list of non-empty id lists, e.g. [['a.b', 'c.d']]", line);
2020
+ return;
2021
+ }
2022
+ const out = [];
2023
+ for (const alt of value) {
2024
+ const ids = [];
2025
+ for (const entry of alt) if (typeof entry !== "string" || entry.length > RECIPE_ID_MAX_LENGTH || !RECIPE_ID_PATTERN.test(entry)) report(`requiresAny entry \`${String(entry)}\` is not a recipe id`, line);
2026
+ else if (entry === id) report("a recipe cannot require itself", line);
2027
+ else if (ids.includes(entry)) report(`requiresAny lists \`${entry}\` more than once`, line);
2028
+ else ids.push(entry);
2029
+ out.push(ids);
2030
+ }
2031
+ return out;
2032
+ }
2033
+ function readLabelled(raw, name, allowed, report) {
2034
+ if (!raw) return void 0;
2035
+ const { value, line } = raw;
2036
+ if (!isPlainObject(value)) {
2037
+ report(`${name} must be an object literal`, line);
2038
+ return;
2039
+ }
2040
+ const out = {};
2041
+ let ok = true;
2042
+ for (const [key, entry] of Object.entries(value)) if (!allowed.has(key)) {
2043
+ report(`${name} accepts only ${[...allowed].join(", ")}, not \`${key}\``, line);
2044
+ ok = false;
2045
+ } else if (typeof entry !== "string" || entry.trim() === "") {
2046
+ report(`${name}.${key} must be a non-empty string`, line);
2047
+ ok = false;
2048
+ } else out[key] = entry;
2049
+ if (!out.id || !out.label) {
2050
+ if (ok) report(`${name} needs both \`id\` and \`label\``, line);
2051
+ return;
2052
+ }
2053
+ if (!SLUG_PATTERN.test(out.id)) {
2054
+ report(`${name}.id \`${out.id}\` must be lowercase letters, digits, \`-\` or \`_\``, line);
2055
+ return;
2056
+ }
2057
+ if (out.icon !== void 0 && !ICON_PATTERN.test(out.icon)) {
2058
+ report(`${name}.icon \`${out.icon}\` must be an icon name`, line);
2059
+ return;
2060
+ }
2061
+ return ok ? out : void 0;
2062
+ }
2063
+ var EXPORT_REF_PATTERN = /^((?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:\/[A-Za-z0-9._-]+)*)#([A-Za-z_$][A-Za-z0-9_$]*)$/;
2064
+ var ROUTE_PATH_PATTERN = /^\/(?![/\\])[^\s?#\\]*$/;
2065
+ var PROVIDER_SLUG_PATTERN = /^[a-z][a-z0-9_-]*$/;
2066
+ var SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
2067
+ var DEMO_SEED_MAX_BYTES = 8192;
2068
+ function readExportRef(value, at, report, line, field = "export") {
2069
+ if (typeof value !== "string") {
2070
+ report(`${at}.${field} must be a string like \`pkg/svelte#Component\``, line);
2071
+ return;
2072
+ }
2073
+ const match = EXPORT_REF_PATTERN.exec(value);
2074
+ if (!match || match[1].split("/").some((part) => part === "." || part === ".." || part.endsWith("."))) {
2075
+ report(`${at}.${field} \`${value}\` must be \`<package specifier>#<ExportName>\` (a package specifier, not a relative path)`, line);
2076
+ return;
2077
+ }
2078
+ return value;
2079
+ }
2080
+ function readNonEmpty(value, at, report, line) {
2081
+ if (typeof value !== "string" || value.trim() === "") {
2082
+ report(`${at} must be a non-empty string`, line);
2083
+ return;
2084
+ }
2085
+ return value;
2086
+ }
2087
+ function readSurfaces(raw, report) {
2088
+ if (!raw) return void 0;
2089
+ const { value, line } = raw;
2090
+ if (!Array.isArray(value) || value.length === 0) {
2091
+ report("surfaces must be a non-empty array of surface objects", line);
2092
+ return;
2093
+ }
2094
+ const out = [];
2095
+ const seen = /* @__PURE__ */ new Set();
2096
+ let ok = true;
2097
+ value.forEach((entry, index) => {
2098
+ const at = `surfaces[${index}]`;
2099
+ if (!isPlainObject(entry)) {
2100
+ report(`${at} must be an object literal`, line);
2101
+ ok = false;
2102
+ return;
2103
+ }
2104
+ const kind = entry.kind;
2105
+ if (typeof kind !== "string" || !RECIPE_SURFACE_KINDS.includes(kind)) {
2106
+ report(`${at}.kind must be one of ${RECIPE_SURFACE_KINDS.join(", ")} (got \`${String(kind)}\`)`, line);
2107
+ ok = false;
2108
+ return;
2109
+ }
2110
+ const allowed = {
2111
+ "shell-widget": [
2112
+ "kind",
2113
+ "slot",
2114
+ "export",
2115
+ "label",
2116
+ "icon"
2117
+ ],
2118
+ route: [
2119
+ "kind",
2120
+ "path",
2121
+ "export",
2122
+ "label"
2123
+ ],
2124
+ "settings-panel": [
2125
+ "kind",
2126
+ "export",
2127
+ "label"
2128
+ ],
2129
+ playground: [
2130
+ "kind",
2131
+ "export",
2132
+ "label"
2133
+ ],
2134
+ widget: [...RECIPE_WIDGET_KEYS]
2135
+ };
2136
+ for (const key of Object.keys(entry)) if (!allowed[kind].includes(key)) {
2137
+ report(`${at} (${kind}) does not accept \`${key}\``, line);
2138
+ ok = false;
2139
+ }
2140
+ const before = ok;
2141
+ const ref = readExportRef(entry.export, at, (m, l) => {
2142
+ report(m, l);
2143
+ ok = false;
2144
+ }, line);
2145
+ const fail = (message) => {
2146
+ report(message, line);
2147
+ ok = false;
2148
+ };
2149
+ let label;
2150
+ if (kind === "playground" && entry.label === void 0) label = void 0;
2151
+ else label = readNonEmpty(entry.label, `${at}.label`, fail, line);
2152
+ let identity = `${kind}:${ref}`;
2153
+ const surface = { kind };
2154
+ if (kind === "shell-widget") {
2155
+ const slot = entry.slot;
2156
+ if (typeof slot !== "string" || !RECIPE_SHELL_SLOTS.includes(slot)) fail(`${at}.slot \`${String(slot)}\` is not a shell slot (${RECIPE_SHELL_SLOTS.join(", ")})`);
2157
+ surface.slot = slot;
2158
+ if (entry.icon !== void 0) if (typeof entry.icon !== "string" || !ICON_PATTERN.test(entry.icon)) fail(`${at}.icon must be an icon name`);
2159
+ else surface.icon = entry.icon;
2160
+ identity = `${identity}@${String(slot)}`;
2161
+ } else if (kind === "route") {
2162
+ const path = entry.path;
2163
+ if (typeof path !== "string" || !ROUTE_PATH_PATTERN.test(path)) fail(`${at}.path must start with \`/\` and contain no whitespace, \`?\` or \`#\``);
2164
+ else if (/%2e|%2f|%5c/i.test(path) || path.slice(1).split("/").some((part, i, parts) => part === "." || part === ".." || part === "" && i < parts.length - 1)) fail(`${at}.path cannot contain \`.\`/\`..\` or empty segments, or encoded separators`);
2165
+ else {
2166
+ surface.path = path;
2167
+ identity = `route-path:${path}`;
2168
+ }
2169
+ } else if (kind === "widget") {
2170
+ const extras = readWidgetSurface(entry, at, fail, (value2, where, field) => readExportRef(value2, where, (m, l) => {
2171
+ report(m, l);
2172
+ ok = false;
2173
+ }, line, field));
2174
+ if (extras) Object.assign(surface, extras);
2175
+ if (typeof entry.type === "string") identity = `widget-type:${entry.type}`;
2176
+ }
2177
+ if (ref !== void 0) {
2178
+ if (seen.has(identity)) fail(`${at} repeats an earlier surface`);
2179
+ seen.add(identity);
2180
+ }
2181
+ if (ok === before && ref !== void 0) {
2182
+ surface.export = ref;
2183
+ if (label !== void 0) surface.label = label;
2184
+ const ordered = { kind };
2185
+ const keyOrder = kind === "widget" ? RECIPE_WIDGET_KEYS : [
2186
+ "slot",
2187
+ "path",
2188
+ "export",
2189
+ "label",
2190
+ "icon"
2191
+ ];
2192
+ for (const key of keyOrder) if (surface[key] !== void 0) ordered[key] = surface[key];
2193
+ out.push(ordered);
2194
+ }
2195
+ });
2196
+ return ok ? out : void 0;
2197
+ }
2198
+ function readProviders(raw, report) {
2199
+ if (!raw) return void 0;
2200
+ const { value, line } = raw;
2201
+ if (!Array.isArray(value) || value.length === 0) {
2202
+ report("providers must be a non-empty array of provider objects", line);
2203
+ return;
2204
+ }
2205
+ const out = [];
2206
+ const ids = /* @__PURE__ */ new Set();
2207
+ let ok = true;
2208
+ const fail = (message) => {
2209
+ report(message, line);
2210
+ ok = false;
2211
+ };
2212
+ value.forEach((entry, index) => {
2213
+ const at = `providers[${index}]`;
2214
+ if (!isPlainObject(entry)) {
2215
+ fail(`${at} must be an object literal`);
2216
+ return;
2217
+ }
2218
+ for (const key of Object.keys(entry)) if (![
2219
+ "id",
2220
+ "kind",
2221
+ "options",
2222
+ "required",
2223
+ "secrets",
2224
+ "browserOptions"
2225
+ ].includes(key)) fail(`${at} does not accept \`${key}\``);
2226
+ const { id, kind, options, required, secrets, browserOptions } = entry;
2227
+ if (typeof id !== "string" || !PROVIDER_SLUG_PATTERN.test(id)) fail(`${at}.id must be a lowercase slug`);
2228
+ else if (ids.has(id)) fail(`${at}.id \`${id}\` is declared more than once`);
2229
+ else ids.add(id);
2230
+ if (typeof kind !== "string" || !PROVIDER_SLUG_PATTERN.test(kind)) fail(`${at}.kind must be a lowercase slug such as email, oauth, storage`);
2231
+ if (!Array.isArray(options) || options.length === 0 || !options.every((o) => typeof o === "string" && PROVIDER_SLUG_PATTERN.test(o)) || new Set(options).size !== options.length) fail(`${at}.options must be a non-empty list of distinct lowercase slugs`);
2232
+ if (typeof required !== "boolean") fail(`${at}.required must be written as true or false`);
2233
+ if (secrets !== void 0 && (!Array.isArray(secrets) || !secrets.every((n) => typeof n === "string" && SECRET_NAME_PATTERN.test(n)) || new Set(secrets).size !== secrets.length)) fail(`${at}.secrets must be distinct UPPER_SNAKE names (names, not values)`);
2234
+ if (browserOptions !== void 0 && (!Array.isArray(browserOptions) || browserOptions.length === 0 || new Set(browserOptions).size !== browserOptions.length || !browserOptions.every((o) => typeof o === "string" && Array.isArray(options) && options.includes(o)))) fail(`${at}.browserOptions must be a non-empty list of distinct entries of options`);
2235
+ if (ok) out.push({
2236
+ id,
2237
+ kind,
2238
+ options,
2239
+ required,
2240
+ ...secrets ? { secrets } : {},
2241
+ ...browserOptions ? { browserOptions } : {}
2242
+ });
2243
+ });
2244
+ return ok ? out : void 0;
2245
+ }
2246
+ function readRuntime(raw, report) {
2247
+ if (!raw) return void 0;
2248
+ const { value, line } = raw;
2249
+ if (typeof value !== "string" || !RECIPE_RUNTIMES.includes(value)) {
2250
+ report(`runtime must be one of ${RECIPE_RUNTIMES.join(", ")}`, line);
2251
+ return;
2252
+ }
2253
+ return value;
2254
+ }
2255
+ function readDemoSeed(raw, report) {
2256
+ if (!raw) return void 0;
2257
+ const { value, line } = raw;
2258
+ if (!isPlainObject(value)) {
2259
+ report("demoSeed must be { export } or { data }", line);
2260
+ return;
2261
+ }
2262
+ const keys = Object.keys(value);
2263
+ if (keys.length !== 1 || keys[0] !== "export" && keys[0] !== "data") {
2264
+ report("demoSeed must have exactly one of `export` or `data`", line);
2265
+ return;
2266
+ }
2267
+ if (keys[0] === "export") {
2268
+ const ref = readExportRef(value.export, "demoSeed", report, line);
2269
+ return ref ? { export: ref } : void 0;
2270
+ }
2271
+ const finite = (node) => typeof node === "number" ? Number.isFinite(node) : Array.isArray(node) ? node.every(finite) : isPlainObject(node) ? Object.values(node).every(finite) : true;
2272
+ if (!finite(value.data)) {
2273
+ report("demoSeed.data cannot contain non-finite numbers", line);
2274
+ return;
2275
+ }
2276
+ const size = Buffer.byteLength(JSON.stringify(value.data ?? null));
2277
+ if (value.data === void 0 || value.data === null) {
2278
+ report("demoSeed.data must be JSON data", line);
2279
+ return;
2280
+ }
2281
+ if (size > DEMO_SEED_MAX_BYTES) {
2282
+ report(`demoSeed.data is ${size} bytes; inline seeds are limited to ${DEMO_SEED_MAX_BYTES}, reference a fixture export instead`, line);
2283
+ return;
2284
+ }
2285
+ return { data: value.data };
2286
+ }
1642
2287
  function resolveRecipes(raws, classes) {
1643
2288
  const errors = [];
1644
2289
  const recipes = [];
@@ -1688,16 +2333,35 @@ function resolveRecipes(raws, classes) {
1688
2333
  report(`nav entry \`${entry.label}\` points at \`${model.className}\`, which is not listed in models`, entry.line);
1689
2334
  continue;
1690
2335
  }
2336
+ const filter = readNavFilter(entry.filter, (message) => report(`nav entry \`${entry.label}\`: ${message}`, entry.line));
2337
+ if (entry.key !== void 0 && !SLUG_PATTERN.test(entry.key)) report(`nav entry \`${entry.label}\`: key \`${entry.key}\` must be lowercase letters, digits, \`-\` or \`_\``, entry.line);
2338
+ if (entry.icon !== void 0 && !ICON_PATTERN.test(entry.icon)) report(`nav entry \`${entry.label}\`: icon \`${entry.icon}\` must be an icon name`, entry.line);
2339
+ for (const text of ["description", "noun"]) if (entry[text] !== void 0 && entry[text]?.trim() === "") report(`nav entry \`${entry.label}\`: ${text} must not be empty`, entry.line);
2340
+ if (filter && entry.key === void 0) report(`nav entry \`${entry.label}\`: a filter needs a key, which names its view`, entry.line);
1691
2341
  nav.push({
1692
2342
  label: entry.label,
1693
- model: model.className
2343
+ model: model.className,
2344
+ ...entry.icon !== void 0 ? { icon: entry.icon } : {},
2345
+ ...entry.description !== void 0 ? { description: entry.description } : {},
2346
+ ...entry.key !== void 0 ? { key: entry.key } : {},
2347
+ ...entry.noun !== void 0 ? { noun: entry.noun } : {},
2348
+ ...filter ? { filter } : {}
1694
2349
  });
1695
2350
  }
2351
+ const viewKeys = /* @__PURE__ */ new Set();
2352
+ for (const entry of nav) {
2353
+ const view = `${entry.model}:${entry.key ?? ""}`;
2354
+ if (viewKeys.has(view)) report(`nav entry \`${entry.label}\` repeats the view of ${entry.model}${entry.key ? ` key \`${entry.key}\`` : ""}; give each extra entry over a model its own key`);
2355
+ viewKeys.add(view);
2356
+ }
1696
2357
  const requires = [];
1697
2358
  for (const required of raw.requires) if (required.length > RECIPE_ID_MAX_LENGTH || !RECIPE_ID_PATTERN.test(required)) report(`requires \`${required}\` is not a recipe id`);
1698
2359
  else if (required === id) report("a recipe cannot require itself");
1699
2360
  else if (requires.includes(required)) report(`requires lists \`${required}\` more than once`);
1700
2361
  else requires.push(required);
2362
+ const requiresAny = readRequiresAny(raw, id, report);
2363
+ const group = readLabelled(raw.group, "group", GROUP_KEYS, report);
2364
+ const section = readLabelled(raw.section, "section", SECTION_KEYS, report);
1701
2365
  let options;
1702
2366
  for (const entry of raw.options) {
1703
2367
  const model = byLocal.get(entry.key);
@@ -1712,6 +2376,10 @@ function resolveRecipes(raws, classes) {
1712
2376
  else options[model.className] = read;
1713
2377
  }
1714
2378
  }
2379
+ const surfaces = readSurfaces(raw.surfaces, report);
2380
+ const providers = readProviders(raw.providers, report);
2381
+ const runtime = readRuntime(raw.runtime, report);
2382
+ const demoSeed = readDemoSeed(raw.demoSeed, report);
1715
2383
  const help = raw.help ? attempt(() => readHelp(raw.help, raw), raw.help.line) : void 0;
1716
2384
  recipes.push({
1717
2385
  id,
@@ -1722,9 +2390,52 @@ function resolveRecipes(raws, classes) {
1722
2390
  models: models.map((model) => model.className),
1723
2391
  nav,
1724
2392
  requires,
2393
+ ...requiresAny ? { requiresAny } : {},
2394
+ ...group ? { group } : {},
2395
+ ...section ? { section } : {},
1725
2396
  ...options && Object.keys(options).length > 0 ? { options } : {},
1726
- ...help ? { help } : {}
2397
+ ...help ? { help } : {},
2398
+ ...surfaces ? { surfaces } : {},
2399
+ ...providers ? { providers } : {},
2400
+ ...runtime ? { runtime } : {},
2401
+ ...demoSeed ? { demoSeed } : {}
2402
+ });
2403
+ }
2404
+ recipes.sort((a, b) => a.id.localeCompare(b.id));
2405
+ const navKeys = /* @__PURE__ */ new Map();
2406
+ const widgetTypes = /* @__PURE__ */ new Map();
2407
+ const labelled = /* @__PURE__ */ new Map();
2408
+ for (const recipe of recipes) {
2409
+ const raw = ids.get(recipe.id);
2410
+ const note = (message) => errors.push({
2411
+ message: `Recipe ${recipe.className}: ${message}`,
2412
+ filePath: raw?.filePath ?? "",
2413
+ line: raw?.line,
2414
+ severity: "error"
1727
2415
  });
2416
+ for (const entry of recipe.nav) {
2417
+ if (!entry.key) continue;
2418
+ const slot = `${entry.model}:${entry.key}`;
2419
+ const owner = navKeys.get(slot);
2420
+ if (owner && owner !== recipe.id) note(`nav key \`${entry.key}\` over ${entry.model} is already used by recipe ${owner}; layout ids must be unique`);
2421
+ navKeys.set(slot, owner ?? recipe.id);
2422
+ }
2423
+ for (const surface of recipe.surfaces ?? []) {
2424
+ if (surface.kind !== "widget") continue;
2425
+ const owner = widgetTypes.get(surface.type);
2426
+ if (owner && owner !== recipe.id) note(`widget type \`${surface.type}\` is already used by recipe ${owner}; widget types must be unique`);
2427
+ widgetTypes.set(surface.type, owner ?? recipe.id);
2428
+ }
2429
+ for (const [kind, value] of [["group", recipe.group], ["section", recipe.section]]) {
2430
+ if (!value) continue;
2431
+ const slot = `${kind}:${value.id}`;
2432
+ const first = labelled.get(slot);
2433
+ if (first && first.label !== value.label) note(`${kind} \`${value.id}\` is labelled "${value.label}" here but "${first.label}" in recipe ${first.recipe}`);
2434
+ else if (!first) labelled.set(slot, {
2435
+ label: value.label,
2436
+ recipe: recipe.id
2437
+ });
2438
+ }
1728
2439
  }
1729
2440
  const byId = new Map(recipes.map((recipe) => [recipe.id, recipe]));
1730
2441
  const reported = /* @__PURE__ */ new Set();
@@ -2306,13 +3017,13 @@ function reportUnresolvedSpreads(unresolved, filePath, sourceText, errors) {
2306
3017
  });
2307
3018
  }
2308
3019
  }
2309
- function extractClassFromNode(node, filePath, sourceText, importAliases, ctx) {
2310
- if (node.type === "ExportNamedDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx);
2311
- if (node.type === "ExportDefaultDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx);
2312
- if (node.type === "ClassDeclaration") return extractClassDeclaration(node, filePath, sourceText, importAliases, ctx);
3020
+ function extractClassFromNode(node, filePath, sourceText, importAliases, ctx, outerStart) {
3021
+ if (node.type === "ExportNamedDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx, node.start ?? outerStart);
3022
+ if (node.type === "ExportDefaultDeclaration" && node.declaration) return extractClassFromNode(node.declaration, filePath, sourceText, importAliases, ctx, node.start ?? outerStart);
3023
+ if (node.type === "ClassDeclaration") return extractClassDeclaration(node, filePath, sourceText, importAliases, ctx, outerStart);
2313
3024
  return null;
2314
3025
  }
2315
- function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx) {
3026
+ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx, outerStart) {
2316
3027
  const className = node.id?.name || "AnonymousClass";
2317
3028
  const decorators = node.decorators || [];
2318
3029
  const smrtDecorator = decorators.find((d) => isSmrtDecorator(d, importAliases));
@@ -2338,6 +3049,11 @@ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx)
2338
3049
  const method = extractMethodDefinition(member, sourceText, ctx);
2339
3050
  if (method) methods.push(method);
2340
3051
  }
3052
+ const description = (typeof smrtConfig?.description === "string" ? normalizeModelDescription(smrtConfig.description) : void 0) ?? extractClassJsDocDescription(sourceText, className, [
3053
+ node.start,
3054
+ outerStart,
3055
+ ...decorators.map((d) => d.start)
3056
+ ].filter((n) => typeof n === "number"));
2341
3057
  return {
2342
3058
  className,
2343
3059
  filePath,
@@ -2345,6 +3061,7 @@ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx)
2345
3061
  extendsTypeArg,
2346
3062
  decoratorConfig,
2347
3063
  hasSmartDecorator,
3064
+ ...description ? { description } : {},
2348
3065
  fields,
2349
3066
  methods,
2350
3067
  startLine: node.loc?.start.line || 1,
@@ -2352,6 +3069,59 @@ function extractClassDeclaration(node, filePath, sourceText, importAliases, ctx)
2352
3069
  ...typeof node.body?.start === "number" ? { bodyStart: node.body.start } : {}
2353
3070
  };
2354
3071
  }
3072
+ var MODEL_DESCRIPTION_MAX = 200;
3073
+ function normalizeModelDescription(raw) {
3074
+ const text = raw.replace(/\s+/g, " ").trim();
3075
+ if (!text) return void 0;
3076
+ if (text.length <= MODEL_DESCRIPTION_MAX) return text;
3077
+ const head = text.slice(0, MODEL_DESCRIPTION_MAX);
3078
+ const sentenceEnd = Math.max(head.lastIndexOf(". "), head.lastIndexOf("! "), head.lastIndexOf("? "));
3079
+ if (sentenceEnd > 0) return head.slice(0, sentenceEnd + 1);
3080
+ const wordEnd = head.lastIndexOf(" ");
3081
+ return `${(wordEnd > 0 ? head.slice(0, wordEnd) : head).replace(/[,;:\s]+$/, "")}\u2026`;
3082
+ }
3083
+ function extractClassJsDocDescription(sourceText, className, anchors) {
3084
+ if (anchors.length === 0) return void 0;
3085
+ const before = sourceText.slice(0, Math.min(...anchors)).trimEnd();
3086
+ if (!before.endsWith("*/")) return void 0;
3087
+ const open = before.lastIndexOf("/**");
3088
+ if (open < 0 || before.indexOf("*/", open) !== before.length - 2) return;
3089
+ return summarizeJsDoc(before.slice(open, before.length), className);
3090
+ }
3091
+ function summarizeJsDoc(block, className) {
3092
+ const lines = block.replace(/^\/\*\*/, "").replace(/\*\/$/, "").split("\n").map((line) => line.replace(/^\s*\*\s?/, "").trimEnd());
3093
+ const summary = [];
3094
+ let internal = false;
3095
+ let inFence = false;
3096
+ for (const line of lines) {
3097
+ if (/^\s*@internal\b/.test(line)) internal = true;
3098
+ if (/^\s*```/.test(line)) {
3099
+ inFence = !inFence;
3100
+ continue;
3101
+ }
3102
+ if (inFence) continue;
3103
+ if (/^\s*@\w+/.test(line)) {
3104
+ summary.push("\0");
3105
+ continue;
3106
+ }
3107
+ summary.push(line);
3108
+ }
3109
+ if (internal) return void 0;
3110
+ const paragraph = [];
3111
+ for (const line of summary) {
3112
+ if (line === "\0") break;
3113
+ if (line.trim() === "") {
3114
+ if (paragraph.length > 0) break;
3115
+ continue;
3116
+ }
3117
+ paragraph.push(line.trim());
3118
+ }
3119
+ let text = paragraph.join(" ");
3120
+ const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3121
+ text = text.replace(new RegExp(`^${escaped}\\s*(?:[-\u2013\u2014:]+)\\s*`), "").replace(/\{@(?:link|linkcode|linkplain)\s+([^}|\s]+)(?:[|\s]+([^}]*))?\}/g, (_m, target, label) => label?.trim() ? label.trim() : target).replace(/\{@\w+\s*([^}]*)\}/g, "$1").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/`([^`]*)`/g, "$1").replace(/<\/?[a-zA-Z][^>]*>/g, "").replace(/(\*\*|__)(.+?)\1/g, "$2");
3122
+ if (text.length > 0) text = text[0].toUpperCase() + text.slice(1);
3123
+ return normalizeModelDescription(text);
3124
+ }
2355
3125
  function isSmrtDecorator(decorator, importAliases) {
2356
3126
  return isNamedDecorator(decorator, "smrt", importAliases);
2357
3127
  }
@@ -2959,7 +3729,7 @@ var ManifestAdapter = class ManifestAdapter {
2959
3729
  ...recipe,
2960
3730
  models: recipe.models.map(qualify),
2961
3731
  nav: recipe.nav.map((entry) => ({
2962
- label: entry.label,
3732
+ ...entry,
2963
3733
  model: qualify(entry.model)
2964
3734
  })),
2965
3735
  ...recipe.options ? { options: Object.fromEntries(Object.entries(recipe.options).map(([model, value]) => [qualify(model), value])) } : {}
@@ -3023,7 +3793,7 @@ var ManifestAdapter = class ManifestAdapter {
3023
3793
  const converted = this.convertMethod(method);
3024
3794
  if (converted) methods[method.name] = converted;
3025
3795
  }
3026
- const collection = this.pluralize(classDef.className);
3796
+ const collection = typeof classDef.decoratorConfig?.collection === "string" ? classDef.decoratorConfig.collection : this.pluralize(classDef.className);
3027
3797
  const packageName = options.packageName || classDef.packageName;
3028
3798
  const qualifiedName = packageName ? createQualifiedName(packageName, classDef.className) : void 0;
3029
3799
  return {
@@ -3031,6 +3801,7 @@ var ManifestAdapter = class ManifestAdapter {
3031
3801
  className: classDef.className,
3032
3802
  qualifiedName,
3033
3803
  collection,
3804
+ ...classDef.description ? { description: classDef.description } : {},
3034
3805
  filePath: classDef.filePath,
3035
3806
  packageName: packageName || void 0,
3036
3807
  fields,
@@ -3234,7 +4005,8 @@ var ManifestAdapter = class ManifestAdapter {
3234
4005
  "description",
3235
4006
  "default",
3236
4007
  "constraint",
3237
- "onDelete"
4008
+ "onDelete",
4009
+ "onUpdate"
3238
4010
  ];
3239
4011
  if (parsedOptions) {
3240
4012
  for (const key of META_KEYS) if (parsedOptions[key] !== void 0) meta[key] = parsedOptions[key];
@@ -3283,7 +4055,8 @@ var ManifestAdapter = class ManifestAdapter {
3283
4055
  "default",
3284
4056
  "indexed",
3285
4057
  "idType",
3286
- "onDelete"
4058
+ "onDelete",
4059
+ "onUpdate"
3287
4060
  ];
3288
4061
  if (parsedOptions) {
3289
4062
  for (const key of META_KEYS) if (parsedOptions[key] !== void 0) meta[key] = parsedOptions[key];
@@ -4138,4 +4911,4 @@ var OxcScanner = class {
4138
4911
  //#endregion
4139
4912
  export { mergeAgentSurfaces as C, isPrunedAgentSurfacePath as S, sourceMayDeclareAgentSurface as T, relativeGlobToCwd as _, parseAgentSurfaceFile as a, extractAgentSurface as b, parseSource as c, resolveRecipes as d, sourceMayDeclareRecipe as f, normalizeGlobSeparators as g, discoverSourceFiles as h, extractSmrtImports as i, deriveHelpFieldRefs as l, resolveEnumValues as m, parseUiSelectorsFile as n, parseFile as o, InheritanceResolver as p, ManifestAdapter as r, parseRecipeFile as s, OxcScanner as t, extractRecipes as u, checkAgentSurfaceToolNames as v, scanSvelteAgentSurface as w, isAgentSurfaceSourcePath as x, emptyAgentSurface as y };
4140
4913
 
4141
- //# sourceMappingURL=scanner-CeTWAIar.js.map
4914
+ //# sourceMappingURL=scanner-BbpPS8Aq.js.map