@btst/stack 2.11.5 → 2.11.6

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.
@@ -23,6 +23,14 @@ interface JSONSchema {
23
23
  $schema?: string;
24
24
  /** Step definitions for multi-step forms (stored in schema meta) */
25
25
  steps?: FormStep[];
26
+ /**
27
+ * Optional root-level field-name → step-index map. This is the format produced
28
+ * by `zodToFormSchema(schema, { steps, stepGroupMap })` and consumed by
29
+ * `SteppedAutoForm`. The visual FormBuilder writes per-property `stepGroup`
30
+ * instead, but we accept both on read so a Zod-seeded form opens with its
31
+ * fields correctly assigned to their steps.
32
+ */
33
+ stepGroupMap?: Record<string, number>;
26
34
  additionalProperties?: boolean;
27
35
  }
28
36
  /**
@@ -23,6 +23,14 @@ interface JSONSchema {
23
23
  $schema?: string;
24
24
  /** Step definitions for multi-step forms (stored in schema meta) */
25
25
  steps?: FormStep[];
26
+ /**
27
+ * Optional root-level field-name → step-index map. This is the format produced
28
+ * by `zodToFormSchema(schema, { steps, stepGroupMap })` and consumed by
29
+ * `SteppedAutoForm`. The visual FormBuilder writes per-property `stepGroup`
30
+ * instead, but we accept both on read so a Zod-seeded form opens with its
31
+ * fields correctly assigned to their steps.
32
+ */
33
+ stepGroupMap?: Record<string, number>;
26
34
  additionalProperties?: boolean;
27
35
  }
28
36
  /**
@@ -23,6 +23,14 @@ interface JSONSchema {
23
23
  $schema?: string;
24
24
  /** Step definitions for multi-step forms (stored in schema meta) */
25
25
  steps?: FormStep[];
26
+ /**
27
+ * Optional root-level field-name → step-index map. This is the format produced
28
+ * by `zodToFormSchema(schema, { steps, stepGroupMap })` and consumed by
29
+ * `SteppedAutoForm`. The visual FormBuilder writes per-property `stepGroup`
30
+ * instead, but we accept both on read so a Zod-seeded form opens with its
31
+ * fields correctly assigned to their steps.
32
+ */
33
+ stepGroupMap?: Record<string, number>;
26
34
  additionalProperties?: boolean;
27
35
  }
