@tangle-network/ui 11.16.4 → 11.16.5

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.
@@ -7,7 +7,11 @@ const string = { type: "string" } as const;
7
7
  const boolean = { type: "boolean" } as const;
8
8
  const gap = { enum: ["sm", "md", "lg"] } as const;
9
9
  const primitive = { type: ["string", "number", "boolean", "null"] } as const;
10
- const children = { type: "array", minItems: 1, items: { $ref: "#/$defs/node" } } as const;
10
+ // No minItems here: stack and grid require `children` and a child can itself be
11
+ // a stack, so a nonempty minimum makes a required recursive loop that model
12
+ // tool-schema converters reject (opencode refused render_ui outright). The
13
+ // persistence gate in schema.ts still rejects an empty container.
14
+ const children = { type: "array", items: { $ref: "#/$defs/node" } } as const;
11
15
  const actions = { type: "array", minItems: 1, items: { $ref: "#/$defs/action" } } as const;
12
16
 
13
17
  function node(type: string, fields: Record<string, unknown>, required: string[] = []) {
@@ -24,6 +24,45 @@ const validPage = {
24
24
  ],
25
25
  };
26
26
 
27
+ type JsonSchema = Record<string, unknown>;
28
+
29
+ /**
30
+ * Model tool-schema converters accept a recursive `$ref` only when the loop can
31
+ * terminate: it must pass through an optional property or an array that may be
32
+ * empty. Return the first `$defs` path that recurses through required,
33
+ * nonempty values only.
34
+ */
35
+ function findRequiredRefLoop(defs: Record<string, JsonSchema>): string | null {
36
+ function visit(schema: JsonSchema, path: string, stack: string[]): string | null {
37
+ const ref = typeof schema.$ref === "string" ? schema.$ref.replace("#/$defs/", "") : null;
38
+ if (ref) {
39
+ if (stack.includes(ref)) return path;
40
+ return visit(defs[ref]!, `$defs.${ref}`, [...stack, ref]);
41
+ }
42
+ for (const key of ["oneOf", "anyOf"] as const) {
43
+ const options = schema[key];
44
+ if (Array.isArray(options)) {
45
+ for (const [index, option] of options.entries()) {
46
+ const loop = visit(option as JsonSchema, `${path}.${key}.${index}`, stack);
47
+ if (loop) return loop;
48
+ }
49
+ }
50
+ }
51
+ if (schema.type === "array" && schema.items && Number(schema.minItems ?? 0) > 0) {
52
+ return visit(schema.items as JsonSchema, `${path}.items`, stack);
53
+ }
54
+ const required = Array.isArray(schema.required) ? schema.required as string[] : [];
55
+ const properties = (schema.properties ?? {}) as Record<string, JsonSchema>;
56
+ for (const name of required) {
57
+ if (!properties[name]) continue;
58
+ const loop = visit(properties[name], `${path}.properties.${name}`, stack);
59
+ if (loop) return loop;
60
+ }
61
+ return null;
62
+ }
63
+ return visit({ $ref: "#/$defs/node" }, "$", []);
64
+ }
65
+
27
66
  describe("OpenUI JSON contract", () => {
28
67
  it("advertises every renderer node and accepts nested JSON with primitive cells", () => {
29
68
  expect(OPENUI_NODE_TYPES).toHaveLength(13);
@@ -32,6 +71,12 @@ describe("OpenUI JSON contract", () => {
32
71
  expect(validateOpenUIJsonArtifact([validPage, { type: "text", text: "Afterword" }])).toMatchObject({ ok: true });
33
72
  });
34
73
 
74
+ it("keeps every recursive node path terminable for model tool-schema converters", () => {
75
+ expect(findRequiredRefLoop(OPENUI_NODE_JSON_SCHEMA.$defs as unknown as Record<string, JsonSchema>)).toBeNull();
76
+ expect(validateOpenUIJsonNode({ type: "card", title: "Empty", children: [] }))
77
+ .toMatchObject({ ok: false, issue: { path: "$.children" } });
78
+ });
79
+
35
80
  it("rejects an unsupported top-level or nested section before it can be dropped", () => {
36
81
  expect(validateOpenUIJsonNode({ type: "section", children: [{ type: "text", text: "hidden" }] }))
37
82
  .toMatchObject({ ok: false, issue: { path: "$", message: expect.stringContaining("section") } });
@@ -40,6 +40,11 @@ export function findUnsupportedOpenUINode(value: unknown): OpenUISchemaIssue | n
40
40
  return { path, message: `Unsupported OpenUI node type: ${String(record.type)}.` };
41
41
  }
42
42
  if (record.type === "stack" || record.type === "grid" || record.type === "card") {
43
+ // The model-facing schema cannot express this minimum without a required
44
+ // recursive loop (see schema-data.ts), so the gate enforces it here.
45
+ if (Array.isArray(record.children) && record.children.length === 0) {
46
+ return { path: `${path}.children`, message: "OpenUI containers need at least one child." };
47
+ }
43
48
  if (Array.isArray(record.children)) {
44
49
  for (const [index, child] of record.children.entries()) {
45
50
  const issue = visit(child, `${path}.children[${index}]`, depth + 1);