@formancy/builder-core 0.1.0 → 0.2.0

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/dist/index.d.mts CHANGED
@@ -1,5 +1,122 @@
1
+ import { FieldDef, FormSchema, LayoutNode, LogicRule, SpecVersion, Text } from "@formancy/spec";
1
2
  import { SchemaError } from "@formancy/spec/validate";
2
- import { FieldDef, FormSchema, LogicRule } from "@formancy/spec";
3
+ //#region src/authoring.d.ts
4
+ /**
5
+ * Writing a form from an instruction, and refusing to hand back one that does not work.
6
+ *
7
+ * The answer is **checked, and the model is told what was wrong with it and asked
8
+ * again**: the format has a published JSON Schema and the expressions type-check, so
9
+ * both failures come back as sentences a model can act on — `no such overload: double *
10
+ * int … write 4.0` — rather than as a stack trace. Nothing reaches the editor until it
11
+ * would work.
12
+ *
13
+ * **The model is the host's.** `AskModel` is supplied exactly as `Uploader` is: no
14
+ * vendor, no API key, no network call and no opinion about who pays for tokens.
15
+ */
16
+ /** One turn with whatever model the host has. Text in, text out. */
17
+ type AskModel = (prompt: AuthoringPrompt) => Promise<string>;
18
+ interface AuthoringPrompt {
19
+ /** What the model is, and the rules of the format. Stable across turns. */
20
+ readonly system: string;
21
+ /** The instruction, plus whatever went wrong last time. */
22
+ readonly user: string;
23
+ }
24
+ /**
25
+ * Why an attempt was rejected, in the words the model is given back.
26
+ *
27
+ * `logic` is the engine refusing to open the document at all — a misspelled
28
+ * field, a cycle, a condition that is not certain to be a bool — and
29
+ * `expression` is one it would open whose rule then never does anything.
30
+ */
31
+ interface AuthoringProblem {
32
+ readonly kind: 'not-json' | 'invalid-document' | 'logic' | 'expression';
33
+ readonly detail: string;
34
+ }
35
+ type AuthoringResult = {
36
+ readonly ok: true;
37
+ readonly document: FormSchema;
38
+ readonly attempts: number;
39
+ } | {
40
+ readonly ok: false;
41
+ readonly attempts: number;
42
+ readonly problems: readonly AuthoringProblem[];
43
+ /** The last thing the model said, so a person can see what went wrong. */
44
+ readonly lastAnswer: string;
45
+ };
46
+ interface AuthoringOptions {
47
+ /**
48
+ * How many times to ask. Three by default: the first answer, one correction,
49
+ * and one more for the mistake the correction introduced. Past that a model
50
+ * is usually circling rather than converging, and a person reading four
51
+ * failed attempts is better served by the first error than the fourth.
52
+ */
53
+ readonly attempts?: number;
54
+ /**
55
+ * The document being edited, when there is one. Its presence changes the
56
+ * instruction from "write a form" to "change this form", which is a
57
+ * different request and produces a different answer.
58
+ */
59
+ readonly current?: FormSchema;
60
+ }
61
+ export declare function authorForm(ask: AskModel, instruction: string, options?: AuthoringOptions): Promise<AuthoringResult>;
62
+ //#endregion
63
+ //#region src/layout.d.ts
64
+ /**
65
+ * Navigating an arrangement.
66
+ *
67
+ * A layout node has no key. A field is `contact.email` wherever it sits; a row
68
+ * is "the second thing inside the third thing", and every insert or removal
69
+ * renumbers its neighbours. So a layout address is an index path, and these
70
+ * helpers exist because reading one wrong is silent — you edit a different
71
+ * node and the document stays valid.
72
+ *
73
+ * Kept apart from the field commands rather than folded in beside them,
74
+ * because the two addressing schemes look alike and are not: `['a','b']` is a
75
+ * key path into the model and `[0, 1]` is a position path into one layout, and
76
+ * a function that took either would be a function that could confuse them.
77
+ */
78
+ /** Where a layout node goes: which arrangement, which container, which slot. */
79
+ interface LayoutLocation {
80
+ /** The layout's name. Arrangements are asked for by name, never by index. */
81
+ layout: string;
82
+ /** Index path of the container, root-first. `[]` is the layout itself. */
83
+ parent: readonly number[];
84
+ index: number;
85
+ }
86
+ /** A layout node's address. `path` is never empty — that would be the layout. */
87
+ interface LayoutAddress {
88
+ layout: string;
89
+ path: readonly number[];
90
+ }
91
+ /** The kinds that hold other nodes. A `field` node is a leaf. */
92
+ export declare const LAYOUT_CONTAINER_KINDS: readonly ["section", "row", "column"];
93
+ type LayoutContainerKind = (typeof LAYOUT_CONTAINER_KINDS)[number];
94
+ export declare function isLayoutContainer(node: LayoutNode): node is Extract<LayoutNode, {
95
+ children: LayoutNode[];
96
+ }>;
97
+ /** The named layout's top-level node list, or undefined if there is no such layout. */
98
+ export declare function nodesOfLayout(document: FormSchema, name: string): LayoutNode[] | undefined;
99
+ /**
100
+ * The child list of the container at `parent` — the layout's own list for `[]`.
101
+ *
102
+ * Undefined rather than empty when the path points at a field or at nothing,
103
+ * so a caller cannot insert into a leaf and get an array nobody reads back.
104
+ */
105
+ declare function childrenAt(document: FormSchema, layout: string, parent: readonly number[]): LayoutNode[] | undefined;
106
+ /** The node at `path`, or undefined. An empty path addresses no node. */
107
+ declare function nodeAt(document: FormSchema, layout: string, path: readonly number[]): LayoutNode | undefined;
108
+ /** Every container's index path in one layout, the layout itself first. */
109
+ declare function containerPaths(document: FormSchema, layout: string): number[][];
110
+ /** Whether `maybeAncestor` is `path` itself or encloses it. */
111
+ declare function encloses(maybeAncestor: readonly number[], path: readonly number[]): boolean;
112
+ /**
113
+ * A human name for one layout node, for a tree row and for an announcement.
114
+ *
115
+ * A row has nothing to call itself, so it is named by what it holds: "Row with
116
+ * First name and Last name" is findable in a list of six rows and "Row" is not.
117
+ */
118
+ declare function describeNode(node: LayoutNode, nameOfPath: (path: string) => string): string;
119
+ //#endregion
3
120
  //#region src/session.d.ts
4
121
  /**
5
122
  * The headless document engine under the form builder.
@@ -56,6 +173,18 @@ interface BuilderSession {
56
173
  };
57
174
  /** One notification per accepted command, undo or redo. */
58
175
  subscribe(listener: () => void): () => void;
176
+ /**
177
+ * Take a whole document, as ONE undoable edit.
178
+ *
179
+ * For a change that is not an edit to the document so much as a different
180
+ * document — a form written from an instruction, or a paste into the
181
+ * schema editor. It goes through the same validator every other command
182
+ * does, so a session can never come to hold something invalid, and it lands
183
+ * on the undo stack as a single step: Ctrl+Z after "write me a contact form"
184
+ * puts back what was there before, which is the only behaviour anybody
185
+ * would expect.
186
+ */
187
+ replaceDocument(next: FormSchema): CommandOutcome;
59
188
  insertField(location: Location, def: FieldDef): CommandOutcome;
60
189
  removeField(keyPath: readonly string[]): CommandOutcome;
61
190
  moveField(from: readonly string[], to: Location): CommandOutcome;
@@ -69,8 +198,77 @@ interface BuilderSession {
69
198
  * definition) or an existing field being moved (pass its key path).
70
199
  */
71
200
  validTargets(what: FieldDef | readonly string[]): Location[];
201
+ addLayout(name: string): CommandOutcome;
202
+ removeLayout(name: string): CommandOutcome;
203
+ insertLayoutNode(location: LayoutLocation, node: LayoutNode): CommandOutcome;
204
+ removeLayoutNode(address: LayoutAddress): CommandOutcome;
205
+ /** Replace a container with its children, in order, where it stood. */
206
+ unwrapLayoutNode(address: LayoutAddress): CommandOutcome;
207
+ /**
208
+ * Put several nodes inside a new container, where the earliest of them stood.
209
+ *
210
+ * The inverse of `unwrapLayoutNode`, and what "drop this field beside that
211
+ * one" compiles to. The addresses need NOT be siblings — the field being
212
+ * dragged is usually somewhere else — and the new container's children follow
213
+ * the order the addresses are given in, because the side somebody dropped on
214
+ * is what decides which field ends up on the left.
215
+ *
216
+ * One command rather than a move plus a wrap, so one gesture is one undo. A
217
+ * gesture that takes three presses of undo to reverse is one people stop
218
+ * trusting.
219
+ */
220
+ wrapLayoutNodes(layout: string, addresses: ReadonlyArray<readonly number[]>, container: LayoutNode,
221
+ /**
222
+ * Which of the addresses the new container takes the position of.
223
+ *
224
+ * Defaults to the earliest in document order, which is right when there is
225
+ * no reason to prefer one. A DROP has one: the new row belongs where the
226
+ * thing dropped ON was, not where the thing being dragged came from --
227
+ * otherwise dragging a field out of a row and onto a top-level field nests
228
+ * the new row inside the old one, which is not what anybody aimed at.
229
+ *
230
+ * Separate from the order of `addresses` because the two are independent:
231
+ * dropping on the left makes the dragged node the first child while the
232
+ * position still comes from the target, which is the second address.
233
+ */
234
+ positionOf?: readonly number[]): CommandOutcome;
235
+ moveLayoutNode(from: LayoutAddress, to: LayoutLocation): CommandOutcome;
236
+ setLayoutNodeLabel(address: LayoutAddress, label: Text | undefined): CommandOutcome;
237
+ /**
238
+ * Set any property the format gives a layout node, or remove it with `undefined`.
239
+ *
240
+ * Generic on purpose: the builder's panel is generated from the JSON Schema, so a
241
+ * command per property would have to be remembered every time the format grows one.
242
+ * `span` arrived and had no way to be set at all
243
+ * ([0074](../../../docs/decisions/0074-a-table-child-may-span.md)); so had a table's
244
+ * `columns` and a section's `label`, since the day layouts existed.
245
+ *
246
+ * `kind`, `children` and `path` are refused: the first is what the node IS, the
247
+ * second is structure the tree edits, and the third is which answer a placement
248
+ * places — all three have their own commands that keep the document consistent.
249
+ *
250
+ * Like every command here it is attempted against the validator, so an illegal value
251
+ * leaves the document where it was rather than requiring the panel to know the rules.
252
+ */
253
+ setLayoutNodeProperty(address: LayoutAddress, property: string, value: unknown): CommandOutcome;
254
+ /**
255
+ * Every position a layout node may legally occupy: a new node (pass it) or
256
+ * an existing one being moved (pass its index path).
257
+ */
258
+ validLayoutTargets(layout: string, what: LayoutNode | readonly number[]): LayoutLocation[];
259
+ /** Data paths the named layout does not place. Empty for an unknown layout. */
260
+ unplacedFields(layout: string): string[];
261
+ /**
262
+ * Move the document to a newer spec version.
263
+ *
264
+ * One line of document change, undoable like any other command, and the
265
+ * reason it is a command at all: the builder offers the newer field types
266
+ * only to a document that allows them, so somebody who wants one has to be
267
+ * able to say yes from inside the builder rather than by editing JSON.
268
+ */
269
+ upgradeSpec(to?: SpecVersion): CommandOutcome;
72
270
  }
73
271
  export declare function createBuilderSession(initial: FormSchema): BuilderSession;
74
272
  //#endregion