28
36
  /**
@@ -49,7 +49,7 @@ function fieldsToJSONSchema(fields, components, steps) {
49
49
  ...hasMultipleSteps ? { steps } : {}
50
50
  };
51
51
  }
52
- function propertiesToFields(properties, requiredSet, components) {
52
+ function propertiesToFields(properties, requiredSet, components, stepGroupMap) {
53
53
  const fields = [];
54
54
  for (const [key, prop] of Object.entries(properties)) {
55
55
  const isRequired = requiredSet.has(key);
@@ -60,9 +60,10 @@ function propertiesToFields(properties, requiredSet, components) {
60
60
  break;
61
61
  }
62
62
  }
63
+ const resolvedStepGroup = prop.stepGroup !== void 0 ? prop.stepGroup : stepGroupMap?.[key];
63
64
  if (field) {
64
- if (prop.stepGroup !== void 0) {
65
- field.stepGroup = prop.stepGroup;
65
+ if (resolvedStepGroup !== void 0) {
66
+ field.stepGroup = resolvedStepGroup;
66
67
  }
67
68
  if (field.type === "object" && prop.properties) {
68
69
  const childRequiredSet = new Set(prop.required || []);
@@ -85,7 +86,7 @@ function propertiesToFields(properties, requiredSet, components) {
85
86
  required: isRequired
86
87
  },
87
88
  // Include stepGroup even for fallback fields
88
- ...prop.stepGroup !== void 0 ? { stepGroup: prop.stepGroup } : {}
89
+ ...resolvedStepGroup !== void 0 ? { stepGroup: resolvedStepGroup } : {}
89
90
  });
90
91
  }
91
92
  }
@@ -96,7 +97,12 @@ function jsonSchemaToFieldsAndSteps(schema, components) {
96
97
  return { fields: [], steps: [] };
97
98
  }
98
99
  const requiredSet = new Set(schema.required || []);
99
- const fields = propertiesToFields(schema.properties, requiredSet, components);
100
+ const fields = propertiesToFields(
101
+ schema.properties,
102
+ requiredSet,
103
+ components,
104
+ schema.stepGroupMap
105
+ );
100
106
  const steps = schema.steps || [];
101
107
  return { fields, steps };
102
108
  }
@@ -47,7 +47,7 @@ function fieldsToJSONSchema(fields, components, steps) {
47
47
  ...hasMultipleSteps ? { steps } : {}
48
48
  };
49
49
  }
50
- function propertiesToFields(properties, requiredSet, components) {
50
+ function propertiesToFields(properties, requiredSet, components, stepGroupMap) {
51
51
  const fields = [];
52
52
  for (const [key, prop] of Object.entries(properties)) {
53
53
  const isRequired = requiredSet.has(key);
@@ -58,9 +58,10 @@ function propertiesToFields(properties, requiredSet, components) {
58
58
  break;
59
59
  }
60
60
  }
61
+ const resolvedStepGroup = prop.stepGroup !== void 0 ? prop.stepGroup : stepGroupMap?.[key];
61
62
  if (field) {
62
- if (prop.stepGroup !== void 0) {
63
- field.stepGroup = prop.stepGroup;
63
+ if (resolvedStepGroup !== void 0) {
64
+ field.stepGroup = resolvedStepGroup;
64
65
  }
65
66
  if (field.type === "object" && prop.properties) {
66
67
  const childRequiredSet = new Set(prop.required || []);
@@ -83,7 +84,7 @@ function propertiesToFields(properties, requiredSet, components) {
83
84
  required: isRequired
84
85
  },
85
86
  // Include stepGroup even for fallback fields
86
- ...prop.stepGroup !== void 0 ? { stepGroup: prop.stepGroup } : {}
87
+ ...resolvedStepGroup !== void 0 ? { stepGroup: resolvedStepGroup } : {}
87
88
  });
88
89
  }
89
90
  }
@@ -94,7 +95,12 @@ function jsonSchemaToFieldsAndSteps(schema, components) {
94
95
  return { fields: [], steps: [] };
95
96
  }
96
97
  const requiredSet = new Set(schema.required || []);
97
- const fields = propertiesToFields(schema.properties, requiredSet, components);
98
+ const fields = propertiesToFields(
99
+ schema.properties,
100
+ requiredSet,
101
+ components,
102
+ schema.stepGroupMap
103
+ );
98
104
  const steps = schema.steps || [];
99
105
  return { fields, steps };
100
106
  }
@@ -1,4 +1,4 @@
1
- export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.Rtcvl8sS.cjs';
1
+ export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.f6MrG7ys.cjs';
2
2
  import { B as BoardWithColumns, S as SerializedBoardWithColumns, C as ColumnWithTasks, a as SerializedColumn, T as Task, b as SerializedTask } from '../../../shared/stack.DJaKVY7v.cjs';
3
3
  import '@btst/stack/plugins/api';
4
4
  import '@btst/db';
@@ -1,4 +1,4 @@
1
- export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.HE_IvqV5.mjs';
1
+ export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.BzKW7ToW.mjs';
2
2
  import { B as BoardWithColumns, S as SerializedBoardWithColumns, C as ColumnWithTasks, a as SerializedColumn, T as Task, b as SerializedTask } from '../../../shared/stack.DJaKVY7v.mjs';
3
3
  import '@btst/stack/plugins/api';
4
4
  import '@btst/db';
@@ -1,4 +1,4 @@
1
- export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.D4Cea8II.js';
1
+ export { B as BoardListResult, C as CreateKanbanTaskInput, i as KANBAN_QUERY_KEYS, b as KanbanApiContext, K as KanbanApiRouter, c as KanbanBackendHooks, a as KanbanRouteKey, e as createKanbanTask, f as findOrCreateKanbanBoard, g as getAllBoards, d as getBoardById, h as getKanbanColumnsByBoardId, k as kanbanBackendPlugin } from '../../../shared/stack.9dFHtvXR.js';
2
2
  import { B as BoardWithColumns, S as SerializedBoardWithColumns, C as ColumnWithTasks, a as SerializedColumn, T as Task, b as SerializedTask } from '../../../shared/stack.DJaKVY7v.js';
3
3
  import '@btst/stack/plugins/api';
4
4
  import '@btst/db';
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.Rtcvl8sS.cjs';
2
+ import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.f6MrG7ys.cjs';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
4
  import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.cjs';
5
5
  import '@btst/stack/plugins/api';
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.HE_IvqV5.mjs';
2
+ import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.BzKW7ToW.mjs';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
4
  import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.mjs';
5
5
  import '@btst/stack/plugins/api';
@@ -1,5 +1,5 @@
1
1
  import * as _tanstack_react_query from '@tanstack/react-query';
2
- import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.D4Cea8II.js';
2
+ import { K as KanbanApiRouter, j as BoardsListDiscriminator } from '../../shared/stack.9dFHtvXR.js';
3
3
  import { createApiClient } from '@btst/stack/plugins/client';
4
4
  import { S as SerializedBoardWithColumns } from '../../shared/stack.DJaKVY7v.js';
5
5
  import '@btst/stack/plugins/api';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@btst/stack",
3
- "version": "2.11.5",
3
+ "version": "2.11.6",
4
4
  "description": "A composable, plugin-based library for building full-stack applications.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Tests for the JSON Schema ↔ FormBuilderField conversion utilities.
3
+ *
4
+ * The form-builder ecosystem accepts two flavours of multi-step encoding:
5
+ * - **Path A**: per-property `stepGroup` on each field (what the visual
6
+ * FormBuilder writes on save — canonical wire format).
7
+ * - **Path B**: root-level `stepGroupMap` (what `zodToFormSchema(schema, {
8
+ * steps, stepGroupMap })` produces and what `SteppedAutoForm` reads).
9
+ *
10
+ * Both must be accepted on read so a Zod-seeded form opens with all fields
11
+ * correctly assigned to their step tabs in the admin canvas. Without Path B
12
+ * support, every field in a Zod-seeded multi-step form collapses onto step 0.
13
+ *
14
+ * @see packages/ui/src/components/form-builder/schema-utils.ts
15
+ * @see packages/ui/src/lib/schema-converter.ts
16
+ */
17
+
18
+ import { describe, it, expect } from "vitest";
19
+ import {
20
+ fieldsToJSONSchema,
21
+ jsonSchemaToFieldsAndSteps,
22
+ } from "@workspace/ui/components/form-builder/schema-utils";
23
+ import { defaultComponents } from "@workspace/ui/components/form-builder";
24
+ import type {
25
+ JSONSchema,
26
+ FormStep,
27
+ } from "@workspace/ui/components/form-builder/types";
28
+
29
+ const STEPS: FormStep[] = [
30
+ { id: "goals", title: "Your goals" },
31
+ { id: "experience", title: "Experience" },
32
+ { id: "constraints", title: "Constraints" },
33
+ ];
34
+
35
+ describe("jsonSchemaToFieldsAndSteps", () => {
36
+ describe("core usage", () => {
37
+ it("returns empty result for null/undefined schema", () => {
38
+ expect(jsonSchemaToFieldsAndSteps(null, defaultComponents)).toEqual({
39
+ fields: [],
40
+ steps: [],
41
+ });
42
+ expect(jsonSchemaToFieldsAndSteps(undefined, defaultComponents)).toEqual({
43
+ fields: [],
44
+ steps: [],
45
+ });
46
+ });
47
+
48
+ it("returns empty result when schema has no properties", () => {
49
+ const schema = { type: "object", properties: {} } as JSONSchema;
50
+ expect(jsonSchemaToFieldsAndSteps(schema, defaultComponents)).toEqual({
51
+ fields: [],
52
+ steps: [],
53
+ });
54
+ });
55
+
56
+ it("parses a flat single-step schema with mixed field types", () => {
57
+ const schema: JSONSchema = {
58
+ type: "object",
59
+ properties: {
60
+ name: { type: "string", label: "Full Name" },
61
+ email: { type: "string", format: "email", label: "Email" },
62
+ age: { type: "number", label: "Age", minimum: 0 },
63
+ },
64
+ required: ["name", "email"],
65
+ };
66
+
67
+ const { fields, steps } = jsonSchemaToFieldsAndSteps(
68
+ schema,
69
+ defaultComponents,
70
+ );
71
+
72
+ expect(steps).toEqual([]);
73
+ expect(fields).toHaveLength(3);
74
+
75
+ const [name, email, age] = fields;
76
+ expect(name).toMatchObject({
77
+ id: "name",
78
+ type: "text",
79
+ props: { label: "Full Name", required: true },
80
+ });
81
+ expect(email).toMatchObject({
82
+ id: "email",
83
+ type: "email",
84
+ props: { label: "Email", required: true },
85
+ });
86
+ expect(age).toMatchObject({
87
+ id: "age",
88
+ type: "number",
89
+ props: { label: "Age", required: false },
90
+ });
91
+ expect(name?.stepGroup).toBeUndefined();
92
+ expect(email?.stepGroup).toBeUndefined();
93
+ expect(age?.stepGroup).toBeUndefined();
94
+ });
95
+
96
+ it("preserves the steps array on the output", () => {
97
+ const schema: JSONSchema = {
98
+ type: "object",
99
+ properties: {
100
+ name: { type: "string", label: "Name", stepGroup: 0 },
101
+ },
102
+ steps: STEPS,
103
+ };
104
+
105
+ const { steps } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
106
+ expect(steps).toEqual(STEPS);
107
+ });
108
+ });
109
+
110
+ describe("multi-step: Path A — per-property stepGroup", () => {
111
+ // Canonical format written by the visual FormBuilder when saving.
112
+ const schema: JSONSchema = {
113
+ type: "object",
114
+ properties: {
115
+ name: { type: "string", label: "Name", stepGroup: 0 },
116
+ email: {
117
+ type: "string",
118
+ format: "email",
119
+ label: "Email",
120
+ stepGroup: 0,
121
+ },
122
+ experience: {
123
+ type: "string",
124
+ enum: ["beginner", "advanced"],
125
+ label: "Experience",
126
+ stepGroup: 1,
127
+ },
128
+ agree: {
129
+ type: "boolean",
130
+ fieldType: "switch",
131
+ label: "I agree",
132
+ stepGroup: 2,
133
+ },
134
+ },
135
+ steps: STEPS,
136
+ };
137
+
138
+ it("assigns each field to the step encoded on its property", () => {
139
+ const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
140
+
141
+ expect(fields).toHaveLength(4);
142
+ const byId = new Map(fields.map((f) => [f.id, f]));
143
+ expect(byId.get("name")?.stepGroup).toBe(0);
144
+ expect(byId.get("email")?.stepGroup).toBe(0);
145
+ expect(byId.get("experience")?.stepGroup).toBe(1);
146
+ expect(byId.get("agree")?.stepGroup).toBe(2);
147
+ });
148
+ });
149
+
150
+ describe("multi-step: Path B — root-level stepGroupMap (regression)", () => {
151
+ // What `zodToFormSchema(LooksmaxQuizSchema, { steps, stepGroupMap })` writes.
152
+ // Before the fix, every field collapsed onto step 0 because the parser
153
+ // only consulted per-property `stepGroup`.
154
+ const schema: JSONSchema = {
155
+ type: "object",
156
+ properties: {
157
+ primaryGoal: {
158
+ type: "string",
159
+ enum: ["skin", "hair", "jaw", "lean", "all"],
160
+ label: "Primary goal",
161
+ fieldType: "radio",
162
+ },
163
+ experience: {
164
+ type: "string",
165
+ enum: ["none", "some-peptides", "full-cycle-history"],
166
+ label: "Experience",
167
+ fieldType: "radio",
168
+ },
169
+ riskTolerance: {
170
+ type: "string",
171
+ enum: ["low", "moderate", "high"],
172
+ label: "Risk tolerance",
173
+ fieldType: "radio",
174
+ },
175
+ noInjections: {
176
+ type: "boolean",
177
+ fieldType: "switch",
178
+ label: "No injections",
179
+ },
180
+ email: {
181
+ type: "string",
182
+ format: "email",
183
+ label: "Email",
184
+ },
185
+ },
186
+ steps: STEPS,
187
+ stepGroupMap: {
188
+ primaryGoal: 0,
189
+ experience: 1,
190
+ riskTolerance: 1,
191
+ noInjections: 2,
192
+ email: 2,
193
+ },
194
+ };
195
+
196
+ it("assigns step from root-level stepGroupMap when properties omit stepGroup", () => {
197
+ const { fields, steps } = jsonSchemaToFieldsAndSteps(
198
+ schema,
199
+ defaultComponents,
200
+ );
201
+
202
+ expect(steps).toEqual(STEPS);
203
+ expect(fields).toHaveLength(5);
204
+
205
+ const byId = new Map(fields.map((f) => [f.id, f]));
206
+ expect(byId.get("primaryGoal")?.stepGroup).toBe(0);
207
+ expect(byId.get("experience")?.stepGroup).toBe(1);
208
+ expect(byId.get("riskTolerance")?.stepGroup).toBe(1);
209
+ expect(byId.get("noInjections")?.stepGroup).toBe(2);
210
+ expect(byId.get("email")?.stepGroup).toBe(2);
211
+ });
212
+
213
+ it("works for fields that fall back to the generic text component", () => {
214
+ // Fields with unknown shapes fall through to the fallback `text` field
215
+ // in propertiesToFields. The fix must apply there too.
216
+ const schemaWithUnknown: JSONSchema = {
217
+ type: "object",
218
+ properties: {
219
+ mystery: {
220
+ // no `type` → no component matches → fallback to text
221
+ label: "Mystery field",
222
+ } as JSONSchema["properties"][string],
223
+ },
224
+ steps: STEPS,
225
+ stepGroupMap: { mystery: 2 },
226
+ };
227
+
228
+ const { fields } = jsonSchemaToFieldsAndSteps(
229
+ schemaWithUnknown,
230
+ defaultComponents,
231
+ );
232
+
233
+ expect(fields).toHaveLength(1);
234
+ expect(fields[0]?.id).toBe("mystery");
235
+ expect(fields[0]?.stepGroup).toBe(2);
236
+ });
237
+ });
238
+
239
+ describe("multi-step: precedence when both encodings are present", () => {
240
+ it("per-property stepGroup wins over stepGroupMap", () => {
241
+ const schema: JSONSchema = {
242
+ type: "object",
243
+ properties: {
244
+ primary: {
245
+ type: "string",
246
+ label: "Primary",
247
+ stepGroup: 0, // per-property says step 0
248
+ },
249
+ secondary: {
250
+ type: "string",
251
+ label: "Secondary",
252
+ stepGroup: 1, // per-property says step 1
253
+ },
254
+ tertiary: {
255
+ type: "string",
256
+ label: "Tertiary",
257
+ // no per-property — should use map
258
+ },
259
+ },
260
+ steps: STEPS,
261
+ stepGroupMap: {
262
+ primary: 99, // map disagrees, but per-property wins
263
+ secondary: 99,
264
+ tertiary: 2, // map applies
265
+ },
266
+ };
267
+
268
+ const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
269
+
270
+ const byId = new Map(fields.map((f) => [f.id, f]));
271
+ expect(byId.get("primary")?.stepGroup).toBe(0);
272
+ expect(byId.get("secondary")?.stepGroup).toBe(1);
273
+ expect(byId.get("tertiary")?.stepGroup).toBe(2);
274
+ });
275
+
276
+ it("missing entries in stepGroupMap leave stepGroup undefined", () => {
277
+ const schema: JSONSchema = {
278
+ type: "object",
279
+ properties: {
280
+ mapped: { type: "string", label: "Mapped" },
281
+ unmapped: { type: "string", label: "Unmapped" },
282
+ },
283
+ steps: STEPS,
284
+ stepGroupMap: { mapped: 1 },
285
+ };
286
+
287
+ const { fields } = jsonSchemaToFieldsAndSteps(schema, defaultComponents);
288
+ const byId = new Map(fields.map((f) => [f.id, f]));
289
+ expect(byId.get("mapped")?.stepGroup).toBe(1);
290
+ expect(byId.get("unmapped")?.stepGroup).toBeUndefined();
291
+ });
292
+ });
293
+ });
294
+
295
+ describe("fieldsToJSONSchema → jsonSchemaToFieldsAndSteps round-trip", () => {
296
+ it("normalises Path B (stepGroupMap) input to Path A (per-property) output", () => {
297
+ // This is the workflow that breaks the original bug: open a Zod-seeded
298
+ // form (Path B), save without changes — the FormBuilder writes Path A.
299
+ const inputSchema: JSONSchema = {
300
+ type: "object",
301
+ properties: {
302
+ goal: { type: "string", label: "Goal" },
303
+ risk: { type: "string", label: "Risk" },
304
+ },
305
+ steps: STEPS.slice(0, 2),
306
+ stepGroupMap: { goal: 0, risk: 1 },
307
+ };
308
+
309
+ const { fields, steps } = jsonSchemaToFieldsAndSteps(
310
+ inputSchema,
311
+ defaultComponents,
312
+ );
313
+
314
+ // Re-serialise with the same steps array
315
+ const roundTripped = fieldsToJSONSchema(fields, defaultComponents, steps);
316
+
317
+ // Output uses Path A (per-property stepGroup) and drops the root-level map
318
+ expect(roundTripped.properties.goal?.stepGroup).toBe(0);
319
+ expect(roundTripped.properties.risk?.stepGroup).toBe(1);
320
+ expect(roundTripped.steps).toEqual(STEPS.slice(0, 2));
321
+ expect(
322
+ (roundTripped as JSONSchema & { stepGroupMap?: unknown }).stepGroupMap,
323
+ ).toBeUndefined();
324
+
325
+ // And re-parsing the normalised output produces the same field shape
326
+ const reparsed = jsonSchemaToFieldsAndSteps(
327
+ roundTripped,
328
+ defaultComponents,
329
+ );
330
+ const byIdOriginal = new Map(fields.map((f) => [f.id, f.stepGroup]));
331
+ const byIdRoundTrip = new Map(
332
+ reparsed.fields.map((f) => [f.id, f.stepGroup]),
333
+ );
334
+ expect(byIdRoundTrip).toEqual(byIdOriginal);
335
+ });
336
+
337
+ it("preserves Path A through a full round-trip", () => {
338
+ const inputSchema: JSONSchema = {
339
+ type: "object",
340
+ properties: {
341
+ name: { type: "string", label: "Name", stepGroup: 0 },
342
+ email: {
343
+ type: "string",
344
+ format: "email",
345
+ label: "Email",
346
+ stepGroup: 1,
347
+ },
348
+ },
349
+ steps: STEPS.slice(0, 2),
350
+ };
351
+
352
+ const { fields, steps } = jsonSchemaToFieldsAndSteps(
353
+ inputSchema,
354
+ defaultComponents,
355
+ );
356
+ const roundTripped = fieldsToJSONSchema(fields, defaultComponents, steps);
357
+
358
+ expect(roundTripped.properties.name?.stepGroup).toBe(0);
359
+ expect(roundTripped.properties.email?.stepGroup).toBe(1);
360
+ expect(roundTripped.steps).toEqual(STEPS.slice(0, 2));
361
+ });
362
+ });
@@ -7,9 +7,9 @@ import { QueryClient } from '@tanstack/react-query';
7
7
 
