@changfenhuang/dsh-genui 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { readFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
+ import { basename, dirname, join, resolve } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  //#region src/client/spec.ts
5
6
  /**
@@ -138,7 +139,7 @@ function enu(v, values) {
138
139
  return inEnum(v, values) ? v : void 0;
139
140
  }
140
141
  /** Plain object (not array, not null). */
141
- function obj(v) {
142
+ function obj$1(v) {
142
143
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
143
144
  }
144
145
  /**
@@ -280,7 +281,7 @@ function repairItems(list, ctx, depth) {
280
281
  }
281
282
  function repairNode(value, ctx, depth) {
282
283
  if (depth > GENUI_LIMITS.maxDepth) return null;
283
- const v = obj(value);
284
+ const v = obj$1(value);
284
285
  if (v === void 0) return null;
285
286
  const type = v.type;
286
287
  if (typeof type !== "string") return null;
@@ -369,6 +370,15 @@ function repairNode(value, ctx, depth) {
369
370
  ...opt("href", safeHref(v.href))
370
371
  };
371
372
  }
373
+ case "image": {
374
+ const src = safeMediaSrc(v.src);
375
+ if (src === void 0) return null;
376
+ return {
377
+ type: "image",
378
+ src,
379
+ ...opt("alt", str(v.alt, GENUI_LIMITS.maxString))
380
+ };
381
+ }
372
382
  case "audio": {
373
383
  const src = safeMediaSrc(v.src);
374
384
  if (src === void 0) return null;
@@ -720,7 +730,7 @@ function repairListItems(v, cap, ctx, depth) {
720
730
  out.push(item.slice(0, GENUI_LIMITS.maxString));
721
731
  continue;
722
732
  }
723
- const o = obj(item);
733
+ const o = obj$1(item);
724
734
  const title = o === void 0 ? void 0 : str(o.title, GENUI_LIMITS.maxString);
725
735
  if (title !== void 0) {
726
736
  out.push({
@@ -759,7 +769,7 @@ function repairChartData(v, cap) {
759
769
  const out = [];
760
770
  for (const datum of v) {
761
771
  if (out.length >= cap) break;
762
- const o = obj(datum);
772
+ const o = obj$1(datum);
763
773
  const label = o === void 0 ? void 0 : str(o.label, 128);
764
774
  const value = o === void 0 ? void 0 : num(o.value, -0xe8d4a51000, 0xe8d4a51000);
765
775
  if (label === void 0 || value === void 0) continue;
@@ -776,7 +786,7 @@ function repairSeries(v, cap, pointCap) {
776
786
  const out = [];
777
787
  for (const s of v) {
778
788
  if (out.length >= cap) break;
779
- const o = obj(s);
789
+ const o = obj$1(s);
780
790
  const label = o === void 0 ? void 0 : str(o.label, 128);
781
791
  const data = o === void 0 ? void 0 : repairChartData(o.data, pointCap);
782
792
  if (label === void 0 || data === void 0) continue;
@@ -793,7 +803,7 @@ function repairTabs(v, ctx, depth) {
793
803
  const out = [];
794
804
  for (const tab of v) {
795
805
  if (out.length >= GENUI_LIMITS.maxTabs) break;
796
- const o = obj(tab);
806
+ const o = obj$1(tab);
797
807
  const label = o === void 0 ? void 0 : str(o.label, 128);
798
808
  if (label === void 0 || o === void 0) continue;
799
809
  const rawItems = o.items !== void 0 ? o.items : o.content !== void 0 ? Array.isArray(o.content) ? o.content : [o.content] : void 0;
@@ -806,7 +816,7 @@ function repairTabs(v, ctx, depth) {
806
816
  }
807
817
  /** Header text for an object-shaped table column ({title,key} antd style). */
808
818
  function columnHeaderText(c) {
809
- const o = obj(c);
819
+ const o = obj$1(c);
810
820
  if (o === void 0) return String(c);
811
821
  for (const k of [
812
822
  "title",
@@ -821,7 +831,7 @@ function columnHeaderText(c) {
821
831
  }
822
832
  /** Row key for an object-shaped column, mirroring columnHeaderText's order. */
823
833
  function columnKeyOf(c) {
824
- const o = obj(c);
834
+ const o = obj$1(c);
825
835
  if (o === void 0) return void 0;
826
836
  for (const k of [
827
837
  "key",
@@ -847,13 +857,13 @@ function repairPlotSeries(v, cap) {
847
857
  const out = [];
848
858
  for (const s of v) {
849
859
  if (out.length >= cap) break;
850
- const o = obj(s);
860
+ const o = obj$1(s);
851
861
  const expr = o === void 0 ? void 0 : str(o.expr, 512);
852
862
  if (expr === void 0 || o === void 0) continue;
853
863
  const params = [];
854
864
  if (Array.isArray(o.params)) for (const p of o.params) {
855
865
  if (params.length >= GENUI_LIMITS.maxPlotParams) break;
856
- const po = obj(p);
866
+ const po = obj$1(p);
857
867
  const name = po === void 0 ? void 0 : str(po.name, 64);
858
868
  const value = po === void 0 ? void 0 : num(po.value, -1e9, 1e9);
859
869
  if (name === void 0 || value === void 0) continue;
@@ -883,7 +893,7 @@ function repairSteps(v) {
883
893
  const out = [];
884
894
  for (const s of v) {
885
895
  if (out.length >= GENUI_LIMITS.maxSteps) break;
886
- const o = obj(s);
896
+ const o = obj$1(s);
887
897
  const title = o === void 0 ? void 0 : str(o.title, 256);
888
898
  if (title === void 0) continue;
889
899
  out.push({
@@ -898,7 +908,7 @@ function repairPairs(v, cap) {
898
908
  const out = [];
899
909
  for (const p of v) {
900
910
  if (out.length >= cap) break;
901
- const o = obj(p);
911
+ const o = obj$1(p);
902
912
  const key = o === void 0 ? void 0 : str(o.key, 256);
903
913
  const value = o === void 0 ? void 0 : str(o.value, GENUI_LIMITS.maxString);
904
914
  if (key === void 0 || value === void 0) continue;
@@ -914,7 +924,7 @@ function repairDiffs(v) {
914
924
  const out = [];
915
925
  for (const d of v) {
916
926
  if (out.length >= 24) break;
917
- const o = obj(d);
927
+ const o = obj$1(d);
918
928
  const path = o === void 0 ? void 0 : str(o.path, 1024);
919
929
  const newText = o === void 0 ? void 0 : str(o.newText, 2e4);
920
930
  if (path === void 0 || newText === void 0) continue;
@@ -932,7 +942,7 @@ function repairAccordion(v, ctx, depth) {
932
942
  const out = [];
933
943
  for (const item of v) {
934
944
  if (out.length >= GENUI_LIMITS.maxAccordionItems) break;
935
- const o = obj(item);
945
+ const o = obj$1(item);
936
946
  const title = o === void 0 ? void 0 : str(o.title, 256);
937
947
  if (title === void 0 || o === void 0) continue;
938
948
  out.push({
@@ -947,7 +957,7 @@ function repairMeshes(v) {
947
957
  const out = [];
948
958
  for (const m of v) {
949
959
  if (out.length >= GENUI_LIMITS.maxMeshes) break;
950
- const o = obj(m);
960
+ const o = obj$1(m);
951
961
  const shape = o === void 0 ? void 0 : enu(o.shape, MESH_SHAPES);
952
962
  if (shape === void 0) continue;
953
963
  const scale = o === void 0 ? void 0 : num(o.scale, -1e6, 1e6) ?? tuple3(o.scale);
@@ -983,7 +993,7 @@ function repairDiagramNodes(v) {
983
993
  const seen = /* @__PURE__ */ new Set();
984
994
  for (const raw of v) {
985
995
  if (out.length >= GENUI_LIMITS.maxDiagramNodes) break;
986
- const o = obj(raw);
996
+ const o = obj$1(raw);
987
997
  if (o === void 0) continue;
988
998
  const id = str(o.id, 128);
989
999
  const label = str(o.label, GENUI_LIMITS.maxString);
@@ -1015,7 +1025,7 @@ function repairDiagramEdges(v) {
1015
1025
  const out = [];
1016
1026
  for (const raw of v) {
1017
1027
  if (out.length >= GENUI_LIMITS.maxDiagramEdges) break;
1018
- const o = obj(raw);
1028
+ const o = obj$1(raw);
1019
1029
  if (o === void 0) continue;
1020
1030
  const from = str(o.from, 128);
1021
1031
  const to = str(o.to, 128);
@@ -1031,7 +1041,7 @@ function repairDiagramEdges(v) {
1031
1041
  return out;
1032
1042
  }
1033
1043
  function repairDiagramTheme(v) {
1034
- const o = obj(v);
1044
+ const o = obj$1(v);
1035
1045
  if (o === void 0) return void 0;
1036
1046
  const out = {};
1037
1047
  for (const key of [
@@ -1056,7 +1066,7 @@ function repairDiagramZones(v) {
1056
1066
  const out = [];
1057
1067
  for (const raw of v) {
1058
1068
  if (out.length >= GENUI_LIMITS.maxDiagramZones) break;
1059
- const o = obj(raw);
1069
+ const o = obj$1(raw);
1060
1070
  if (o === void 0) continue;
1061
1071
  const label = str(o.label, 64);
1062
1072
  if (label === void 0) continue;
@@ -1071,7 +1081,7 @@ function repairDiagramZones(v) {
1071
1081
  return out;
1072
1082
  }
1073
1083
  function repairDiagram(v) {
1074
- const o = obj(v);
1084
+ const o = obj$1(v);
1075
1085
  if (o === void 0) return null;
1076
1086
  const kind = enu(o.kind, DIAGRAM_KINDS);
1077
1087
  if (kind === void 0) return null;
@@ -1097,7 +1107,7 @@ function repairTimeline(v, cap) {
1097
1107
  const out = [];
1098
1108
  for (const item of v) {
1099
1109
  if (out.length >= cap) break;
1100
- const o = obj(item);
1110
+ const o = obj$1(item);
1101
1111
  const title = o === void 0 ? void 0 : str(o.title, 256);
1102
1112
  if (title === void 0) continue;
1103
1113
  out.push({
@@ -1116,7 +1126,7 @@ function walkTree(v, cap, depthLeft) {
1116
1126
  const out = [];
1117
1127
  for (const item of v) {
1118
1128
  if (out.length >= cap) break;
1119
- const o = obj(item);
1129
+ const o = obj$1(item);
1120
1130
  const name = o === void 0 ? void 0 : str(o.name, 256);
1121
1131
  if (name === void 0) continue;
1122
1132
  const children = o !== void 0 && depthLeft > 0 && Array.isArray(o.children) ? walkTree(o.children, cap, depthLeft - 1) : void 0;
@@ -1133,7 +1143,7 @@ function repairQuizOptions(v, answer) {
1133
1143
  const out = [];
1134
1144
  for (const optItem of v) {
1135
1145
  if (out.length >= GENUI_LIMITS.maxQuizOptions) break;
1136
- const o = obj(optItem);
1146
+ const o = obj$1(optItem);
1137
1147
  const label = typeof optItem === "string" ? str(optItem, 512) : o === void 0 ? void 0 : str(o.label, 512);
1138
1148
  if (label === void 0) continue;
1139
1149
  out.push({
@@ -1200,7 +1210,7 @@ function sanitizeEChartOption(v, depth, budget) {
1200
1210
  }
1201
1211
  return arr.length > 0 ? arr : void 0;
1202
1212
  }
1203
- const o = obj(v);
1213
+ const o = obj$1(v);
1204
1214
  if (o === void 0) return void 0;
1205
1215
  const out = {};
1206
1216
  for (const [key, val] of Object.entries(o)) {
@@ -1220,7 +1230,7 @@ function sanitizeEChartOption(v, depth, budget) {
1220
1230
  * no-op.
1221
1231
  */
1222
1232
  function repairGenuiSpec(value) {
1223
- const v = obj(value);
1233
+ const v = obj$1(value);
1224
1234
  if (v === void 0) return null;
1225
1235
  if (!Array.isArray(v.items)) {
1226
1236
  const wrapped = wrapSingleComponentRoot(value);
@@ -1252,28 +1262,28 @@ function countGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1252
1262
  for (const item of list) {
1253
1263
  if (count >= cap) return;
1254
1264
  count += 1;
1255
- const v = obj(item);
1265
+ const v = obj$1(item);
1256
1266
  if (v === void 0) continue;
1257
1267
  if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) {
1258
1268
  if (count >= cap) return;
1259
- const to = obj(t);
1269
+ const to = obj$1(t);
1260
1270
  if (to !== void 0) walk(to.items);
1261
1271
  }
1262
1272
  else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) {
1263
1273
  if (count >= cap) return;
1264
- const io = obj(it);
1274
+ const io = obj$1(it);
1265
1275
  if (io !== void 0) walk(io.items);
1266
1276
  }
1267
1277
  else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
1268
1278
  else if (v.type === "file-tree" && Array.isArray(v.items)) walk(v.items);
1269
1279
  else if (v.type === "list" && Array.isArray(v.items)) for (const li of v.items) {
1270
1280
  if (count >= cap) return;
1271
- const lo = obj(li);
1281
+ const lo = obj$1(li);
1272
1282
  if (lo !== void 0 && typeof lo.type === "string") walk([lo]);
1273
1283
  }
1274
1284
  }
1275
1285
  };
1276
- const root = obj(value);
1286
+ const root = obj$1(value);
1277
1287
  walk(root === void 0 ? [] : root.items);
1278
1288
  return count;
1279
1289
  }
@@ -1298,6 +1308,7 @@ const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
1298
1308
  "divider",
1299
1309
  "file-tree",
1300
1310
  "grid",
1311
+ "image",
1301
1312
  "input",
1302
1313
  "json",
1303
1314
  "keyvalue",
@@ -1327,47 +1338,108 @@ const GENUI_NODE_TYPES = /* @__PURE__ */ new Set([
1327
1338
  "diagram"
1328
1339
  ]);
1329
1340
  /**
1330
- * Count DECLARED nodes in a raw spec tree: objects whose `type` is a
1341
+ * Visit and count declared nodes in a raw spec tree: objects whose `type` is a
1331
1342
  * white-listed string, descending the same containers `countGenuiNodes`
1332
- * walks. `validate_dsh_ui` compares this with the repaired count to surface
1333
- * children the repair silently dropped (blank-render class of bugs, issue
1334
- * #42) instead of reporting a green check on a half-empty tree.
1343
+ * walks. Callers can inspect field semantics or compare the count with the
1344
+ * repaired tree without maintaining another traversal.
1335
1345
  */
1336
- function countDeclaredGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1346
+ function visitDeclaredGenuiNodes(value, cap, visit) {
1337
1347
  let count = 0;
1338
1348
  const declared = (candidate) => {
1339
- const o = obj(candidate);
1349
+ const o = obj$1(candidate);
1340
1350
  return o !== void 0 && typeof o.type === "string" && GENUI_NODE_TYPES.has(o.type);
1341
1351
  };
1342
- const walk = (list) => {
1352
+ function walk(list, path) {
1343
1353
  if (!Array.isArray(list)) return;
1344
- for (const item of list) {
1354
+ for (let index = 0; index < list.length; index++) {
1355
+ walkNode(list[index], `${path}[${index}]`);
1345
1356
  if (count >= cap) return;
1346
- if (!declared(item)) continue;
1347
- count += 1;
1348
- const v = obj(item);
1349
- if (v === void 0) continue;
1350
- if (v.type === "tabs" && Array.isArray(v.tabs)) for (const t of v.tabs) walkItemsOf(t);
1351
- else if (v.type === "accordion" && Array.isArray(v.items)) for (const it of v.items) walkItemsOf(it);
1352
- else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items);
1353
- else if (v.type === "list" && Array.isArray(v.items)) {
1354
- for (const li of v.items) if (declared(li)) walk([li]);
1355
- }
1356
1357
  }
1357
- };
1358
- const walkItemsOf = (holder) => {
1359
- const o = obj(holder);
1358
+ }
1359
+ function walkNode(item, at) {
1360
+ if (count >= cap || !declared(item)) return;
1361
+ const v = obj$1(item);
1362
+ if (v === void 0) return;
1363
+ count += 1;
1364
+ visit(v, at);
1365
+ if (v.type === "tabs" && Array.isArray(v.tabs)) for (let tab = 0; tab < v.tabs.length; tab++) walkItemsOf(v.tabs[tab], `${at}.tabs[${tab}]`);
1366
+ else if (v.type === "accordion" && Array.isArray(v.items)) for (let row = 0; row < v.items.length; row++) walkItemsOf(v.items[row], `${at}.items[${row}]`);
1367
+ else if ((v.type === "row" || v.type === "col" || v.type === "grid" || v.type === "card") && Array.isArray(v.items)) walk(v.items, `${at}.items`);
1368
+ else if (v.type === "list" && Array.isArray(v.items)) for (let row = 0; row < v.items.length; row++) walkNode(v.items[row], `${at}.items[${row}]`);
1369
+ }
1370
+ function walkItemsOf(holder, path) {
1371
+ const o = obj$1(holder);
1360
1372
  if (o === void 0) return;
1361
1373
  const items = o.items !== void 0 ? o.items : o.content;
1362
- if (Array.isArray(items)) walk(items);
1363
- else if (declared(items)) walk([items]);
1364
- };
1365
- const root = obj(value);
1374
+ if (Array.isArray(items)) walk(items, `${path}.items`);
1375
+ else walkNode(items, `${path}.items`);
1376
+ }
1377
+ const root = obj$1(value);
1366
1378
  if (root === void 0) return count;
1367
- if (!Array.isArray(root.items) && declared(value)) walk([value]);
1368
- else walk(root.items);
1379
+ if (!Array.isArray(root.items) && declared(value)) walkNode(value, "spec");
1380
+ else walk(root.items, "items");
1369
1381
  return count;
1370
1382
  }
1383
+ /**
1384
+ * Count white-listed nodes declared by a raw spec before repair drops invalid entries.
1385
+ * @param value - raw GenUI spec.
1386
+ * @param cap - traversal ceiling.
1387
+ * @returns declared node count up to the ceiling.
1388
+ */
1389
+ function countDeclaredGenuiNodes(value, cap = Number.POSITIVE_INFINITY) {
1390
+ return visitDeclaredGenuiNodes(value, cap, () => {});
1391
+ }
1392
+ /**
1393
+ * Return field-level chart errors without changing other repairable component families.
1394
+ * @param value - raw GenUI spec.
1395
+ * @returns chart semantic errors in tree order.
1396
+ */
1397
+ function validateGenuiChartSemantics(value) {
1398
+ const errors = [];
1399
+ visitDeclaredGenuiNodes(value, GENUI_LIMITS.maxNodes + 1, (node, at) => {
1400
+ if (node.type !== "chart") return;
1401
+ validateChartNode(node, at, errors);
1402
+ });
1403
+ return errors;
1404
+ }
1405
+ function validateChartData(value, at, errors) {
1406
+ if (!Array.isArray(value)) return;
1407
+ for (let index = 0; index < value.length; index++) {
1408
+ const datum = obj$1(value[index]);
1409
+ const path = `${at}[${index}]`;
1410
+ if (datum === void 0) {
1411
+ errors.push(`${path} must be an object`);
1412
+ continue;
1413
+ }
1414
+ if (typeof datum.label !== "string") errors.push(`${path}.label must be a string`);
1415
+ if (typeof datum.value !== "number" || !Number.isFinite(datum.value)) errors.push(`${path}.value must be a finite number`);
1416
+ if (datum.color !== void 0 && typeof datum.color !== "string") errors.push(`${path}.color must be a string`);
1417
+ }
1418
+ }
1419
+ function validateChartSeries(value, at, errors) {
1420
+ if (!Array.isArray(value)) return;
1421
+ for (let index = 0; index < value.length; index++) {
1422
+ const series = obj$1(value[index]);
1423
+ const path = `${at}[${index}]`;
1424
+ if (series === void 0) {
1425
+ errors.push(`${path} must be an object`);
1426
+ continue;
1427
+ }
1428
+ if (typeof series.label !== "string") errors.push(`${path}.label must be a string`);
1429
+ if (series.color !== void 0 && typeof series.color !== "string") errors.push(`${path}.color must be a string`);
1430
+ if (!Array.isArray(series.data)) errors.push(`${path}.data must be an array`);
1431
+ validateChartData(series.data, `${path}.data`, errors);
1432
+ }
1433
+ }
1434
+ function validateChartNode(v, at, errors) {
1435
+ if (!Array.isArray(v.data) && !Array.isArray(v.series)) errors.push(`${at}: type 'chart' requires data or series (array)`);
1436
+ if (v.variant !== void 0) errors.push(`${at}.variant is unsupported; use kind`);
1437
+ if (v.data !== void 0 && !Array.isArray(v.data)) errors.push(`${at}.data must be an array`);
1438
+ if (v.series !== void 0 && !Array.isArray(v.series)) errors.push(`${at}.series must be an array`);
1439
+ if (v.kind !== void 0 && (typeof v.kind !== "string" || !CHART_KINDS.includes(v.kind))) errors.push(`${at}.kind must be bars, line, or donut`);
1440
+ validateChartData(v.data, `${at}.data`, errors);
1441
+ validateChartSeries(v.series, `${at}.series`, errors);
1442
+ }
1371
1443
  //#endregion
1372
1444
  //#region src/shared/fence-repair.ts
1373
1445
  /**
@@ -1478,6 +1550,83 @@ function completeFenceJson(raw) {
1478
1550
  }
1479
1551
  }
1480
1552
  //#endregion
1553
+ //#region src/plugin/chart-contract.ts
1554
+ /** Plain object helper for raw model-authored specs. */
1555
+ function obj(value) {
1556
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1557
+ }
1558
+ /**
1559
+ * Visit chart nodes through the component-bearing containers understood by
1560
+ * the GenUI guard. This deliberately does not recursively inspect arbitrary
1561
+ * objects (for example `json.value` or `echart.option`), where a nested
1562
+ * `{type:"chart"}` is data rather than a GenUI node.
1563
+ */
1564
+ function visitChartNodes(value, visit) {
1565
+ function walkNode(value, at) {
1566
+ const node = obj(value);
1567
+ if (node === void 0 || typeof node.type !== "string") return;
1568
+ if (node.type === "chart") visit(node, at);
1569
+ if ((node.type === "row" || node.type === "col" || node.type === "grid" || node.type === "card") && Array.isArray(node.items)) {
1570
+ walk(node.items, `${at}.items`);
1571
+ return;
1572
+ }
1573
+ if (node.type === "tabs" && Array.isArray(node.tabs)) {
1574
+ for (let index = 0; index < node.tabs.length; index++) {
1575
+ const tab = obj(node.tabs[index]);
1576
+ if (tab === void 0) continue;
1577
+ const items = tab.items !== void 0 ? tab.items : tab.content;
1578
+ if (Array.isArray(items)) walk(items, `${at}.tabs[${index}].items`);
1579
+ else walkNode(items, `${at}.tabs[${index}].items`);
1580
+ }
1581
+ return;
1582
+ }
1583
+ if (node.type === "accordion" && Array.isArray(node.items)) {
1584
+ for (let index = 0; index < node.items.length; index++) {
1585
+ const item = obj(node.items[index]);
1586
+ if (item === void 0) continue;
1587
+ const items = item.items !== void 0 ? item.items : item.content;
1588
+ if (Array.isArray(items)) walk(items, `${at}.items[${index}].items`);
1589
+ else walkNode(items, `${at}.items[${index}].items`);
1590
+ }
1591
+ return;
1592
+ }
1593
+ if (node.type === "list" && Array.isArray(node.items)) for (let index = 0; index < node.items.length; index++) walkNode(node.items[index], `${at}.items[${index}]`);
1594
+ }
1595
+ function walk(list, path) {
1596
+ if (!Array.isArray(list)) return;
1597
+ for (let index = 0; index < list.length; index++) walkNode(list[index], `${path}[${index}]`);
1598
+ }
1599
+ const root = obj(value);
1600
+ if (root === void 0) return;
1601
+ if (!Array.isArray(root.items) && root.type === "chart") walkNode(root, "spec");
1602
+ else walk(root.items, "items");
1603
+ }
1604
+ /**
1605
+ * Validate the native chart contract all the way to something the renderer
1606
+ * can actually draw. The shared guard still owns field/type validation; this
1607
+ * layer adds renderer-specific invariants that would otherwise produce an
1608
+ * empty line/donut chart or a grouped-bars shell with no points.
1609
+ */
1610
+ function validateRenderableChartSemantics(value) {
1611
+ const errors = validateGenuiChartSemantics(value);
1612
+ visitChartNodes(value, (node, at) => {
1613
+ const kind = node.kind === void 0 ? "bars" : node.kind;
1614
+ if (Array.isArray(node.data) && node.data.length === 0) errors.push(`${at}.data must not be empty`);
1615
+ if (Array.isArray(node.series)) {
1616
+ if (node.series.length === 0) errors.push(`${at}.series must not be empty`);
1617
+ for (let index = 0; index < node.series.length; index++) {
1618
+ const series = obj(node.series[index]);
1619
+ if (series !== void 0 && Array.isArray(series.data) && series.data.length === 0) errors.push(`${at}.series[${index}].data must not be empty`);
1620
+ }
1621
+ }
1622
+ if (kind === "line" || kind === "donut") {
1623
+ if (node.series !== void 0) errors.push(`${at}.series is only supported for bars`);
1624
+ if (node.data === void 0) errors.push(`${at}.data is required for ${kind}`);
1625
+ }
1626
+ });
1627
+ return errors;
1628
+ }
1629
+ //#endregion
1481
1630
  //#region src/plugin/tool.ts
1482
1631
  /**
1483
1632
  * Arguments schema: an open `spec` slot. The schema must NOT reject anything
@@ -1616,7 +1765,10 @@ function createRenderUiTool() {
1616
1765
  }
1617
1766
  },
1618
1767
  async execute(args) {
1619
- const spec = repairGenuiSpec(specOf(args));
1768
+ const input = specOf(args);
1769
+ const chartErrors = validateRenderableChartSemantics(input);
1770
+ if (chartErrors.length > 0) throw new Error("render_ui spec invalid: " + chartErrors.join("; "));
1771
+ const spec = repairGenuiSpec(input);
1620
1772
  if (spec === null) return "render_ui:spec 无效 —— 根对象需要 \"items\" 数组(组件树白名单见系统提示词),请修正后重试。";
1621
1773
  return `已渲染 UI「${spec.title ?? "未命名"}」(${countNodes(spec)} 个组件)。用户现在可以看到这张卡片;组件带 action 时,用户交互会以 [genui-action] 消息发回给你,届时请重新渲染更新后的界面。`;
1622
1774
  },
@@ -1719,6 +1871,11 @@ function bracketDiagnostic(raw) {
1719
1871
  return diffs.length === 0 ? "" : ` 括号计数:${diffs.join(";")}(长表格最易在收尾处错位,如把 ]]}]} 写成 ]}]}]})\n`;
1720
1872
  }
1721
1873
  const COMMON_CAUSES = "常见原因:① 收尾括号错位/缺失({ 与 }、[ 与 ] 数量不相等)② 字符串值内用了半角引号 \"(中文引语请用 “” 或 「」)③ 尾随逗号 ④ 字符串未闭合";
1874
+ /** Return the model-facing chart field failure for one parsed fence body. */
1875
+ function chartValidationFailure(value) {
1876
+ const errors = validateRenderableChartSemantics(value);
1877
+ return errors.length === 0 ? void 0 : `❌ chart 字段验证失败:\n- ${errors.join("\n- ")}`;
1878
+ }
1722
1879
  /** Build the validate_dsh_ui tool definition (registered alongside render_ui). */
1723
1880
  function createValidateDshUiTool() {
1724
1881
  return {
@@ -1746,9 +1903,16 @@ function createValidateDshUiTool() {
1746
1903
  } catch (error) {
1747
1904
  const detail = error instanceof Error ? error.message : String(error);
1748
1905
  const repaired = completeFenceJson(raw);
1749
- if (repaired !== null && repairGenuiSpec(JSON.parse(repaired.text)) !== null) return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
1906
+ if (repaired !== null) {
1907
+ const repairedValue = JSON.parse(repaired.text);
1908
+ const chartFailure = chartValidationFailure(repairedValue);
1909
+ if (chartFailure !== void 0) return chartFailure;
1910
+ if (repairGenuiSpec(repairedValue) !== null) return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 已自动修复 ${repaired.repairs} 处,下面是修复后的 JSON,直接作为围栏正文发出即可(无需再验证):\n\`\`\`\n${repaired.text}\n\`\`\``;
1911
+ }
1750
1912
  return `❌ dsh-ui 围栏 JSON 解析失败:${detail}。\n${bracketDiagnostic(raw)} 自动修复未能恢复(结构损坏),请按错误信息修正后重新调用本工具验证,通过后再发出围栏。\n${COMMON_CAUSES}`;
1751
1913
  }
1914
+ const chartFailure = chartValidationFailure(parsed);
1915
+ if (chartFailure !== void 0) return chartFailure;
1752
1916
  const spec = repairGenuiSpec(parsed);
1753
1917
  if (spec === null) return "❌ 不是合法 GenUI spec:根对象需要 \"items\" 数组,且每个节点 type 必须在白名单内(见系统提示词)。请修正后重新验证。";
1754
1918
  const validCount = countNodes(spec);
@@ -1843,7 +2007,7 @@ The spec is a white-listed component tree rendered inline where the fence sits.
1843
2007
 
1844
2008
  - 布局: text · row · col · grid · card · divider · spacer
1845
2009
  - 展示: badge · stat · progress · list · table · keyvalue · avatar · audio · video · timeline · file-tree · breadcrumb · callout · steps · diff · json · code · copy
1846
- - 图表: chart (bars|line|donut) · echart (preset|option) · plot (函数图)
2010
+ - 图表: chart {"type":"chart","kind":"bars|line|donut","data":[{"label":"...","value":n,"color":"#hex?"}],"series":[...]?}(series 仅 bars;未知扩展字段可通过校验,但原生 chart repair/render 会忽略) · echart (preset|option) · plot (函数图)
1847
2011
  - 交互: button · input · textarea · select · checkbox · switch · slider · radio · submit · quiz · link · tabs · accordion
1848
2012
  - 高级: mermaid (flowchart/sequence/class/gantt/pie/er/state/journey) · diagram (编辑级架构/流程图,27 种 kind) · scene3d (3D WebGL)
1849
2013
 
@@ -1862,6 +2026,51 @@ Rules:
1862
2026
  * @param ctx - cordis context.
1863
2027
  */
1864
2028
  const inject = ["systemPrompt"];
2029
+ const BUNDLED_SKILL_RANK = 600;
2030
+ const BUNDLED_SKILL_PROVIDER = "dsh-genui";
2031
+ const BUNDLED_SKILL_DESCRIPTION = "GenUI 完整组件与字段规范,用于生成 dsh-ui 结构化交互界面。";
2032
+ const BUNDLED_SKILL_INVOCATION = {
2033
+ modelInvocable: true,
2034
+ userInvocable: true
2035
+ };
2036
+ /** Register through the provider path so source=bundled also gets bundled precedence. */
2037
+ function bundledSkillProvider() {
2038
+ const moduleDirectory = dirname(fileURLToPath(new URL(import.meta.url)));
2039
+ const path = basename(moduleDirectory) === "plugin" ? resolve(moduleDirectory, "../../SKILL.md") : resolve(moduleDirectory, "../SKILL.md");
2040
+ const raw = readFileSync(path, "utf8");
2041
+ const end = raw.indexOf("\n---\n", 4);
2042
+ if (!raw.startsWith("---\n") || end < 0) throw new Error("genui SKILL.md has invalid frontmatter");
2043
+ return {
2044
+ name: BUNDLED_SKILL_PROVIDER,
2045
+ list: () => Promise.resolve([{
2046
+ name: "genui",
2047
+ description: BUNDLED_SKILL_DESCRIPTION,
2048
+ invocation: BUNDLED_SKILL_INVOCATION,
2049
+ source: "bundled",
2050
+ provider: BUNDLED_SKILL_PROVIDER,
2051
+ path,
2052
+ resourceBase: {
2053
+ kind: "directory",
2054
+ path: dirname(path)
2055
+ },
2056
+ rank: BUNDLED_SKILL_RANK,
2057
+ locator: path
2058
+ }]),
2059
+ get: () => Promise.resolve({
2060
+ name: "genui",
2061
+ description: BUNDLED_SKILL_DESCRIPTION,
2062
+ invocation: BUNDLED_SKILL_INVOCATION,
2063
+ source: "bundled",
2064
+ provider: BUNDLED_SKILL_PROVIDER,
2065
+ path,
2066
+ resourceBase: {
2067
+ kind: "directory",
2068
+ path: dirname(path)
2069
+ },
2070
+ content: raw.slice(end + 5)
2071
+ })
2072
+ };
2073
+ }
1865
2074
  function apply(ctx) {
1866
2075
  ctx.systemPrompt.section({
1867
2076
  name: "genui:fence",
@@ -1881,6 +2090,9 @@ function apply(ctx) {
1881
2090
  ctx.on("internal/service", (name, value) => {
1882
2091
  if (name === "tools") tryRegister(value);
1883
2092
  });
2093
+ ctx.inject(["skills"], (skillCtx) => {
2094
+ skillCtx.skills.registerProvider(() => bundledSkillProvider());
2095
+ });
1884
2096
  let assetsRegistered = false;
1885
2097
  const tryRegisterAssets = (value) => {
1886
2098
  if (assetsRegistered) return;
@@ -1,7 +1,10 @@
1
1
  import type { GenuiBlockProps } from './blocks/state.ts';
2
2
  export declare const GENUI_ACTION_DEBOUNCE_MS = 300;
3
3
  /**
4
- * Render a GenUI spec as an inline block. Falls back to nothing when the spec
5
- * carries no items (the fence renderer already refused non-specs before us).
4
+ * Render a GenUI spec as an inline block. `stateKey` is also the durable
5
+ * component identity: changing it remounts the stateful implementation so
6
+ * state loaded for one block can never leak into or be saved under another
7
+ * key. Identity-less streaming renders deliberately share one stable
8
+ * volatile instance, preserving local interaction state as the spec grows.
6
9
  */
7
10
  export declare const GenuiBlock: import("react").NamedExoticComponent<GenuiBlockProps>;
@@ -101,4 +101,4 @@ export declare const BreadcrumbNode: import("react").NamedExoticComponent<{
101
101
  * names stay independent. The model round-trip takes seconds, so a few
102
102
  * hundred ms of trailing delay is imperceptible — and it stops bursts of
103
103
  * queued user turns.
104
- */
104
+ */
@@ -0,0 +1,6 @@
1
+ import { type ReactNode } from 'react';
2
+ import type { GenuiImage } from '../spec.ts';
3
+ /** Native image display; source safety is enforced by repairGenuiSpec/safeMediaSrc. */
4
+ export declare function ImageNode({ node }: {
5
+ node: GenuiImage;
6
+ }): ReactNode;
@@ -19,6 +19,8 @@ export interface GenuiBlockProps {
19
19
  * v2.7: durable-state key (session + slot + content fingerprint). When set,
20
20
  * interaction state (radio answers, submit lock, field values) persists to
21
21
  * localStorage and restores on refresh / re-render of the same content.
22
+ * Changing this key also starts a fresh in-memory interaction-state lifetime;
23
+ * callers only provide stateKey — GenuiBlock owns the matching React identity.
22
24
  */
23
25
  stateKey?: string | undefined;
24
26
  }
@@ -39,6 +39,9 @@ export interface GenuiFenceContext {
39
39
  * - Tier-2 completion (missing quotes/brackets): settled renders only —
40
40
  * `context.source` exists exclusively once the message finished, so
41
41
  * streaming halves are never completed early.
42
+ * - Native chart semantics are checked before repair on every candidate so
43
+ * aliases, empty collections, or line/donut series cannot repair into a
44
+ * default/blank chart.
42
45
  */
43
46
  export declare function resolveGenuiSpec(raw: string, context?: GenuiFenceContext): GenuiSpec | null;
44
47
  /**
@@ -1,9 +1,7 @@
1
1
  /**
2
- * GenUI gallery: one spec exercising every white-listed node type, used as
3
- * the canonical full-vocabulary sample (tests render it and assert every
4
- * component family appears; docs and demos reuse it as the "show everything"
5
- * fence).
2
+ * GenUI gallery: canonical broad-coverage sample of built-in node types.
3
+ * Docs, demos, and renderer tests reuse it as the component gallery fence.
6
4
  */
7
5
  import type { GenuiSpec } from './spec.ts';
8
- /** A single spec covering all 40 node types in the vocabulary. */
6
+ /** Canonical gallery spec for the built-in GenUI component vocabulary. */
9
7
  export declare const gallerySpec: GenuiSpec;