75
- export type { BuilderSession, CommandOutcome, Location, Refusal };
273
+ export { type AskModel, type AuthoringOptions, type AuthoringProblem, type AuthoringPrompt, type AuthoringResult, type BuilderSession, type CommandOutcome, type LayoutAddress, type LayoutContainerKind, type LayoutLocation, type Location, type Refusal, describeNode as describeLayoutNode, childrenAt as layoutChildrenAt, containerPaths as layoutContainerPaths, encloses as layoutEncloses, nodeAt as layoutNodeAt };
76
274
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/session.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;UA0BiB;EACf;;EAEA;EACA;;KAGU;EAAmB;EAAU,UAAU;IAAe;;UAGjD;EACf;EACA;;UAGe;;EAEf,YAAY;;EAEZ,kBAAkB;;EAElB;EACA;EACA;EACA;EACA;;EAEA;IAAgB;IAAgB,QAAQ;;;EAExC,UAAU;EAEV,YAAY,UAAU,UAAU,KAAK,WAAW;EAChD,YAAY,6BAA6B;EACzC,UAAU,yBAAyB,IAAI,WAAW;EAClD,YAAY,4BAA4B,iBAAiB;EACzD,iBAAiB,4BAA4B,kBAAkB,iBAAiB;EAChF,QAAQ,MAAM,YAAY;EAC1B,WAAW,eAAe,MAAM,YAAY;EAC5C,WAAW,gBAAgB;;;;;EAM3B,aAAa,MAAM,+BAA+B;;wBAKpC,qBAAqB,SAAS,aAAa"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/authoring.ts","../src/layout.ts","../src/session.ts"],"mappings":";;;;;;;;;;;;;;;;KAmBY,YAAY,QAAQ,oBAAoB;UAEnC;;WAEN;;WAEA;;;;;;;;;UAUM;WACN;WACA;;KAGC;WACG;WAAmB,UAAU;WAAqB;;WAElD;WACA;WACA,mBAAmB;;WAEnB;;UAGE;;;;;;;WAON;;;;;;WAMA,UAAU;;wBAKC,WACpB,KAAK,UACL,qBACA,UAAS,mBACR,QAAQ;;;;;;;;;;;;;;;;;;UCrDM;;EAEf;;EAEA;EACA;;;UAIe;EACf;EACA;;;qBAIW;KAED,8BAA8B;wBAE1B,kBACd,MAAM,aACL,QAAQ,QAAQ;EAAc,UAAU;;;wBAQ3B,cAAc,UAAU,YAAY,eAAe;;;;;;;iBAUnD,WACd,UAAU,YACV,gBACA,4BACC;;iBAYa,OACd,UAAU,YACV,gBACA,0BACC;;iBAOa,eAAe,UAAU,YAAY;;iBAkBrC,SAAS,kCAAkC;;;;;;;iBAe3C,aAAa,MAAM,YAAY,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;UCjF3C;EACf;;EAEA;EACA;;KAGU;EAAmB;EAAU,UAAU;IAAe;;UAGjD;EACf;EACA;;UAGe;;EAEf,YAAY;;EAEZ,kBAAkB;;EAElB;EACA;EACA;EACA;EACA;;EAEA;IAAgB;IAAgB,QAAQ;;;EAExC,UAAU;;;;;;;;;;;;EAaV,gBAAgB,MAAM,aAAa;EAEnC,YAAY,UAAU,UAAU,KAAK,WAAW;EAChD,YAAY,6BAA6B;EACzC,UAAU,yBAAyB,IAAI,WAAW;EAClD,YAAY,4BAA4B,iBAAiB;EACzD,iBAAiB,4BAA4B,kBAAkB,iBAAiB;EAChF,QAAQ,MAAM,YAAY;EAC1B,WAAW,eAAe,MAAM,YAAY;EAC5C,WAAW,gBAAgB;;;;;EAM3B,aAAa,MAAM,+BAA+B;EAIlD,UAAU,eAAe;EACzB,aAAa,eAAe;EAC5B,iBAAiB,UAAU,gBAAgB,MAAM,aAAa;EAC9D,iBAAiB,SAAS,gBAAgB;;EAE1C,iBAAiB,SAAS,gBAAgB;;;;;;;;;;;;;;EAc1C,gBACE,gBACA,WAAW,kCACX,WAAW,YAcX;;;;;;;;;;;;;;EAAA,iCACC;EACH,eAAe,MAAM,eAAe,IAAI,iBAAiB;EACzD,mBAAmB,SAAS,eAAe,OAAO,mBAAmB;;;;;;;;;;;;;;;;;EAiBrE,sBACE,SAAS,eACT,kBACA,iBACC;;;;;EAMH,mBAAmB,gBAAgB,MAAM,iCAAiC;;EAG1E,eAAe;;;;;;;;;EAUf,YAAY,KAAK,cAAc;;wBAcjB,qBAAqB,SAAS,aAAa"}
package/dist/index.mjs CHANGED
@@ -1,10 +1,214 @@
1
+ import { CURRENT_SPEC_VERSION, LAYOUT_LEAF_KINDS, authoringBriefing, canonicalize, layoutChildren, unreferencedPaths } from "@formancy/spec";
1
2
  import { validateSchema } from "@formancy/spec/validate";
3
+ import { engineRefusal, expressionProblems } from "@formancy/core";
4
+ //#region src/authoring.ts
5
+ const DEFAULT_ATTEMPTS = 3;
6
+ async function authorForm(ask, instruction, options = {}) {
7
+ const limit = Math.max(1, options.attempts ?? DEFAULT_ATTEMPTS);
8
+ const system = authoringBriefing();
9
+ const problems = [];
10
+ let lastAnswer = "";
11
+ for (let attempt = 1; attempt <= limit; attempt += 1) {
12
+ const answer = await ask({
13
+ system,
14
+ user: userPrompt(instruction, options.current, problems)
15
+ });
16
+ lastAnswer = answer;
17
+ const parsed = readDocument(answer);
18
+ if (parsed === void 0) {
19
+ problems.push({
20
+ kind: "not-json",
21
+ detail: "That was not JSON. Answer with the document alone: no commentary, no code fence."
22
+ });
23
+ continue;
24
+ }
25
+ const validated = validateSchema(parsed);
26
+ if (!validated.valid) {
27
+ problems.push({
28
+ kind: "invalid-document",
29
+ detail: validated.errors.map((error) => `${error.path}: ${error.message}`).join("\n")
30
+ });
31
+ continue;
32
+ }
33
+ const refusal = engineRefusal(parsed);
34
+ if (refusal !== void 0) {
35
+ problems.push({
36
+ kind: "logic",
37
+ detail: refusal
38
+ });
39
+ continue;
40
+ }
41
+ const silent = expressionProblems(parsed);
42
+ if (silent.length > 0) {
43
+ problems.push({
44
+ kind: "expression",
45
+ detail: silent.map((problem) => problem.message).join("\n")
46
+ });
47
+ continue;
48
+ }
49
+ return {
50
+ ok: true,
51
+ document: parsed,
52
+ attempts: attempt
53
+ };
54
+ }
55
+ return {
56
+ ok: false,
57
+ attempts: limit,
58
+ problems,
59
+ lastAnswer
60
+ };
61
+ }
62
+ /**
63
+ * The instruction, and what went wrong last time.
64
+ *
65
+ * Only the most recent problem is repeated. A model given three rounds of
66
+ * accumulated complaints starts fixing the first one again.
67
+ */
68
+ function userPrompt(instruction, current, problems) {
69
+ const parts = [];
70
+ if (current !== void 0) {
71
+ parts.push("Here is the current document. Change it as asked and answer with the whole thing:");
72
+ parts.push(canonicalize(current));
73
+ parts.push("");
74
+ }
75
+ parts.push(instruction);
76
+ const last = problems.at(-1);
77
+ if (last !== void 0) {
78
+ parts.push("");
79
+ parts.push("Your previous answer was rejected. Fix exactly this and answer again:");
80
+ parts.push(last.detail);
81
+ }
82
+ return parts.join("\n");
83
+ }
84
+ /**
85
+ * The document out of whatever the model said.
86
+ *
87
+ * Models put JSON in code fences and add a sentence in front of it however
88
+ * firmly they are told not to, and refusing those answers would spend a turn
89
+ * on formatting rather than on the form. So a fence is stripped and, failing
90
+ * that, the outermost braces are taken. Anything looser would start accepting
91
+ * prose that happens to contain a bracket.
92
+ */
93
+ function readDocument(answer) {
94
+ const candidates = [
95
+ /```(?:json)?\s*([\s\S]*?)```/.exec(answer)?.[1],
96
+ answer,
97
+ betweenBraces(answer)
98
+ ];
99
+ for (const candidate of candidates) {
100
+ if (candidate === void 0) continue;
101
+ try {
102
+ const parsed = JSON.parse(candidate.trim());
103
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
104
+ } catch {}
105
+ }
106
+ }
107
+ function betweenBraces(answer) {
108
+ const start = answer.indexOf("{");
109
+ const end = answer.lastIndexOf("}");
110
+ return start === -1 || end <= start ? void 0 : answer.slice(start, end + 1);
111
+ }
112
+ //#endregion
113
+ //#region src/layout.ts
114
+ /** The kinds that hold other nodes. A `field` node is a leaf. */
115
+ const LAYOUT_CONTAINER_KINDS = [
116
+ "section",
117
+ "row",
118
+ "column"
119
+ ];
120
+ function isLayoutContainer(node) {
121
+ return !LAYOUT_LEAF_KINDS.has(node.kind);
122
+ }
123
+ /** The named layout's top-level node list, or undefined if there is no such layout. */
124
+ function nodesOfLayout(document, name) {
125
+ return document.layouts?.find((layout) => layout.name === name)?.nodes;
126
+ }
127
+ /**
128
+ * The child list of the container at `parent` — the layout's own list for `[]`.
129
+ *
130
+ * Undefined rather than empty when the path points at a field or at nothing,
131
+ * so a caller cannot insert into a leaf and get an array nobody reads back.
132
+ */
133
+ function childrenAt(document, layout, parent) {
134
+ let nodes = nodesOfLayout(document, layout);
135
+ if (nodes === void 0) return void 0;
136
+ for (const step of parent) {
137
+ const node = nodes[step];
138
+ if (node === void 0 || !isLayoutContainer(node)) return void 0;
139
+ nodes = node.children;
140
+ }
141
+ return nodes;
142
+ }
143
+ /** The node at `path`, or undefined. An empty path addresses no node. */
144
+ function nodeAt(document, layout, path) {
145
+ if (path.length === 0) return void 0;
146
+ return childrenAt(document, layout, path.slice(0, -1))?.[path[path.length - 1]];
147
+ }
148
+ /** Every container's index path in one layout, the layout itself first. */
149
+ function containerPaths(document, layout) {
150
+ const nodes = nodesOfLayout(document, layout);
151
+ if (nodes === void 0) return [];
152
+ const paths = [[]];
153
+ const walk = (children, prefix) => {
154
+ for (const [index, node] of children.entries()) {
155
+ if (!isLayoutContainer(node)) continue;
156
+ const here = [...prefix, index];
157
+ paths.push(here);
158
+ walk(node.children, here);
159
+ }
160
+ };
161
+ walk(nodes, []);
162
+ return paths;
163
+ }
164
+ /** Whether `maybeAncestor` is `path` itself or encloses it. */
165
+ function encloses(maybeAncestor, path) {
166
+ if (maybeAncestor.length > path.length) return false;
167
+ return maybeAncestor.every((step, at) => path[at] === step);
168
+ }
169
+ function samePath(a, b) {
170
+ return a.length === b.length && a.every((step, at) => b[at] === step);
171
+ }
172
+ /**
173
+ * A human name for one layout node, for a tree row and for an announcement.
174
+ *
175
+ * A row has nothing to call itself, so it is named by what it holds: "Row with
176
+ * First name and Last name" is findable in a list of six rows and "Row" is not.
177
+ */
178
+ function describeNode(node, nameOfPath) {
179
+ if (node.kind === "field") return nameOfPath(node.path);
180
+ if (node.kind === "qrcode") return `Code for ${nameOfPath(node.path)}`;
181
+ const label = typeof node.label === "string" ? node.label : void 0;
182
+ const kind = node.kind[0].toUpperCase() + node.kind.slice(1);
183
+ if (label !== void 0) return `${kind} “${label}”`;
184
+ const inside = layoutChildren(node).map((child) => shortNameOf(child, nameOfPath));
185
+ if (inside.length === 0) return `Empty ${node.kind}`;
186
+ return `${kind} with ${listOf(inside)}`;
187
+ }
188
+ function shortNameOf(node, nameOfPath) {
189
+ if (node.kind === "field") return nameOfPath(node.path);
190
+ if (node.kind === "qrcode") return `code for ${nameOfPath(node.path)}`;
191
+ const label = typeof node.label === "string" ? node.label : void 0;
192
+ return label === void 0 ? `a ${node.kind}` : `the “${label}” ${node.kind}`;
193
+ }
194
+ function listOf(items) {
195
+ if (items.length === 1) return items[0];
196
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
197
+ return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
198
+ }
199
+ //#endregion
2
200
  //#region src/session.ts
3
201
  const CONTAINER_TYPES = /* @__PURE__ */ new Set([
4
202
  "group",
5
203
  "page",
6
204
  "repeater"
7
205
  ]);