8
8
  declare const createBoardSchema: z.ZodObject<{
9
9
  description: z.ZodOptional<z.ZodString>;
10
- name: z.ZodString;
11
10
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
11
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
+ name: z.ZodString;
13
13
  slug: z.ZodOptional<z.ZodString>;
14
14
  ownerId: z.ZodOptional<z.ZodString>;
15
15
  organizationId: z.ZodOptional<z.ZodString>;
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
30
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
- order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
31
  boardId: z.ZodString;
32
+ order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
35
35
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -360,17 +360,17 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
360
360
  method: "PUT";
361
361
  body: better_call.StandardSchemaV1<{
362
362
  description?: string | undefined;
363
- name?: string | undefined;
364
363
  createdAt?: unknown;
365
364
  updatedAt?: unknown;
365
+ name?: string | undefined;
366
366
  slug?: string | undefined;
367
367
  ownerId?: string | undefined;
368
368
  organizationId?: string | undefined;
369
369
  }, {
370
370
  description?: string | undefined;
371
- name?: string | undefined;
372
371
  createdAt?: unknown;
373
372
  updatedAt?: unknown;
373
+ name?: string | undefined;
374
374
  slug?: string | undefined;
375
375
  ownerId?: string | undefined;
376
376
  organizationId?: string | undefined;
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
404
404
  title?: string | undefined;
405
405
  createdAt?: unknown;
406
406
  updatedAt?: unknown;
407
- order?: number | undefined;
408
407
  boardId?: string | undefined;
408
+ order?: number | undefined;
409
409
  }, {
410
410
  title?: string | undefined;
411
411
  createdAt?: unknown;
412
412
  updatedAt?: unknown;
413
- order?: number | undefined;
414
413
  boardId?: string | undefined;
414
+ order?: number | undefined;
415
415
  }>;
416
416
  }, Column>;
417
417
  readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
@@ -7,9 +7,9 @@ import { QueryClient } from '@tanstack/react-query';
7
7
 
8
8
  declare const createBoardSchema: z.ZodObject<{
9
9
  description: z.ZodOptional<z.ZodString>;
10
- name: z.ZodString;
11
10
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
11
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
+ name: z.ZodString;
13
13
  slug: z.ZodOptional<z.ZodString>;
14
14
  ownerId: z.ZodOptional<z.ZodString>;
15
15
  organizationId: z.ZodOptional<z.ZodString>;
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
30
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
- order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
31
  boardId: z.ZodString;
32
+ order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
35
35
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -360,17 +360,17 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
360
360
  method: "PUT";
361
361
  body: better_call.StandardSchemaV1<{
362
362
  description?: string | undefined;
363
- name?: string | undefined;
364
363
  createdAt?: unknown;
365
364
  updatedAt?: unknown;
365
+ name?: string | undefined;
366
366
  slug?: string | undefined;
367
367
  ownerId?: string | undefined;
368
368
  organizationId?: string | undefined;
369
369
  }, {
370
370
  description?: string | undefined;
371
- name?: string | undefined;
372
371
  createdAt?: unknown;
373
372
  updatedAt?: unknown;
373
+ name?: string | undefined;
374
374
  slug?: string | undefined;
375
375
  ownerId?: string | undefined;
376
376
  organizationId?: string | undefined;
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
404
404
  title?: string | undefined;
405
405
  createdAt?: unknown;
406
406
  updatedAt?: unknown;
407
- order?: number | undefined;
408
407
  boardId?: string | undefined;
408
+ order?: number | undefined;
409
409
  }, {
410
410
  title?: string | undefined;
411
411
  createdAt?: unknown;
412
412
  updatedAt?: unknown;
413
- order?: number | undefined;
414
413
  boardId?: string | undefined;
414
+ order?: number | undefined;
415
415
  }>;
416
416
  }, Column>;