206
+ /** Document order: earlier position first, and a parent before its child. */
207
+ function comparePaths(a, b) {
208
+ const depth = Math.min(a.length, b.length);
209
+ for (let at = 0; at < depth; at += 1) if (a[at] !== b[at]) return a[at] - b[at];
210
+ return a.length - b.length;
211
+ }
8
212
  function createBuilderSession(initial) {
9
213
  const opened = deepFreeze(copy(initial));
10
214
  const verdict = verdictFor(opened);
@@ -88,6 +292,13 @@ function createBuilderSession(initial) {
88
292
  listeners.add(listener);
89
293
  return () => listeners.delete(listener);
90
294
  },
295
+ replaceDocument(next) {
296
+ return attempt((draft) => {
297
+ const mutable = draft;
298
+ for (const key of Object.keys(mutable)) delete mutable[key];
299
+ Object.assign(draft, copy(next));
300
+ });
301
+ },
91
302
  insertField(location, def) {
92
303
  return attempt((draft) => {
93
304
  const container = containerAt(draft, location.parent);
@@ -99,7 +310,9 @@ function createBuilderSession(initial) {
99
310
  return attempt((draft) => {
100
311
  const found = locate(draft, keyPath);
101
312
  if (found === void 0) return refuse("/model/fields", `No field at "${keyPath.join(".")}".`);
313
+ const dataPath = dataPathOf(draft, keyPath);
102
314
  found.siblings.splice(found.index, 1);
315
+ if (dataPath !== void 0) unplaceEverywhere(draft, dataPath);
103
316
  });
104
317
  },
105
318
  moveField(from, to) {
@@ -119,9 +332,12 @@ function createBuilderSession(initial) {
119
332
  if (found === void 0) return refuse("/model/fields", `No field at "${keyPath.join(".")}".`);
120
333
  const field = found.siblings[found.index];
121
334
  const baseline = baselineByCurrentKey.get(field.key) ?? field.key;
335
+ const before = dataPathOf(draft, keyPath);
122
336
  field.key = newKey;
123
337
  if (newKey === baseline) delete field.renamedFrom;
124
338
  else field.renamedFrom = baseline;
339
+ const after = dataPathOf(draft, [...keyPath.slice(0, -1), newKey]);
340
+ if (before !== void 0 && after !== void 0) repathEverywhere(draft, before, after);
125
341
  baselineByCurrentKey.set(newKey, baseline);
126
342
  });
127
343
  },
@@ -154,12 +370,189 @@ function createBuilderSession(initial) {
154
370
  rules.splice(index, 1);
155
371
  });
156
372
  },
373
+ addLayout(name) {
374
+ return attempt((draft) => {
375
+ draft.layouts = draft.layouts ?? [];
376
+ draft.layouts.push({
377
+ name,
378
+ nodes: []
379
+ });
380
+ });
381
+ },
382
+ removeLayout(name) {
383
+ return attempt((draft) => {
384
+ const at = draft.layouts?.findIndex((layout) => layout.name === name) ?? -1;
385
+ if (at === -1 || draft.layouts === void 0) return refuse("/layouts", `No layout called "${name}".`);
386
+ draft.layouts.splice(at, 1);
387
+ if (draft.layouts.length === 0) delete draft.layouts;
388
+ });
389
+ },
390
+ insertLayoutNode(location, node) {
391
+ return attempt((draft) => {
392
+ const container = childrenAt(draft, location.layout, location.parent);
393
+ if (container === void 0) return noSuchContainer(location);
394
+ container.splice(clampIndex(location.index, container.length), 0, copy(node));
395
+ });
396
+ },
397
+ removeLayoutNode(address) {
398
+ return attempt((draft) => {
399
+ const found = locateLayout(draft, address);
400
+ if (found === void 0) return noSuchNode(address);
401
+ found.siblings.splice(found.index, 1);
402
+ });
403
+ },
404
+ unwrapLayoutNode(address) {
405
+ return attempt((draft) => {
406
+ const found = locateLayout(draft, address);
407
+ if (found === void 0) return noSuchNode(address);
408
+ const node = found.siblings[found.index];
409
+ if (!isLayoutContainer(node)) return refuse(layoutPointer(address), "A field node places one field. There is nothing inside it to keep.");
410
+ found.siblings.splice(found.index, 1, ...node.children);
411
+ });
412
+ },
413
+ wrapLayoutNodes(layout, addresses, container, positionOf) {
414
+ if (addresses.length < 2) return refuse(`/layouts/${layout}`, "Wrapping needs at least two nodes. Use insertLayoutNode for one.");
415
+ if (!isLayoutContainer(container)) return refuse(`/layouts/${layout}`, "The wrapper has to be a container. A field node cannot hold anything.");
416
+ for (let outer = 0; outer < addresses.length; outer += 1) for (let inner = 0; inner < addresses.length; inner += 1) {
417
+ if (outer === inner) continue;
418
+ const a = addresses[outer];
419
+ const b = addresses[inner];
420
+ if (samePath(a, b)) return refuse(layoutPointer({
421
+ layout,
422
+ path: a
423
+ }), "That node is listed twice. Each one can only go in once.");
424
+ if (encloses(a, b)) return refuse(layoutPointer({
425
+ layout,
426
+ path: a
427
+ }), "Cannot wrap a container together with something inside it.");
428
+ }
429
+ return attempt((draft) => {
430
+ const taken = [];
431
+ for (const path of addresses) {
432
+ const found = locateLayout(draft, {
433
+ layout,
434
+ path
435
+ });
436
+ if (found === void 0) return noSuchNode({
437
+ layout,
438
+ path
439
+ });
440
+ taken.push(found.siblings[found.index]);
441
+ }
442
+ const anchorPath = positionOf !== void 0 && addresses.some((path) => samePath(path, positionOf)) ? positionOf : [...addresses].sort(comparePaths)[0];
443
+ const home = locateLayout(draft, {
444
+ layout,
445
+ path: anchorPath
446
+ });
447
+ if (home === void 0) return noSuchNode({
448
+ layout,
449
+ path: anchorPath
450
+ });
451
+ const parent = home.siblings;
452
+ const at = home.index;
453
+ for (const path of [...addresses].sort(comparePaths).reverse()) {
454
+ const found = locateLayout(draft, {
455
+ layout,
456
+ path
457
+ });
458
+ if (found === void 0) return noSuchNode({
459
+ layout,
460
+ path
461
+ });
462
+ found.siblings.splice(found.index, 1);
463
+ }
464
+ parent.splice(Math.min(at, parent.length), 0, {
465
+ ...container,
466
+ children: taken
467
+ });
468
+ });
469
+ },
470
+ moveLayoutNode(from, to) {
471
+ if (from.layout !== to.layout) return refuse(layoutPointer(from), `Cannot move between the "${from.layout}" and "${to.layout}" layouts. Remove it from one and place it in the other.`);
472
+ if (encloses(from.path, to.parent)) return refuse(layoutPointer(from), "Cannot move this inside itself or one of its own children.");
473
+ return attempt((draft) => {
474
+ const found = locateLayout(draft, from);
475
+ if (found === void 0) return noSuchNode(from);
476
+ const [moved] = found.siblings.splice(found.index, 1);
477
+ const container = childrenAt(draft, to.layout, shiftAfterLift(to.parent, from.path));
478
+ if (container === void 0) return noSuchContainer(to);
479
+ container.splice(clampIndex(to.index, container.length), 0, moved);
480
+ });
481
+ },
482
+ setLayoutNodeLabel(address, label) {
483
+ return attempt((draft) => {
484
+ const found = locateLayout(draft, address);
485
+ if (found === void 0) return noSuchNode(address);
486
+ const node = found.siblings[found.index];
487
+ if (!isLayoutContainer(node)) return refuse(layoutPointer(address), "A field node takes its name from the field it places.");
488
+ if (label === void 0) delete node.label;
489
+ else node.label = copy(label);
490
+ });
491
+ },
492
+ setLayoutNodeProperty(address, property, value) {
493
+ return attempt((draft) => {
494
+ const found = locateLayout(draft, address);
495
+ if (found === void 0) return noSuchNode(address);
496
+ if (STRUCTURAL_LAYOUT_PROPERTIES.has(property)) return refuse(`${layoutPointer(address)}/${property}`, `"${property}" is not a setting. It is what the node is, or where it sits, and the arrangement's own commands change it.`);
497
+ if (property === "__proto__" || property === "prototype" || property === "constructor") return refuse(`${layoutPointer(address)}/${property}`, `"${property}" is not allowed as a layout node setting name.`);
498
+ const node = found.siblings[found.index];
499
+ if (value === void 0) Reflect.deleteProperty(node, property);
500
+ else Object.defineProperty(node, property, {
501
+ value: copy(value),
502
+ writable: true,
503
+ enumerable: true,
504
+ configurable: true
505
+ });
506
+ });
507
+ },
508
+ validLayoutTargets(layout, what) {
509
+ const movingPath = Array.isArray(what) ? what : void 0;
510
+ const probe = movingPath === void 0 ? what : nodeAt(present, layout, movingPath);
511
+ if (probe === void 0) return [];
512
+ if (nodesOfLayout(present, layout) === void 0) return [];
513
+ const targets = [];
514
+ for (const parent of containerPaths(present, layout)) {
515
+ if (movingPath !== void 0 && encloses(movingPath, parent)) continue;
516
+ const draft = copy(present);
517
+ if (movingPath !== void 0) {
518
+ const found = locateLayout(draft, {
519
+ layout,
520
+ path: movingPath
521
+ });
522
+ if (found === void 0) continue;
523
+ found.siblings.splice(found.index, 1);
524
+ }
525
+ const container = childrenAt(draft, layout, shiftAfterLift(parent, movingPath));
526
+ if (container === void 0) continue;
527
+ container.push(copy(probe));
528
+ if (!verdictFor(draft).valid) continue;
529
+ container.pop();
530
+ for (let index = 0; index <= container.length; index += 1) {
531
+ if (movingPath !== void 0 && sameLayoutPath(parent, movingPath.slice(0, -1)) && index === movingPath[movingPath.length - 1]) continue;
532
+ targets.push({
533
+ layout,
534
+ parent,
535
+ index
536
+ });
537
+ }
538
+ }
539
+ return targets;
540
+ },
541
+ unplacedFields(layout) {
542
+ return unreferencedPaths(present, layout) ?? [];
543
+ },
544
+ upgradeSpec(to = CURRENT_SPEC_VERSION) {
545
+ return attempt((draft) => {
546
+ if (draft.specVersion > to) return refuse("/specVersion", `This form is written against spec ${draft.specVersion} and cannot go back to ${to}: whatever the newer version added has nowhere to go, so it would be data loss rather than a change.`);
547
+ draft.specVersion = to;
548
+ });
549
+ },
157
550
  validTargets(what) {
158
551
  const movingPath = Array.isArray(what) ? what : void 0;
159
552
  const probe = movingPath === void 0 ? what : locate(present, movingPath)?.siblings[locate(present, movingPath).index];
160
553
  if (probe === void 0) return [];
161
554
  const targets = [];
162
- for (const parent of containerPaths(present)) {
555
+ for (const parent of containerPaths$1(present)) {
163
556
  if (movingPath !== void 0 && isPrefix(movingPath, parent)) continue;
164
557
  const draft = copy(present);
165
558
  if (movingPath !== void 0) {
@@ -170,9 +563,10 @@ function createBuilderSession(initial) {
170
563
  const container = containerAt(draft, parent);
171
564
  if (container === void 0) continue;
172
565
  container.push(movingPath === void 0 ? withUniqueKey(copy(probe), draft) : copy(probe));
173
- if (verdictFor(draft).valid) targets.push({
566
+ if (!verdictFor(draft).valid) continue;
567
+ for (let index = 0; index < container.length; index += 1) targets.push({
174
568
  parent,
175
- index: container.length - 1
569
+ index
176
570
  });
177
571
  }
178
572
  return targets;
@@ -205,7 +599,7 @@ function locate(document, keyPath) {
205
599
  }
206
600
  }
207
601
  /** Every container's key path, root first. */
208
- function containerPaths(document) {
602
+ function containerPaths$1(document) {
209
603
  const paths = [[]];
210
604
  const walk = (fields, prefix) => {
211
605
  for (const field of fields) {
@@ -218,6 +612,123 @@ function containerPaths(document) {
218
612
  walk(document.model.fields, []);
219
613
  return paths;
220
614
  }
615
+ /**
616
+ * The data path of a field, which is not its key path.
617
+ *
618
+ * A page contributes no segment — a layout arranges what a person sees, and
619
+ * `page1.email` is not what the engine calls that answer. Undefined when the
620
+ * key path reaches nothing, or passes through something that holds no fields.
621
+ */
622
+ /**
623
+ * Layout node properties that are not settings.
624
+ *
625
+ * `kind` is what the node IS — writing a new one would turn a table into a section
626
+ * without moving its children. `children` is structure, which the tree edits. `path`
627
+ * is which answer a placement places, chosen when the placement is added and governed
628
+ * by the one-place-per-field rule.
629
+ */
630
+ const STRUCTURAL_LAYOUT_PROPERTIES = /* @__PURE__ */ new Set([
631
+ "kind",
632
+ "children",
633
+ "path"
634
+ ]);
635
+ function dataPathOf(document, keyPath) {
636
+ const segments = [];
637
+ let fields = document.model.fields;
638
+ for (const [depth, key] of keyPath.entries()) {
639
+ const field = fields.find((candidate) => candidate.key === key);
640
+ if (field === void 0) return void 0;
641
+ if (field.type !== "page") segments.push(key);
642
+ if (depth === keyPath.length - 1) return segments.join(".");
643
+ fields = field.fields ?? [];
644
+ }
645
+ }
646
+ /** Whether `path` is `prefix` itself or a field inside it. */
647
+ function underPath(path, prefix) {
648
+ return path === prefix || path.startsWith(`${prefix}.`);
649
+ }
650
+ /** Drop every layout node placing `dataPath`, or anything inside it. */
651
+ function unplaceEverywhere(draft, dataPath) {
652
+ for (const layout of draft.layouts ?? []) layout.nodes = pruned(layout.nodes, dataPath);
653
+ }
654
+ function pruned(nodes, dataPath) {
655
+ const kept = [];
656
+ for (const node of nodes) {
657
+ if (node.kind === "field" || node.kind === "qrcode") {
658
+ if (!underPath(node.path, dataPath)) kept.push(node);
659
+ continue;
660
+ }
661
+ kept.push({
662
+ ...node,
663
+ children: pruned(layoutChildren(node), dataPath)
664
+ });
665
+ }
666
+ return kept;
667
+ }
668
+ /** Point every layout node at the new path, including fields inside a group. */
669
+ function repathEverywhere(draft, before, after) {
670
+ const walk = (nodes) => {
671
+ for (const [index, node] of nodes.entries()) {
672
+ if (node.kind === "field" || node.kind === "qrcode") {
673
+ if (underPath(node.path, before)) nodes[index] = {
674
+ ...node,
675
+ path: after + node.path.slice(before.length)
676
+ };
677
+ continue;
678
+ }
679
+ walk(layoutChildren(node));
680
+ }
681
+ };
682
+ for (const layout of draft.layouts ?? []) walk(layout.nodes);
683
+ }
684
+ function locateLayout(document, address) {
685
+ if (address.path.length === 0) return void 0;
686
+ const siblings = childrenAt(document, address.layout, address.path.slice(0, -1));
687
+ if (siblings === void 0) return void 0;
688
+ const index = address.path[address.path.length - 1];
689
+ if (index < 0 || index >= siblings.length) return void 0;
690
+ return {
691
+ siblings,
692
+ index
693
+ };
694
+ }
695
+ function layoutPointer(address) {
696
+ return `/layouts/${address.layout}/nodes/${address.path.join("/")}`;
697
+ }
698
+ function noSuchNode(address) {
699
+ return {
700
+ ok: false,
701
+ path: layoutPointer(address),
702
+ message: `Nothing at that position in the "${address.layout}" layout.`
703
+ };
704
+ }
705
+ function noSuchContainer(location) {
706
+ return {
707
+ ok: false,
708
+ path: `/layouts/${location.layout}/nodes/${location.parent.join("/")}`,
709
+ message: location.parent.length === 0 ? `No layout called "${location.layout}".` : `Nothing at that position in the "${location.layout}" layout holds other nodes.`
710
+ };
711
+ }
712
+ /**
713
+ * A container path, re-read after a node has been lifted out.
714
+ *
715
+ * Both paths index the document as it stood. If the lifted node shared a
716
+ * container with an ancestor of this path and sat before it, that ancestor has
717
+ * shifted up one — and walking the unshifted path now arrives at a different
718
+ * node entirely, which is the silent kind of wrong.
719
+ */
720
+ function shiftAfterLift(parent, lifted) {
721
+ if (lifted === void 0) return [...parent];
722
+ const depth = lifted.length - 1;
723
+ if (parent.length <= depth) return [...parent];
724
+ for (let at = 0; at < depth; at += 1) if (parent[at] !== lifted[at]) return [...parent];
725
+ const shifted = [...parent];
726
+ if (shifted[depth] > lifted[depth]) shifted[depth] = shifted[depth] - 1;
727
+ return shifted;
728
+ }
729
+ function sameLayoutPath(a, b) {
730
+ return a.length === b.length && a.every((step, at) => b[at] === step);
731
+ }
221
732
  function isPrefix(prefix, candidate) {
222
733
  if (prefix.length > candidate.length) return false;
223
734
  return prefix.every((segment, index) => candidate[index] === segment);
@@ -278,6 +789,6 @@ function deepFreeze(value) {
278
789
  return Object.freeze(value);
279
790
  }
280
791
  //#endregion
281
- export { createBuilderSession };
792
+ export { LAYOUT_CONTAINER_KINDS, authorForm, createBuilderSession, describeNode as describeLayoutNode, isLayoutContainer, childrenAt as layoutChildrenAt, containerPaths as layoutContainerPaths, encloses as layoutEncloses, nodeAt as layoutNodeAt, nodesOfLayout };
282
793
 
283
794
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/session.ts"],"sourcesContent":["import type { FieldDef, FormSchema, LogicRule } from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { SchemaError } from '@formancy/spec/validate'\n\n/**\n * The headless document engine under the form builder.\n *\n * Three rules shape everything here.\n *\n * **A session may never hold a document the validator rejects.** Commands are\n * applied to a copy, validated, and only then committed; a command that would\n * produce an invalid document is refused with the validator's own\n * author-facing message and the held document is untouched. That is why\n * `canPublish()` is true by construction rather than by hope — and why legality\n * is decided by *trying* a command rather than by a second implementation of\n * the validator's rules, which would drift from it.\n *\n * **The document is frozen and copied at every boundary.** A builder UI is a\n * projection of this state, and a projection that can reach in and mutate the\n * model is how undo stops working.\n *\n * **Commands, never gestures.** Insert, move, remove, rename, set. WCAG 2.2\n * requires every drag operation to have a keyboard equivalent, so the keyboard\n * path is the API and a drag layer is sugar over it — not the other way round,\n * which is how the keyboard path ends up an afterthought nobody finishes.\n */\nexport interface Refusal {\n ok: false\n /** JSON Pointer into the document, as the validator reports it. */\n path: string\n message: string\n}\n\nexport type CommandOutcome = { ok: true; document: FormSchema } | Refusal\n\n/** Where a field goes: the key path of its container, and a position in it. */\nexport interface Location {\n parent: readonly string[]\n index: number\n}\n\nexport interface BuilderSession {\n /** The current document: frozen, and never the caller's object. */\n document(): FormSchema\n /** A mutable deep copy, safe to hand to anything. */\n exportDocument(): FormSchema\n /** Increments once per accepted command, undo or redo. */\n revision(): number\n canUndo(): boolean\n canRedo(): boolean\n undo(): boolean\n redo(): boolean\n /** The validator's verdict on the held document. Valid by construction. */\n canPublish(): { valid: boolean; errors: SchemaError[] }\n /** One notification per accepted command, undo or redo. */\n subscribe(listener: () => void): () => void\n\n insertField(location: Location, def: FieldDef): CommandOutcome\n removeField(keyPath: readonly string[]): CommandOutcome\n moveField(from: readonly string[], to: Location): CommandOutcome\n renameField(keyPath: readonly string[], newKey: string): CommandOutcome\n setFieldProperty(keyPath: readonly string[], property: string, value: unknown): CommandOutcome\n addRule(rule: LogicRule): CommandOutcome\n updateRule(index: number, rule: LogicRule): CommandOutcome\n removeRule(index: number): CommandOutcome\n\n /**\n * Every location a field may legally land: a palette entry (pass a\n * definition) or an existing field being moved (pass its key path).\n */\n validTargets(what: FieldDef | readonly string[]): Location[]\n}\n\nconst CONTAINER_TYPES = new Set(['group', 'page', 'repeater'])\n\nexport function createBuilderSession(initial: FormSchema): BuilderSession {\n const opened = deepFreeze(copy(initial))\n const verdict = verdictFor(opened)\n if (!verdict.valid) {\n // Opening an invalid document would make every later refusal ambiguous:\n // the caller could not tell whether their command broke the form or merely\n // failed to fix it.\n throw new Error(\n `Cannot open this document in a builder session: ${verdict.errors\n .map((error) => `${error.path} ${error.message}`)\n .join('; ')}`,\n )\n }\n\n /**\n * The keys every field had when the session opened. A rename declares\n * `renamedFrom` against THIS, never against the previous key: chaining\n * would point a migration at an interim key no submission ever used.\n */\n const baselineKeys = new Map<FieldDef, string>()\n indexBaseline(opened.model.fields, baselineKeys)\n\n const past: FormSchema[] = []\n const future: FormSchema[] = []\n let present: FormSchema = opened\n let revision = 0\n const listeners = new Set<() => void>()\n\n /** Identity survives edits by data path, so a renamed field keeps its\n * baseline. Keyed by the path a field had in the baseline document. */\n const baselineByCurrentKey = new Map<string, string>()\n seedBaselineByKey(opened.model.fields, baselineByCurrentKey)\n\n function commit(candidate: FormSchema): CommandOutcome {\n const check = verdictFor(candidate)\n if (!check.valid) {\n const first = check.errors[0]!\n return { ok: false, path: first.path, message: first.message }\n }\n past.push(present)\n // A new command abandons the redo branch: redoing onto a different history\n // would replay an edit against a document it was never made against.\n future.length = 0\n present = deepFreeze(candidate)\n revision += 1\n for (const listener of [...listeners]) listener()\n return { ok: true, document: present }\n }\n\n function refuse(path: string, message: string): Refusal {\n return { ok: false, path, message }\n }\n\n /** Apply `edit` to a working copy and commit if the validator agrees. */\n function attempt(edit: (draft: FormSchema) => Refusal | undefined): CommandOutcome {\n const draft = copy(present)\n const problem = edit(draft)\n if (problem !== undefined) return problem\n return commit(draft)\n }\n\n return {\n document: () => present,\n exportDocument: () => copy(present),\n revision: () => revision,\n canUndo: () => past.length > 0,\n canRedo: () => future.length > 0,\n\n undo() {\n const previous = past.pop()\n if (previous === undefined) return false\n future.push(present)\n present = previous\n revision += 1\n for (const listener of [...listeners]) listener()\n return true\n },\n\n redo() {\n const next = future.pop()\n if (next === undefined) return false\n past.push(present)\n present = next\n revision += 1\n for (const listener of [...listeners]) listener()\n return true\n },\n\n canPublish: () => verdictFor(present),\n\n subscribe(listener) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n insertField(location, def) {\n return attempt((draft) => {\n const container = containerAt(draft, location.parent)\n if (container === undefined) {\n return refuse('/model/fields', `No container at \"${location.parent.join('.')}\".`)\n }\n container.splice(clampIndex(location.index, container.length), 0, copy(def))\n return undefined\n })\n },\n\n removeField(keyPath) {\n return attempt((draft) => {\n const found = locate(draft, keyPath)\n if (found === undefined) return refuse('/model/fields', `No field at \"${keyPath.join('.')}\".`)\n found.siblings.splice(found.index, 1)\n return undefined\n })\n },\n\n moveField(from, to) {\n if (isPrefix(from, to.parent)) {\n return refuse(\n '/model/fields',\n `Cannot move \"${from.join('.')}\" inside itself or one of its own children.`,\n )\n }\n return attempt((draft) => {\n const found = locate(draft, from)\n if (found === undefined) return refuse('/model/fields', `No field at \"${from.join('.')}\".`)\n const [moved] = found.siblings.splice(found.index, 1)\n const container = containerAt(draft, to.parent)\n if (container === undefined) {\n return refuse('/model/fields', `No container at \"${to.parent.join('.')}\".`)\n }\n container.splice(clampIndex(to.index, container.length), 0, moved!)\n return undefined\n })\n },\n\n renameField(keyPath, newKey) {\n return attempt((draft) => {\n const found = locate(draft, keyPath)\n if (found === undefined) return refuse('/model/fields', `No field at \"${keyPath.join('.')}\".`)\n const field = found.siblings[found.index]!\n const baseline = baselineByCurrentKey.get(field.key) ?? field.key\n\n field.key = newKey\n if (newKey === baseline) {\n // Back where it started: there is nothing to migrate, and leaving a\n // renamedFrom pointing at the current key is itself invalid.\n delete field.renamedFrom\n } else {\n field.renamedFrom = baseline\n }\n baselineByCurrentKey.set(newKey, baseline)\n return undefined\n })\n },\n\n setFieldProperty(keyPath, property, value) {\n return attempt((draft) => {\n const found = locate(draft, keyPath)\n if (found === undefined) return refuse('/model/fields', `No field at \"${keyPath.join('.')}\".`)\n const field = found.siblings[found.index]! as unknown as Record<string, unknown>\n if (value === undefined) delete field[property]\n else field[property] = copy(value)\n return undefined\n })\n },\n\n addRule(rule) {\n return attempt((draft) => {\n draft.logic = draft.logic ?? { rules: [] }\n draft.logic.rules.push(copy(rule))\n return undefined\n })\n },\n\n updateRule(index, rule) {\n return attempt((draft) => {\n const rules = draft.logic?.rules\n if (rules === undefined || index < 0 || index >= rules.length) {\n return refuse('/logic/rules', `No rule at index ${index}.`)\n }\n rules[index] = copy(rule)\n return undefined\n })\n },\n\n removeRule(index) {\n return attempt((draft) => {\n const rules = draft.logic?.rules\n if (rules === undefined || index < 0 || index >= rules.length) {\n return refuse('/logic/rules', `No rule at index ${index}.`)\n }\n rules.splice(index, 1)\n return undefined\n })\n },\n\n validTargets(what) {\n const movingPath = Array.isArray(what) ? (what as readonly string[]) : undefined\n const probe: FieldDef =\n movingPath === undefined\n ? (what as FieldDef)\n : (locate(present, movingPath)?.siblings[locate(present, movingPath)!.index] as FieldDef)\n if (probe === undefined) return []\n\n const targets: Location[] = []\n for (const parent of containerPaths(present)) {\n // A container cannot be moved into itself or its own descendants.\n if (movingPath !== undefined && isPrefix(movingPath, parent)) continue\n\n // Legality is decided by TRYING the edit against the validator, so\n // these rules can never drift from the ones publish enforces.\n const draft = copy(present)\n if (movingPath !== undefined) {\n const found = locate(draft, movingPath)\n if (found === undefined) continue\n found.siblings.splice(found.index, 1)\n }\n const container = containerAt(draft, parent)\n if (container === undefined) continue\n container.push(movingPath === undefined ? withUniqueKey(copy(probe), draft) : copy(probe))\n\n if (verdictFor(draft).valid) targets.push({ parent, index: container.length - 1 })\n }\n return targets\n },\n }\n}\n\n// ---------------------------------------------------------------- navigation\n\ninterface Located {\n siblings: FieldDef[]\n index: number\n}\n\n/** The child list of the container at `keyPath` — the root list for []. */\nfunction containerAt(document: FormSchema, keyPath: readonly string[]): FieldDef[] | undefined {\n if (keyPath.length === 0) return document.model.fields\n const found = locate(document, keyPath)\n if (found === undefined) return undefined\n const field = found.siblings[found.index]!\n if (!CONTAINER_TYPES.has(field.type)) return undefined\n field.fields = field.fields ?? []\n return field.fields\n}\n\nfunction locate(document: FormSchema, keyPath: readonly string[]): Located | undefined {\n if (keyPath.length === 0) return undefined\n let siblings: FieldDef[] = document.model.fields\n for (let depth = 0; depth < keyPath.length; depth++) {\n const index = siblings.findIndex((field) => field.key === keyPath[depth])\n if (index === -1) return undefined\n if (depth === keyPath.length - 1) return { siblings, index }\n const next = siblings[index]!.fields\n if (next === undefined) return undefined\n siblings = next\n }\n return undefined\n}\n\n/** Every container's key path, root first. */\nfunction containerPaths(document: FormSchema): string[][] {\n const paths: string[][] = [[]]\n const walk = (fields: readonly FieldDef[], prefix: string[]): void => {\n for (const field of fields) {\n if (!CONTAINER_TYPES.has(field.type)) continue\n const here = [...prefix, field.key]\n paths.push(here)\n walk(field.fields ?? [], here)\n }\n }\n walk(document.model.fields, [])\n return paths\n}\n\nfunction isPrefix(prefix: readonly string[], candidate: readonly string[]): boolean {\n if (prefix.length > candidate.length) return false\n return prefix.every((segment, index) => candidate[index] === segment)\n}\n\nfunction clampIndex(index: number, length: number): number {\n if (!Number.isInteger(index) || index < 0) return 0\n return Math.min(index, length)\n}\n\n/** A palette probe must not fail purely because its placeholder key is taken —\n * validTargets answers \"may this SHAPE go here\", not \"is this key free\". */\nfunction withUniqueKey(def: FieldDef, document: FormSchema): FieldDef {\n const taken = new Set<string>()\n const walk = (fields: readonly FieldDef[]): void => {\n for (const field of fields) {\n taken.add(field.key)\n walk(field.fields ?? [])\n }\n }\n walk(document.model.fields)\n\n let candidate = def.key\n let counter = 1\n while (taken.has(candidate)) candidate = `${def.key}_${counter++}`\n return { ...def, key: candidate }\n}\n\n// ------------------------------------------------------------------ baseline\n\nfunction indexBaseline(fields: readonly FieldDef[], into: Map<FieldDef, string>): void {\n for (const field of fields) {\n into.set(field, field.key)\n indexBaseline(field.fields ?? [], into)\n }\n}\n\nfunction seedBaselineByKey(fields: readonly FieldDef[], into: Map<string, string>): void {\n for (const field of fields) {\n // A field opened with a renamedFrom already declared keeps pointing there:\n // that is the key the collected answers actually live under.\n into.set(field.key, field.renamedFrom ?? field.key)\n seedBaselineByKey(field.fields ?? [], into)\n }\n}\n\n// --------------------------------------------------------------------- utils\n\nfunction verdictFor(document: FormSchema): { valid: boolean; errors: SchemaError[] } {\n const result = validateSchema(document)\n return result.valid ? { valid: true, errors: [] } : { valid: false, errors: result.errors }\n}\n\n/** JSON round-trip: a form document is JSON by definition, and this package\n * runs without structuredClone in its lib on purpose. */\nfunction copy<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (value === null || typeof value !== 'object') return value\n for (const nested of Object.values(value as Record<string, unknown>)) deepFreeze(nested)\n return Object.freeze(value)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["layoutChildrenAt","layoutNodeAt","layoutContainerPaths","containerPaths"],"sources":["../src/authoring.ts","../src/layout.ts","../src/session.ts"],"sourcesContent":["import { authoringBriefing, canonicalize } from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { FormSchema } from '@formancy/spec'\nimport { engineRefusal, expressionProblems } from '@formancy/core'\n\n/**\n * Writing a form from an instruction, and refusing to hand back one that does not work.\n *\n * The answer is **checked, and the model is told what was wrong with it and asked\n * again**: the format has a published JSON Schema and the expressions type-check, so\n * both failures come back as sentences a model can act on — `no such overload: double *\n * int … write 4.0` — rather than as a stack trace. Nothing reaches the editor until it\n * would work.\n *\n * **The model is the host's.** `AskModel` is supplied exactly as `Uploader` is: no\n * vendor, no API key, no network call and no opinion about who pays for tokens.\n */\n\n/** One turn with whatever model the host has. Text in, text out. */\nexport type AskModel = (prompt: AuthoringPrompt) => Promise<string>\n\nexport interface AuthoringPrompt {\n /** What the model is, and the rules of the format. Stable across turns. */\n readonly system: string\n /** The instruction, plus whatever went wrong last time. */\n readonly user: string\n}\n\n/**\n * Why an attempt was rejected, in the words the model is given back.\n *\n * `logic` is the engine refusing to open the document at all — a misspelled\n * field, a cycle, a condition that is not certain to be a bool — and\n * `expression` is one it would open whose rule then never does anything.\n */\nexport interface AuthoringProblem {\n readonly kind: 'not-json' | 'invalid-document' | 'logic' | 'expression'\n readonly detail: string\n}\n\nexport type AuthoringResult =\n | { readonly ok: true; readonly document: FormSchema; readonly attempts: number }\n | {\n readonly ok: false\n readonly attempts: number\n readonly problems: readonly AuthoringProblem[]\n /** The last thing the model said, so a person can see what went wrong. */\n readonly lastAnswer: string\n }\n\nexport interface AuthoringOptions {\n /**\n * How many times to ask. Three by default: the first answer, one correction,\n * and one more for the mistake the correction introduced. Past that a model\n * is usually circling rather than converging, and a person reading four\n * failed attempts is better served by the first error than the fourth.\n */\n readonly attempts?: number\n /**\n * The document being edited, when there is one. Its presence changes the\n * instruction from \"write a form\" to \"change this form\", which is a\n * different request and produces a different answer.\n */\n readonly current?: FormSchema\n}\n\nconst DEFAULT_ATTEMPTS = 3\n\nexport async function authorForm(\n ask: AskModel,\n instruction: string,\n options: AuthoringOptions = {},\n): Promise<AuthoringResult> {\n const limit = Math.max(1, options.attempts ?? DEFAULT_ATTEMPTS)\n const system = authoringBriefing()\n const problems: AuthoringProblem[] = []\n let lastAnswer = ''\n\n for (let attempt = 1; attempt <= limit; attempt += 1) {\n const answer = await ask({ system, user: userPrompt(instruction, options.current, problems) })\n lastAnswer = answer\n\n const parsed = readDocument(answer)\n if (parsed === undefined) {\n problems.push({\n kind: 'not-json',\n detail: 'That was not JSON. Answer with the document alone: no commentary, no code fence.',\n })\n continue\n }\n\n const validated = validateSchema(parsed)\n if (!validated.valid) {\n problems.push({\n kind: 'invalid-document',\n detail: validated.errors.map((error) => `${error.path}: ${error.message}`).join('\\n'),\n })\n continue\n }\n\n // The engine's own compile, which is what the preview and the server run.\n // Without it a document with one misspelled field name passed this loop,\n // landed in the editor, and the preview could not open it.\n const refusal = engineRefusal(parsed as FormSchema)\n if (refusal !== undefined) {\n problems.push({ kind: 'logic', detail: refusal })\n continue\n }\n\n const silent = expressionProblems(parsed as FormSchema)\n if (silent.length > 0) {\n // The failure worth the whole loop. These compile and then evaluate to\n // nothing, so the form would publish cleanly and quietly do nothing.\n problems.push({\n kind: 'expression',\n detail: silent.map((problem) => problem.message).join('\\n'),\n })\n continue\n }\n\n return { ok: true, document: parsed as FormSchema, attempts: attempt }\n }\n\n return { ok: false, attempts: limit, problems, lastAnswer }\n}\n\n/**\n * The instruction, and what went wrong last time.\n *\n * Only the most recent problem is repeated. A model given three rounds of\n * accumulated complaints starts fixing the first one again.\n */\nfunction userPrompt(\n instruction: string,\n current: FormSchema | undefined,\n problems: readonly AuthoringProblem[],\n): string {\n const parts: string[] = []\n\n if (current !== undefined) {\n parts.push('Here is the current document. Change it as asked and answer with the whole thing:')\n // Canonical, so the model is not distracted by key order that means\n // nothing, and two identical documents look identical to it.\n parts.push(canonicalize(current))\n parts.push('')\n }\n\n parts.push(instruction)\n\n const last = problems.at(-1)\n if (last !== undefined) {\n parts.push('')\n parts.push('Your previous answer was rejected. Fix exactly this and answer again:')\n parts.push(last.detail)\n }\n\n return parts.join('\\n')\n}\n\n/**\n * The document out of whatever the model said.\n *\n * Models put JSON in code fences and add a sentence in front of it however\n * firmly they are told not to, and refusing those answers would spend a turn\n * on formatting rather than on the form. So a fence is stripped and, failing\n * that, the outermost braces are taken. Anything looser would start accepting\n * prose that happens to contain a bracket.\n */\nfunction readDocument(answer: string): unknown {\n const fenced = /```(?:json)?\\s*([\\s\\S]*?)```/.exec(answer)\n const candidates = [fenced?.[1], answer, betweenBraces(answer)]\n\n for (const candidate of candidates) {\n if (candidate === undefined) continue\n try {\n const parsed: unknown = JSON.parse(candidate.trim())\n // A bare string or number is valid JSON and is not a document; letting\n // one through would report it as an invalid document rather than as an\n // answer that was not one.\n if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed\n } catch {\n // Try the next reading.\n }\n }\n return undefined\n}\n\nfunction betweenBraces(answer: string): string | undefined {\n const start = answer.indexOf('{')\n const end = answer.lastIndexOf('}')\n return start === -1 || end <= start ? undefined : answer.slice(start, end + 1)\n}\n","import { LAYOUT_LEAF_KINDS, layoutChildren } from '@formancy/spec'\nimport type { FormSchema, LayoutNode } from '@formancy/spec'\n\n/**\n * Navigating an arrangement.\n *\n * A layout node has no key. A field is `contact.email` wherever it sits; a row\n * is \"the second thing inside the third thing\", and every insert or removal\n * renumbers its neighbours. So a layout address is an index path, and these\n * helpers exist because reading one wrong is silent — you edit a different\n * node and the document stays valid.\n *\n * Kept apart from the field commands rather than folded in beside them,\n * because the two addressing schemes look alike and are not: `['a','b']` is a\n * key path into the model and `[0, 1]` is a position path into one layout, and\n * a function that took either would be a function that could confuse them.\n */\n\n/** Where a layout node goes: which arrangement, which container, which slot. */\nexport interface LayoutLocation {\n /** The layout's name. Arrangements are asked for by name, never by index. */\n layout: string\n /** Index path of the container, root-first. `[]` is the layout itself. */\n parent: readonly number[]\n index: number\n}\n\n/** A layout node's address. `path` is never empty — that would be the layout. */\nexport interface LayoutAddress {\n layout: string\n path: readonly number[]\n}\n\n/** The kinds that hold other nodes. A `field` node is a leaf. */\nexport const LAYOUT_CONTAINER_KINDS = ['section', 'row', 'column'] as const\n\nexport type LayoutContainerKind = (typeof LAYOUT_CONTAINER_KINDS)[number]\n\nexport function isLayoutContainer(\n node: LayoutNode,\n): node is Extract<LayoutNode, { children: LayoutNode[] }> {\n // `!LAYOUT_LEAF_KINDS.has(kind)`, not `kind !== 'field'`. The old spelling was true\n // while `field` was the only childless node, and `qrcode` is the second — so it\n // claimed a code has children and every caller of this predicate believed it.\n return !LAYOUT_LEAF_KINDS.has(node.kind)\n}\n\n/** The named layout's top-level node list, or undefined if there is no such layout. */\nexport function nodesOfLayout(document: FormSchema, name: string): LayoutNode[] | undefined {\n return document.layouts?.find((layout) => layout.name === name)?.nodes\n}\n\n/**\n * The child list of the container at `parent` — the layout's own list for `[]`.\n *\n * Undefined rather than empty when the path points at a field or at nothing,\n * so a caller cannot insert into a leaf and get an array nobody reads back.\n */\nexport function childrenAt(\n document: FormSchema,\n layout: string,\n parent: readonly number[],\n): LayoutNode[] | undefined {\n let nodes: LayoutNode[] | undefined = nodesOfLayout(document, layout)\n if (nodes === undefined) return undefined\n for (const step of parent) {\n const node: LayoutNode | undefined = nodes[step]\n if (node === undefined || !isLayoutContainer(node)) return undefined\n nodes = node.children\n }\n return nodes\n}\n\n/** The node at `path`, or undefined. An empty path addresses no node. */\nexport function nodeAt(\n document: FormSchema,\n layout: string,\n path: readonly number[],\n): LayoutNode | undefined {\n if (path.length === 0) return undefined\n const siblings = childrenAt(document, layout, path.slice(0, -1))\n return siblings?.[path[path.length - 1]!]\n}\n\n/** Every container's index path in one layout, the layout itself first. */\nexport function containerPaths(document: FormSchema, layout: string): number[][] {\n const nodes = nodesOfLayout(document, layout)\n if (nodes === undefined) return []\n\n const paths: number[][] = [[]]\n const walk = (children: readonly LayoutNode[], prefix: number[]): void => {\n for (const [index, node] of children.entries()) {\n if (!isLayoutContainer(node)) continue\n const here = [...prefix, index]\n paths.push(here)\n walk(node.children, here)\n }\n }\n walk(nodes, [])\n return paths\n}\n\n/** Whether `maybeAncestor` is `path` itself or encloses it. */\nexport function encloses(maybeAncestor: readonly number[], path: readonly number[]): boolean {\n if (maybeAncestor.length > path.length) return false\n return maybeAncestor.every((step, at) => path[at] === step)\n}\n\nexport function samePath(a: readonly number[], b: readonly number[]): boolean {\n return a.length === b.length && a.every((step, at) => b[at] === step)\n}\n\n/**\n * A human name for one layout node, for a tree row and for an announcement.\n *\n * A row has nothing to call itself, so it is named by what it holds: \"Row with\n * First name and Last name\" is findable in a list of six rows and \"Row\" is not.\n */\nexport function describeNode(node: LayoutNode, nameOfPath: (path: string) => string): string {\n if (node.kind === 'field') return nameOfPath(node.path)\n // A code is named by what it encodes, not by being a code: \"Code for Booking\n // reference\" is findable in a tree and \"Code\" is not — the same reasoning a row is\n // named by what it holds.\n if (node.kind === 'qrcode') return `Code for ${nameOfPath(node.path)}`\n\n const label = typeof node.label === 'string' ? node.label : undefined\n const kind = node.kind[0]!.toUpperCase() + node.kind.slice(1)\n if (label !== undefined) return `${kind} “${label}”`\n\n // One level down, and no further. Recursing all the way produced \"Section\n // with Row with First name and Last name and Email\", where the reader has\n // no way to tell which \"and\" separates what. A nested container is named as\n // what it is; its own row in the tree says what is in it.\n const inside = layoutChildren(node).map((child) => shortNameOf(child, nameOfPath))\n if (inside.length === 0) return `Empty ${node.kind}`\n return `${kind} with ${listOf(inside)}`\n}\n\nfunction shortNameOf(node: LayoutNode, nameOfPath: (path: string) => string): string {\n if (node.kind === 'field') return nameOfPath(node.path)\n if (node.kind === 'qrcode') return `code for ${nameOfPath(node.path)}`\n const label = typeof node.label === 'string' ? node.label : undefined\n return label === undefined ? `a ${node.kind}` : `the “${label}” ${node.kind}`\n}\n\nfunction listOf(items: readonly string[]): string {\n if (items.length === 1) return items[0]!\n if (items.length === 2) return `${items[0]!} and ${items[1]!}`\n return `${items.slice(0, -1).join(', ')} and ${items[items.length - 1]!}`\n}\n","import type { FieldDef, FormSchema, LayoutNode, LogicRule, SpecVersion, Text } from '@formancy/spec'\nimport { CURRENT_SPEC_VERSION, unreferencedPaths, LAYOUT_LEAF_KINDS, layoutChildren} from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { SchemaError } from '@formancy/spec/validate'\nimport {\n childrenAt as layoutChildrenAt,\n containerPaths as layoutContainerPaths,\n encloses,\n isLayoutContainer,\n nodeAt as layoutNodeAt,\n nodesOfLayout,\n samePath,\n} from './layout.js'\nimport type { LayoutAddress, LayoutLocation } from './layout.js'\n\n/**\n * The headless document engine under the form builder.\n *\n * Three rules shape everything here.\n *\n * **A session may never hold a document the validator rejects.** Commands are\n * applied to a copy, validated, and only then committed; a command that would\n * produce an invalid document is refused with the validator's own\n * author-facing message and the held document is untouched. That is why\n * `canPublish()` is true by construction rather than by hope — and why legality\n * is decided by *trying* a command rather than by a second implementation of\n * the validator's rules, which would drift from it.\n *\n * **The document is frozen and copied at every boundary.** A builder UI is a\n * projection of this state, and a projection that can reach in and mutate the\n * model is how undo stops working.\n *\n * **Commands, never gestures.** Insert, move, remove, rename, set. WCAG 2.2\n * requires every drag operation to have a keyboard equivalent, so the keyboard\n * path is the API and a drag layer is sugar over it — not the other way round,\n * which is how the keyboard path ends up an afterthought nobody finishes.\n */\nexport interface Refusal {\n ok: false\n /** JSON Pointer into the document, as the validator reports it. */\n path: string\n message: string\n}\n\nexport type CommandOutcome = { ok: true; document: FormSchema } | Refusal\n\n/** Where a field goes: the key path of its container, and a position in it. */\nexport interface Location {\n parent: readonly string[]\n index: number\n}\n\nexport interface BuilderSession {\n /** The current document: frozen, and never the caller's object. */\n document(): FormSchema\n /** A mutable deep copy, safe to hand to anything. */\n exportDocument(): FormSchema\n /** Increments once per accepted command, undo or redo. */\n revision(): number\n canUndo(): boolean\n canRedo(): boolean\n undo(): boolean\n redo(): boolean\n /** The validator's verdict on the held document. Valid by construction. */\n canPublish(): { valid: boolean; errors: SchemaError[] }\n /** One notification per accepted command, undo or redo. */\n subscribe(listener: () => void): () => void\n\n /**\n * Take a whole document, as ONE undoable edit.\n *\n * For a change that is not an edit to the document so much as a different\n * document — a form written from an instruction, or a paste into the\n * schema editor. It goes through the same validator every other command\n * does, so a session can never come to hold something invalid, and it lands\n * on the undo stack as a single step: Ctrl+Z after \"write me a contact form\"\n * puts back what was there before, which is the only behaviour anybody\n * would expect.\n */\n replaceDocument(next: FormSchema): CommandOutcome\n\n insertField(location: Location, def: FieldDef): CommandOutcome\n removeField(keyPath: readonly string[]): CommandOutcome\n moveField(from: readonly string[], to: Location): CommandOutcome\n renameField(keyPath: readonly string[], newKey: string): CommandOutcome\n setFieldProperty(keyPath: readonly string[], property: string, value: unknown): CommandOutcome\n addRule(rule: LogicRule): CommandOutcome\n updateRule(index: number, rule: LogicRule): CommandOutcome\n removeRule(index: number): CommandOutcome\n\n /**\n * Every location a field may legally land: a palette entry (pass a\n * definition) or an existing field being moved (pass its key path).\n */\n validTargets(what: FieldDef | readonly string[]): Location[]\n\n // --- the arrangement, which is a second document over the same model ---\n\n addLayout(name: string): CommandOutcome\n removeLayout(name: string): CommandOutcome\n insertLayoutNode(location: LayoutLocation, node: LayoutNode): CommandOutcome\n removeLayoutNode(address: LayoutAddress): CommandOutcome\n /** Replace a container with its children, in order, where it stood. */\n unwrapLayoutNode(address: LayoutAddress): CommandOutcome\n /**\n * Put several nodes inside a new container, where the earliest of them stood.\n *\n * The inverse of `unwrapLayoutNode`, and what \"drop this field beside that\n * one\" compiles to. The addresses need NOT be siblings — the field being\n * dragged is usually somewhere else — and the new container's children follow\n * the order the addresses are given in, because the side somebody dropped on\n * is what decides which field ends up on the left.\n *\n * One command rather than a move plus a wrap, so one gesture is one undo. A\n * gesture that takes three presses of undo to reverse is one people stop\n * trusting.\n */\n wrapLayoutNodes(\n layout: string,\n addresses: ReadonlyArray<readonly number[]>,\n container: LayoutNode,\n /**\n * Which of the addresses the new container takes the position of.\n *\n * Defaults to the earliest in document order, which is right when there is\n * no reason to prefer one. A DROP has one: the new row belongs where the\n * thing dropped ON was, not where the thing being dragged came from --\n * otherwise dragging a field out of a row and onto a top-level field nests\n * the new row inside the old one, which is not what anybody aimed at.\n *\n * Separate from the order of `addresses` because the two are independent:\n * dropping on the left makes the dragged node the first child while the\n * position still comes from the target, which is the second address.\n */\n positionOf?: readonly number[],\n ): CommandOutcome\n moveLayoutNode(from: LayoutAddress, to: LayoutLocation): CommandOutcome\n setLayoutNodeLabel(address: LayoutAddress, label: Text | undefined): CommandOutcome\n /**\n * Set any property the format gives a layout node, or remove it with `undefined`.\n *\n * Generic on purpose: the builder's panel is generated from the JSON Schema, so a\n * command per property would have to be remembered every time the format grows one.\n * `span` arrived and had no way to be set at all\n * ([0074](../../../docs/decisions/0074-a-table-child-may-span.md)); so had a table's\n * `columns` and a section's `label`, since the day layouts existed.\n *\n * `kind`, `children` and `path` are refused: the first is what the node IS, the\n * second is structure the tree edits, and the third is which answer a placement\n * places — all three have their own commands that keep the document consistent.\n *\n * Like every command here it is attempted against the validator, so an illegal value\n * leaves the document where it was rather than requiring the panel to know the rules.\n */\n setLayoutNodeProperty(\n address: LayoutAddress,\n property: string,\n value: unknown,\n ): CommandOutcome\n\n /**\n * Every position a layout node may legally occupy: a new node (pass it) or\n * an existing one being moved (pass its index path).\n */\n validLayoutTargets(layout: string, what: LayoutNode | readonly number[]): LayoutLocation[]\n\n /** Data paths the named layout does not place. Empty for an unknown layout. */\n unplacedFields(layout: string): string[]\n\n /**\n * Move the document to a newer spec version.\n *\n * One line of document change, undoable like any other command, and the\n * reason it is a command at all: the builder offers the newer field types\n * only to a document that allows them, so somebody who wants one has to be\n * able to say yes from inside the builder rather than by editing JSON.\n */\n upgradeSpec(to?: SpecVersion): CommandOutcome\n}\n\nconst CONTAINER_TYPES = new Set(['group', 'page', 'repeater'])\n\n/** Document order: earlier position first, and a parent before its child. */\nfunction comparePaths(a: readonly number[], b: readonly number[]): number {\n const depth = Math.min(a.length, b.length)\n for (let at = 0; at < depth; at += 1) {\n if (a[at] !== b[at]) return a[at]! - b[at]!\n }\n return a.length - b.length\n}\n\nexport function createBuilderSession(initial: FormSchema): BuilderSession {\n const opened = deepFreeze(copy(initial))\n const verdict = verdictFor(opened)\n if (!verdict.valid) {\n // Opening an invalid document would make every later refusal ambiguous:\n // the caller could not tell whether their command broke the form or merely\n // failed to fix it.\n throw new Error(\n `Cannot open this document in a builder session: ${verdict.errors\n .map((error) => `${error.path} ${error.message}`)\n .join('; ')}`,\n )\n }\n\n /**\n * The keys every field had when the session opened. A rename declares\n * `renamedFrom` against THIS, never against the previous key: chaining\n * would point a migration at an interim key no submission ever used.\n */\n const baselineKeys = new Map<FieldDef, string>()\n indexBaseline(opened.model.fields, baselineKeys)\n\n const past: FormSchema[] = []\n const future: FormSchema[] = []\n let present: FormSchema = opened\n let revision = 0\n const listeners = new Set<() => void>()\n\n /** Identity survives edits by data path, so a renamed field keeps its\n * baseline. Keyed by the path a field had in the baseline document. */\n const baselineByCurrentKey = new Map<string, string>()\n seedBaselineByKey(opened.model.fields, baselineByCurrentKey)\n\n function commit(candidate: FormSchema): CommandOutcome {\n const check = verdictFor(candidate)\n if (!check.valid) {\n const first = check.errors[0]!\n return { ok: false, path: first.path, message: first.message }\n }\n past.push(present)\n // A new command abandons the redo branch: redoing onto a different history\n // would replay an edit against a document it was never made against.\n future.length = 0\n present = deepFreeze(candidate)\n revision += 1\n for (const listener of [...listeners]) listener()\n return { ok: true, document: present }\n }\n\n function refuse(path: string, message: string): Refusal {\n return { ok: false, path, message }\n }\n\n /** Apply `edit` to a working copy and commit if the validator agrees. */\n function attempt(edit: (draft: FormSchema) => Refusal | undefined): CommandOutcome {\n const draft = copy(present)\n const problem = edit(draft)\n if (problem !== undefined) return problem\n return commit(draft)\n }\n\n return {\n document: () => present,\n exportDocument: () => copy(present),\n revision: () => revision,\n canUndo: () => past.length > 0,\n canRedo: () => future.length > 0,\n\n undo() {\n const previous = past.pop()\n if (previous === undefined) return false\n future.push(present)\n present = previous\n revision += 1\n for (const listener of [...listeners]) listener()\n return true\n },\n\n redo() {\n const next = future.pop()\n if (next === undefined) return false\n past.push(present)\n present = next\n revision += 1\n for (const listener of [...listeners]) listener()\n return true\n },\n\n canPublish: () => verdictFor(present),\n\n subscribe(listener) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n replaceDocument(next) {\n return attempt((draft) => {\n // Assigned key by key rather than returned, because `attempt` commits\n // the draft it was given and a new object would be dropped.\n const mutable = draft as unknown as Record<string, unknown>\n for (const key of Object.keys(mutable)) delete mutable[key]\n Object.assign(draft, copy(next))\n return undefined\n })\n },\n\n insertField(location, def) {\n return attempt((draft) => {\n const container = containerAt(draft, location.parent)\n if (container === undefined) {\n return refuse('/model/fields', `No container at \"${location.parent.join('.')}\".`)\n }\n container.splice(clampIndex(location.index, container.length), 0, copy(def))\n return undefined\n })\n },\n\n removeField(keyPath) {\n return attempt((draft) => {\n const found = locate(draft, keyPath)\n if (found === undefined) return refuse('/model/fields', `No field at \"${keyPath.join('.')}\".`)\n // Read before the removal: afterwards the path cannot be resolved.\n const dataPath = dataPathOf(draft, keyPath)\n found.siblings.splice(found.index, 1)\n // A layout node placing a field that no longer exists is invalid, so\n // without this the command is simply refused and a form author cannot\n // delete a field at all once it has been arranged. Pruning it here is\n // not a convenience: an arrangement is a view of the model, and it\n // cannot outlive what it is a view of.\n if (dataPath !== undefined) unplaceEverywhere(draft, dataPath)\n return undefined\n })\n },\n\n moveField(from, to) {\n if (isPrefix(from, to.parent)) {\n return refuse(\n '/model/fields',\n `Cannot move \"${from.join('.')}\" inside itself or one of its own children.`,\n )\n }\n return attempt((draft) => {\n const found = locate(draft, from)\n if (found === undefined) return refuse('/model/fields', `No field at \"${from.join('.')}\".`)\n const [moved] = found.siblings.splice(found.index, 1)\n const container = containerAt(draft, to.parent)\n if (container === undefined) {\n return refuse('/model/fields', `No container at \"${to.parent.join('.')}\".`)\n }\n container.splice(clampIndex(to.index, container.length), 0, moved!)\n return undefined\n })\n },\n\n renameField(keyPath, newKey) {\n return attempt((draft) => {\n const found = locate(draft, keyPath)\n if (found === undefined) return refuse('/model/fields', `No field at \"${keyPath.join('.')}\".`)\n const field = found.siblings[found.index]!\n const baseline = baselineByCurrentKey.get(field.key) ?? field.key\n\n const before = dataPathOf(draft, keyPath)\n field.key = newKey\n if (newKey === baseline) {\n // Back where it started: there is nothing to migrate, and leaving a\n // renamedFrom pointing at the current key is itself invalid.\n delete field.renamedFrom\n } else {\n field.renamedFrom = baseline\n }\n // Every arrangement follows the rename. A layout addresses fields by\n // data path, so leaving them behind would make the document invalid\n // and the rename impossible — for exactly the fields most likely to\n // need one, the ones somebody has already arranged.\n const after = dataPathOf(draft, [...keyPath.slice(0, -1), newKey])\n if (before !== undefined && after !== undefined) repathEverywhere(draft, before, after)\n baselineByCurrentKey.set(newKey, baseline)\n return undefined\n })\n },\n\n setFieldProperty(keyPath, property, value) {\n return attempt((draft) => {\n const found = locate(draft, keyPath)\n if (found === undefined) return refuse('/model/fields', `No field at \"${keyPath.join('.')}\".`)\n const field = found.siblings[found.index]! as unknown as Record<string, unknown>\n if (value === undefined) delete field[property]\n else field[property] = copy(value)\n return undefined\n })\n },\n\n addRule(rule) {\n return attempt((draft) => {\n draft.logic = draft.logic ?? { rules: [] }\n draft.logic.rules.push(copy(rule))\n return undefined\n })\n },\n\n updateRule(index, rule) {\n return attempt((draft) => {\n const rules = draft.logic?.rules\n if (rules === undefined || index < 0 || index >= rules.length) {\n return refuse('/logic/rules', `No rule at index ${index}.`)\n }\n rules[index] = copy(rule)\n return undefined\n })\n },\n\n removeRule(index) {\n return attempt((draft) => {\n const rules = draft.logic?.rules\n if (rules === undefined || index < 0 || index >= rules.length) {\n return refuse('/logic/rules', `No rule at index ${index}.`)\n }\n rules.splice(index, 1)\n return undefined\n })\n },\n\n // ------------------------------------------------------------- layouts\n\n addLayout(name) {\n return attempt((draft) => {\n draft.layouts = draft.layouts ?? []\n draft.layouts.push({ name, nodes: [] })\n return undefined\n })\n },\n\n removeLayout(name) {\n return attempt((draft) => {\n const at = draft.layouts?.findIndex((layout) => layout.name === name) ?? -1\n if (at === -1 || draft.layouts === undefined) {\n return refuse('/layouts', `No layout called \"${name}\".`)\n }\n draft.layouts.splice(at, 1)\n // An empty list and no list at all mean the same thing to a renderer,\n // and only one of them survives a round trip unchanged.\n if (draft.layouts.length === 0) delete draft.layouts\n return undefined\n })\n },\n\n insertLayoutNode(location, node) {\n return attempt((draft) => {\n const container = layoutChildrenAt(draft, location.layout, location.parent)\n if (container === undefined) return noSuchContainer(location)\n container.splice(clampIndex(location.index, container.length), 0, copy(node))\n return undefined\n })\n },\n\n removeLayoutNode(address) {\n return attempt((draft) => {\n const found = locateLayout(draft, address)\n if (found === undefined) return noSuchNode(address)\n found.siblings.splice(found.index, 1)\n return undefined\n })\n },\n\n unwrapLayoutNode(address) {\n return attempt((draft) => {\n const found = locateLayout(draft, address)\n if (found === undefined) return noSuchNode(address)\n const node = found.siblings[found.index]!\n if (!isLayoutContainer(node)) {\n return refuse(\n layoutPointer(address),\n 'A field node places one field. There is nothing inside it to keep.',\n )\n }\n found.siblings.splice(found.index, 1, ...node.children)\n return undefined\n })\n },\n\n wrapLayoutNodes(layout, addresses, container, positionOf) {\n if (addresses.length < 2) {\n return refuse(\n `/layouts/${layout}`,\n 'Wrapping needs at least two nodes. Use insertLayoutNode for one.',\n )\n }\n\n if (!isLayoutContainer(container)) {\n return refuse(\n `/layouts/${layout}`,\n 'The wrapper has to be a container. A field node cannot hold anything.',\n )\n }\n\n for (let outer = 0; outer < addresses.length; outer += 1) {\n for (let inner = 0; inner < addresses.length; inner += 1) {\n if (outer === inner) continue\n const a = addresses[outer]!\n const b = addresses[inner]!\n if (samePath(a, b)) {\n // Spliced out once and inserted twice would place one field in two\n // positions, which stays syntactically fine and fails at publish.\n return refuse(\n layoutPointer({ layout, path: a }),\n 'That node is listed twice. Each one can only go in once.',\n )\n }\n if (encloses(a, b)) {\n return refuse(\n layoutPointer({ layout, path: a }),\n 'Cannot wrap a container together with something inside it.',\n )\n }\n }\n }\n\n return attempt((draft) => {\n // Collected before anything is removed, because every removal renumbers\n // the addresses after it -- the whole difficulty of editing a document\n // whose nodes have no keys.\n const taken: LayoutNode[] = []\n for (const path of addresses) {\n const found = locateLayout(draft, { layout, path })\n if (found === undefined) return noSuchNode({ layout, path })\n taken.push(found.siblings[found.index]!)\n }\n\n // Where the wrapper goes, read now while the addresses still mean what\n // the caller meant. `at` when given -- a drop wants the target's place,\n // not the dragged node's -- and otherwise the earliest in document\n // order, which is the sensible default when nothing prefers one.\n const anchorPath =\n positionOf !== undefined && addresses.some((path) => samePath(path, positionOf))\n ? positionOf\n : [...addresses].sort(comparePaths)[0]!\n const home = locateLayout(draft, { layout, path: anchorPath })\n if (home === undefined) return noSuchNode({ layout, path: anchorPath })\n const parent = home.siblings\n const at = home.index\n\n // Removed deepest-last so that removing one cannot invalidate the\n // address of another still to be removed.\n for (const path of [...addresses].sort(comparePaths).reverse()) {\n const found = locateLayout(draft, { layout, path })\n if (found === undefined) return noSuchNode({ layout, path })\n found.siblings.splice(found.index, 1)\n }\n\n parent.splice(Math.min(at, parent.length), 0, { ...container, children: taken })\n return undefined\n })\n },\n\n moveLayoutNode(from, to) {\n // A field has one place per arrangement, so moving it across layouts\n // would silently unplace it where it came from — a deletion wearing the\n // word \"move\". Two commands, deliberately, so the second is deliberate.\n if (from.layout !== to.layout) {\n return refuse(\n layoutPointer(from),\n `Cannot move between the \"${from.layout}\" and \"${to.layout}\" layouts. Remove it from one and place it in the other.`,\n )\n }\n if (encloses(from.path, to.parent)) {\n return refuse(\n layoutPointer(from),\n 'Cannot move this inside itself or one of its own children.',\n )\n }\n\n return attempt((draft) => {\n const found = locateLayout(draft, from)\n if (found === undefined) return noSuchNode(from)\n const [moved] = found.siblings.splice(found.index, 1)\n // `to.parent` addresses the container as the CALLER sees it, before\n // anything is lifted — so it is corrected here, once, in the one place\n // that knows the lift has happened. `to.index`, by contrast, already\n // counts positions after the lift, the same convention moveField uses.\n // Splitting it this way is what lets the self-containment check above\n // compare two paths in the same frame of reference.\n const container = layoutChildrenAt(draft, to.layout, shiftAfterLift(to.parent, from.path))\n if (container === undefined) return noSuchContainer(to)\n container.splice(clampIndex(to.index, container.length), 0, moved!)\n return undefined\n })\n },\n\n setLayoutNodeLabel(address, label) {\n return attempt((draft) => {\n const found = locateLayout(draft, address)\n if (found === undefined) return noSuchNode(address)\n const node = found.siblings[found.index]!\n if (!isLayoutContainer(node)) {\n return refuse(layoutPointer(address), 'A field node takes its name from the field it places.')\n }\n if (label === undefined) delete node.label\n else node.label = copy(label)\n return undefined\n })\n },\n\n setLayoutNodeProperty(address, property, value) {\n return attempt((draft) => {\n const found = locateLayout(draft, address)\n if (found === undefined) return noSuchNode(address)\n if (STRUCTURAL_LAYOUT_PROPERTIES.has(property)) {\n return refuse(\n `${layoutPointer(address)}/${property}`,\n `\"${property}\" is not a setting. It is what the node is, or where it sits, and the arrangement's own commands change it.`,\n )\n }\n if (property === '__proto__' || property === 'prototype' || property === 'constructor') {\n return refuse(\n `${layoutPointer(address)}/${property}`,\n `\"${property}\" is not allowed as a layout node setting name.`,\n )\n }\n const node = found.siblings[found.index]! as unknown as Record<string, unknown>\n if (value === undefined) Reflect.deleteProperty(node, property)\n else {\n Object.defineProperty(node, property, {\n value: copy(value) as unknown,\n writable: true,\n enumerable: true,\n configurable: true,\n })\n }\n return undefined\n })\n },\n\n validLayoutTargets(layout, what) {\n const movingPath = Array.isArray(what) ? (what as readonly number[]) : undefined\n const probe =\n movingPath === undefined ? (what as LayoutNode) : layoutNodeAt(present, layout, movingPath)\n if (probe === undefined) return []\n if (nodesOfLayout(present, layout) === undefined) return []\n\n const targets: LayoutLocation[] = []\n for (const parent of layoutContainerPaths(present, layout)) {\n // Nothing may be moved into itself or its own descendants.\n if (movingPath !== undefined && encloses(movingPath, parent)) continue\n\n // Legality is decided by TRYING the edit against the validator, the\n // same way field moves are, so these rules cannot drift from the ones\n // publish enforces — a field already placed elsewhere is refused here\n // because the validator refuses it there.\n const draft = copy(present)\n if (movingPath !== undefined) {\n const found = locateLayout(draft, { layout, path: movingPath })\n if (found === undefined) continue\n found.siblings.splice(found.index, 1)\n }\n // `parent` is the container as the caller sees it. Finding it in the\n // lifted draft needs the correction; handing it back does not, because\n // a Location addresses the document the caller is looking at.\n const container = layoutChildrenAt(draft, layout, shiftAfterLift(parent, movingPath))\n if (container === undefined) continue\n container.push(copy(probe))\n if (!verdictFor(draft).valid) continue\n container.pop()\n\n // Every position, not just the end: offering only the end makes the\n // keyboard path weaker than a drag, which is what SC 2.5.7 forbids.\n for (let index = 0; index <= container.length; index += 1) {\n // Within the container it came from, the slot it already occupies is\n // not a move. After the lift, that is the index it was lifted from.\n if (\n movingPath !== undefined &&\n sameLayoutPath(parent, movingPath.slice(0, -1)) &&\n index === movingPath[movingPath.length - 1]\n ) {\n continue\n }\n targets.push({ layout, parent, index })\n }\n }\n return targets\n },\n\n unplacedFields(layout) {\n return unreferencedPaths(present, layout) ?? []\n },\n\n upgradeSpec(to = CURRENT_SPEC_VERSION) {\n return attempt((draft) => {\n if (draft.specVersion > to) {\n return refuse(\n '/specVersion',\n `This form is written against spec ${draft.specVersion} and cannot go back to ${to}: whatever the newer version added has nowhere to go, so it would be data loss rather than a change.`,\n )\n }\n draft.specVersion = to\n return undefined\n })\n },\n\n validTargets(what) {\n const movingPath = Array.isArray(what) ? (what as readonly string[]) : undefined\n const probe: FieldDef =\n movingPath === undefined\n ? (what as FieldDef)\n : (locate(present, movingPath)?.siblings[locate(present, movingPath)!.index] as FieldDef)\n if (probe === undefined) return []\n\n const targets: Location[] = []\n for (const parent of containerPaths(present)) {\n // A container cannot be moved into itself or its own descendants.\n if (movingPath !== undefined && isPrefix(movingPath, parent)) continue\n\n // Legality is decided by TRYING the edit against the validator, so\n // these rules can never drift from the ones publish enforces.\n const draft = copy(present)\n if (movingPath !== undefined) {\n const found = locate(draft, movingPath)\n if (found === undefined) continue\n found.siblings.splice(found.index, 1)\n }\n const container = containerAt(draft, parent)\n if (container === undefined) continue\n container.push(movingPath === undefined ? withUniqueKey(copy(probe), draft) : copy(probe))\n\n // Legality is tested once per container, at the end, and then every\n // position in that container is offered. Nothing the validator checks\n // depends on where among its siblings a field sits — duplicate keys,\n // nesting depth and rule targets are all position-blind — so testing\n // each index separately would run the validator n times to learn the\n // same thing.\n //\n // Offering only the last position would be cheaper still, and was what\n // this did first. It also made the keyboard path weaker than a drag:\n // you could move a field to the end of a group but never between two\n // of its fields, which is not the equivalent function WCAG 2.2 SC\n // 2.5.7 asks for.\n if (!verdictFor(draft).valid) continue\n for (let index = 0; index < container.length; index += 1) {\n targets.push({ parent, index })\n }\n }\n return targets\n },\n }\n}\n\n// ---------------------------------------------------------------- navigation\n\ninterface Located {\n siblings: FieldDef[]\n index: number\n}\n\n/** The child list of the container at `keyPath` — the root list for []. */\nfunction containerAt(document: FormSchema, keyPath: readonly string[]): FieldDef[] | undefined {\n if (keyPath.length === 0) return document.model.fields\n const found = locate(document, keyPath)\n if (found === undefined) return undefined\n const field = found.siblings[found.index]!\n if (!CONTAINER_TYPES.has(field.type)) return undefined\n field.fields = field.fields ?? []\n return field.fields\n}\n\nfunction locate(document: FormSchema, keyPath: readonly string[]): Located | undefined {\n if (keyPath.length === 0) return undefined\n let siblings: FieldDef[] = document.model.fields\n for (let depth = 0; depth < keyPath.length; depth++) {\n const index = siblings.findIndex((field) => field.key === keyPath[depth])\n if (index === -1) return undefined\n if (depth === keyPath.length - 1) return { siblings, index }\n const next = siblings[index]!.fields\n if (next === undefined) return undefined\n siblings = next\n }\n return undefined\n}\n\n/** Every container's key path, root first. */\nfunction containerPaths(document: FormSchema): string[][] {\n const paths: string[][] = [[]]\n const walk = (fields: readonly FieldDef[], prefix: string[]): void => {\n for (const field of fields) {\n if (!CONTAINER_TYPES.has(field.type)) continue\n const here = [...prefix, field.key]\n paths.push(here)\n walk(field.fields ?? [], here)\n }\n }\n walk(document.model.fields, [])\n return paths\n}\n\n// ----------------------------------------------- keeping layouts in step\n\n/**\n * The data path of a field, which is not its key path.\n *\n * A page contributes no segment — a layout arranges what a person sees, and\n * `page1.email` is not what the engine calls that answer. Undefined when the\n * key path reaches nothing, or passes through something that holds no fields.\n */\n/**\n * Layout node properties that are not settings.\n *\n * `kind` is what the node IS — writing a new one would turn a table into a section\n * without moving its children. `children` is structure, which the tree edits. `path`\n * is which answer a placement places, chosen when the placement is added and governed\n * by the one-place-per-field rule.\n */\nconst STRUCTURAL_LAYOUT_PROPERTIES = new Set(['kind', 'children', 'path'])\n\nfunction dataPathOf(document: FormSchema, keyPath: readonly string[]): string | undefined {\n const segments: string[] = []\n let fields: readonly FieldDef[] = document.model.fields\n\n for (const [depth, key] of keyPath.entries()) {\n const field = fields.find((candidate) => candidate.key === key)\n if (field === undefined) return undefined\n if (field.type !== 'page') segments.push(key)\n if (depth === keyPath.length - 1) return segments.join('.')\n fields = field.fields ?? []\n }\n return undefined\n}\n\n/** Whether `path` is `prefix` itself or a field inside it. */\nfunction underPath(path: string, prefix: string): boolean {\n return path === prefix || path.startsWith(`${prefix}.`)\n}\n\n/** Drop every layout node placing `dataPath`, or anything inside it. */\nfunction unplaceEverywhere(draft: FormSchema, dataPath: string): void {\n for (const layout of draft.layouts ?? []) {\n layout.nodes = pruned(layout.nodes, dataPath)\n }\n}\n\nfunction pruned(nodes: readonly LayoutNode[], dataPath: string): LayoutNode[] {\n const kept: LayoutNode[] = []\n for (const node of nodes) {\n if (node.kind === 'field' || node.kind === 'qrcode') {\n // A code goes when the answer it encodes goes, exactly as its placement does:\n // what would remain is a node drawing a picture of nothing, and the validator\n // would then refuse the document the builder had just produced.\n if (!underPath(node.path, dataPath)) kept.push(node)\n continue\n }\n // The container stays even when it empties. Removing one field should not\n // silently take a row with it and rearrange everything beside it.\n kept.push({ ...node, children: pruned(layoutChildren(node) as LayoutNode[], dataPath) })\n }\n return kept\n}\n\n/** Point every layout node at the new path, including fields inside a group. */\nfunction repathEverywhere(draft: FormSchema, before: string, after: string): void {\n const walk = (nodes: LayoutNode[]): void => {\n for (const [index, node] of nodes.entries()) {\n if (node.kind === 'field' || node.kind === 'qrcode') {\n // A code follows a rename. A declared rename keeps the answer, so a code of it\n // must keep encoding the same answer -- leaving the old path behind would turn\n // a rename into a silently broken code.\n if (underPath(node.path, before)) {\n nodes[index] = { ...node, path: after + node.path.slice(before.length) }\n }\n continue\n }\n walk(layoutChildren(node) as LayoutNode[])\n }\n }\n for (const layout of draft.layouts ?? []) walk(layout.nodes)\n}\n\n// ----------------------------------------------------------- layout helpers\n\ninterface LocatedNode {\n siblings: LayoutNode[]\n index: number\n}\n\nfunction locateLayout(document: FormSchema, address: LayoutAddress): LocatedNode | undefined {\n if (address.path.length === 0) return undefined\n const siblings = layoutChildrenAt(document, address.layout, address.path.slice(0, -1))\n if (siblings === undefined) return undefined\n const index = address.path[address.path.length - 1]!\n if (index < 0 || index >= siblings.length) return undefined\n return { siblings, index }\n}\n\nfunction layoutPointer(address: LayoutAddress): string {\n return `/layouts/${address.layout}/nodes/${address.path.join('/')}`\n}\n\nfunction noSuchNode(address: LayoutAddress): Refusal {\n return {\n ok: false,\n path: layoutPointer(address),\n message: `Nothing at that position in the \"${address.layout}\" layout.`,\n }\n}\n\nfunction noSuchContainer(location: LayoutLocation): Refusal {\n return {\n ok: false,\n path: `/layouts/${location.layout}/nodes/${location.parent.join('/')}`,\n message:\n location.parent.length === 0\n ? `No layout called \"${location.layout}\".`\n : `Nothing at that position in the \"${location.layout}\" layout holds other nodes.`,\n }\n}\n\n/**\n * A container path, re-read after a node has been lifted out.\n *\n * Both paths index the document as it stood. If the lifted node shared a\n * container with an ancestor of this path and sat before it, that ancestor has\n * shifted up one — and walking the unshifted path now arrives at a different\n * node entirely, which is the silent kind of wrong.\n */\nfunction shiftAfterLift(\n parent: readonly number[],\n lifted: readonly number[] | undefined,\n): number[] {\n if (lifted === undefined) return [...parent]\n const depth = lifted.length - 1\n if (parent.length <= depth) return [...parent]\n // Only when the lift happened in an ancestor's own container.\n for (let at = 0; at < depth; at += 1) if (parent[at] !== lifted[at]) return [...parent]\n\n const shifted = [...parent]\n if (shifted[depth]! > lifted[depth]!) shifted[depth] = shifted[depth]! - 1\n return shifted\n}\n\nfunction sameLayoutPath(a: readonly number[], b: readonly number[]): boolean {\n return a.length === b.length && a.every((step, at) => b[at] === step)\n}\n\nfunction isPrefix(prefix: readonly string[], candidate: readonly string[]): boolean {\n if (prefix.length > candidate.length) return false\n return prefix.every((segment, index) => candidate[index] === segment)\n}\n\nfunction clampIndex(index: number, length: number): number {\n if (!Number.isInteger(index) || index < 0) return 0\n return Math.min(index, length)\n}\n\n/** A palette probe must not fail purely because its placeholder key is taken —\n * validTargets answers \"may this SHAPE go here\", not \"is this key free\". */\nfunction withUniqueKey(def: FieldDef, document: FormSchema): FieldDef {\n const taken = new Set<string>()\n const walk = (fields: readonly FieldDef[]): void => {\n for (const field of fields) {\n taken.add(field.key)\n walk(field.fields ?? [])\n }\n }\n walk(document.model.fields)\n\n let candidate = def.key\n let counter = 1\n while (taken.has(candidate)) candidate = `${def.key}_${counter++}`\n return { ...def, key: candidate }\n}\n\n// ------------------------------------------------------------------ baseline\n\nfunction indexBaseline(fields: readonly FieldDef[], into: Map<FieldDef, string>): void {\n for (const field of fields) {\n into.set(field, field.key)\n indexBaseline(field.fields ?? [], into)\n }\n}\n\nfunction seedBaselineByKey(fields: readonly FieldDef[], into: Map<string, string>): void {\n for (const field of fields) {\n // A field opened with a renamedFrom already declared keeps pointing there:\n // that is the key the collected answers actually live under.\n into.set(field.key, field.renamedFrom ?? field.key)\n seedBaselineByKey(field.fields ?? [], into)\n }\n}\n\n// --------------------------------------------------------------------- utils\n\nfunction verdictFor(document: FormSchema): { valid: boolean; errors: SchemaError[] } {\n const result = validateSchema(document)\n return result.valid ? { valid: true, errors: [] } : { valid: false, errors: result.errors }\n}\n\n/** JSON round-trip: a form document is JSON by definition, and this package\n * runs without structuredClone in its lib on purpose. */\nfunction copy<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T\n}\n\nfunction deepFreeze<T>(value: T): T {\n if (value === null || typeof value !== 'object') return value\n for (const nested of Object.values(value as Record<string, unknown>)) deepFreeze(nested)\n return Object.freeze(value)\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formancy/builder-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "The headless form-builder document engine: schema editing as commands with undo, selection and valid-target computation.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -17,7 +17,8 @@
17
17
  "NOTICE"
18
18
  ],
19
19
  "dependencies": {
20
- "@formancy/spec": "0.1.0"
20
+ "@formancy/core": "0.2.0",
21
+ "@formancy/spec": "0.2.0"
21
22
  },
22
23
  "devDependencies": {
23
24
  "@arethetypeswrong/cli": "^0.18.5",
@@ -26,7 +27,8 @@
26
27
  "tsdown": "^0.23.0",
27
28
  "typescript": "~6.0.3",
28
29
  "unrun": "^0.3.1",
29
- "vitest": "^5.0.1"
30
+ "vitest": "^5.0.1",
31
+ "@vitest/coverage-v8": "^5.0.1"
30
32
  },
31
33
  "publishConfig": {
32
34
  "access": "public"
@@ -40,6 +42,7 @@
40
42
  "bugs": "https://github.com/sharkysan/formancy.ai/issues",
41
43
  "scripts": {
42
44
  "build": "tsdown",
45
+ "test:coverage": "vitest run --coverage",
43
46
  "test": "vitest run",
44
47
  "typecheck": "tsc --noEmit",
45
48
  "check:pkg": "publint && attw --pack . --profile esm-only"