417
417
  readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {
@@ -7,9 +7,9 @@ import { QueryClient } from '@tanstack/react-query';
7
7
 
8
8
  declare const createBoardSchema: z.ZodObject<{
9
9
  description: z.ZodOptional<z.ZodString>;
10
- name: z.ZodString;
11
10
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
11
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
12
+ name: z.ZodString;
13
13
  slug: z.ZodOptional<z.ZodString>;
14
14
  ownerId: z.ZodOptional<z.ZodString>;
15
15
  organizationId: z.ZodOptional<z.ZodString>;
@@ -28,8 +28,8 @@ declare const createColumnSchema: z.ZodObject<{
28
28
  title: z.ZodString;
29
29
  createdAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
30
30
  updatedAt: z.ZodOptional<z.ZodCoercedDate<unknown>>;
31
- order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
31
  boardId: z.ZodString;
32
+ order: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
33
33
  }, z.core.$strip>;
34
34
  declare const updateColumnSchema: z.ZodObject<{
35
35
  createdAt: z.ZodOptional<z.ZodOptional<z.ZodCoercedDate<unknown>>>;
@@ -360,17 +360,17 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
360
360
  method: "PUT";
361
361
  body: better_call.StandardSchemaV1<{
362
362
  description?: string | undefined;
363
- name?: string | undefined;
364
363
  createdAt?: unknown;
365
364
  updatedAt?: unknown;
365
+ name?: string | undefined;
366
366
  slug?: string | undefined;
367
367
  ownerId?: string | undefined;
368
368
  organizationId?: string | undefined;
369
369
  }, {
370
370
  description?: string | undefined;
371
- name?: string | undefined;
372
371
  createdAt?: unknown;
373
372
  updatedAt?: unknown;
373
+ name?: string | undefined;
374
374
  slug?: string | undefined;
375
375
  ownerId?: string | undefined;
376
376
  organizationId?: string | undefined;
@@ -404,14 +404,14 @@ declare const kanbanBackendPlugin: (hooks?: KanbanBackendHooks) => _btst_stack_p
404
404
  title?: string | undefined;
405
405
  createdAt?: unknown;
406
406
  updatedAt?: unknown;
407
- order?: number | undefined;
408
407
  boardId?: string | undefined;
408
+ order?: number | undefined;
409
409
  }, {
410
410
  title?: string | undefined;
411
411
  createdAt?: unknown;
412
412
  updatedAt?: unknown;
413
- order?: number | undefined;
414
413
  boardId?: string | undefined;
414
+ order?: number | undefined;
415
415
  }>;
416
416
  }, Column>;
417
417
  readonly deleteColumn: better_call.StrictEndpoint<"/columns/:id", {} & {